Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions benchmarks/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ react {
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = false
def flagsPrototypeEnabled = System.getenv('DD_FLAGS_KOTLIN_PROTOTYPE_PATH') != null

/**
* The preferred build flavor of JavaScriptCore (JSC)
Expand Down Expand Up @@ -90,6 +91,7 @@ android {
versionCode 1
versionName "1.0"
buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString())
buildConfigField("boolean", "DD_FLAGS_PROTOTYPE_ENABLED", flagsPrototypeEnabled.toString())
}
signingConfigs {
debug {
Expand All @@ -111,13 +113,22 @@ android {
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
if (flagsPrototypeEnabled) {
sourceSets.main.java.srcDir('src/flagsPrototype/java')
sourceSets.main.res.srcDir('src/flagsPrototype/res')
sourceSets.release.manifest.srcFile('src/flagsPrototype/AndroidManifest.xml')
}
}

repositories {

}

dependencies {
if (flagsPrototypeEnabled) {
implementation 'com.datadog.flags.benchmark:rules-evaluation-prototype:0.0.1-prototype'
implementation 'com.datadoghq:dd-sdk-android-flags:3.13.1'
}
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:networkSecurityConfig="@xml/flags_benchmark_network_security" />
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2026-Present Datadog, Inc.
*/

package com.flagsbenchmark

import android.os.Build
import android.os.Process
import com.benchmarkrunner.BuildConfig
import com.benchmarkvitals.NativeFlagsBenchmarkSpec
import com.datadog.flags.benchmark.FlagsBenchmark
import com.datadog.android.Datadog
import com.datadog.android._InternalProxy
import com.datadog.android.core.configuration.Configuration
import com.datadog.android.core.configuration.BatchSize
import com.datadog.android.core.configuration.UploadFrequency
import com.datadog.android.flags.Flags
import com.datadog.android.flags.FlagsClient
import com.datadog.android.flags.FlagsConfiguration
import com.datadog.android.privacy.TrackingConsent
import com.datadog.reactnative.DdFlagsImplementation
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.PromiseImpl
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap
import com.facebook.react.module.annotations.ReactModule
import org.json.JSONObject
import java.io.File
import java.util.UUID
import java.util.concurrent.Executors

@ReactModule(name = FlagsBenchmarkModule.NAME)
class FlagsBenchmarkModule(private val context: ReactApplicationContext) : NativeFlagsBenchmarkSpec(context) {
private val cache = File(context.cacheDir, "flags-benchmark-${UUID.randomUUID()}.bin")
private val evaluator = FlagsBenchmark(cache)
private val lock = Any()
private val queue = Executors.newSingleThreadExecutor()
private val emulator = Build.HARDWARE in listOf("ranchu", "goldfish")
private var initialization: Map<String, Any?>? = null
private val tracking by lazy { DdFlagsImplementation() }

private fun settings(): Map<String, Any?> {
val intent = requireNotNull(context.currentActivity?.intent)
val mode = intent.getStringExtra("flagsExperiment") ?: ""
if ((mode in listOf("none", "bridge-only", "exposures", "evaluations", "both") || mode.startsWith("batching-")) && initialization == null) {
check(!BuildConfig.DEBUG) { "Tracking requires a Release emulator build" }
val port = intent.getIntExtra("flagsSinkPort", 0)
require(port in 1..65535) { "Missing loopback collector port" }
val endpoint = "http://127.0.0.1:$port"
val config = Configuration.Builder("synthetic-benchmark-only", "benchmark", "", "flags-benchmark")
.setBatchSize(BatchSize.SMALL)
.setUploadFrequency(UploadFrequency.FREQUENT)
val core = requireNotNull(Datadog.initialize(context, _InternalProxy.allowClearTextHttp(config).build(), TrackingConsent.GRANTED))
Datadog.clearAllData(core)
Flags.enable(FlagsConfiguration.Builder()
.trackExposures(mode == "exposures" || mode == "both" || mode.startsWith("batching-"))
.trackEvaluations(mode == "evaluations" || mode == "both" || mode.startsWith("batching-"))
.rumIntegrationEnabled(false)
.evaluationFlushInterval(1000)
.useCustomFlagEndpoint("$endpoint/config")
.useCustomExposureEndpoint("$endpoint/exposures")
.useCustomEvaluationEndpoint("$endpoint/evaluations")
.build(), core)
val client = FlagsClient.Builder("benchmark-preflight", core).build()
check(client.javaClass.simpleName == "DatadogFlagsClient") { "Native flags initialization failed" }
initialization = mapOf("coreType" to core.javaClass.simpleName, "clientType" to client.javaClass.simpleName)
}
return mapOf("mode" to mode, "initialization" to initialization,
"autorun" to (intent.getStringExtra("flagsRun") ?: "smoke"))
}

private fun snapshot(): Map<String, Any?> {
// Whole-process RSS, not evaluator heap or allocation peak.
val rss = File("/proc/${Process.myPid()}/status").readLines()
.first { it.startsWith("VmRSS:") }.trim().split(Regex("\\s+"))[1].toLong() * 1024
return mapOf("residentBytes" to rss, "thermalState" to "not measured")
}

override fun getName(): String = NAME

override fun trackBatch(records: ReadableArray, promise: Promise) {
try {
check(emulator && !BuildConfig.DEBUG && initialization != null)
require(records.size() in 1..25)
for (index in 0 until records.size()) {
val record = requireNotNull(records.getMap(index))
var resolved = false
var failure: String? = null
tracking.trackEvaluation(
requireNotNull(record.getString("client")), requireNotNull(record.getString("key")),
requireNotNull(record.getMap("flag")), requireNotNull(record.getString("targetingKey")),
requireNotNull(record.getMap("attributes")),
PromiseImpl(Callback { resolved = true }, Callback { args -> failure = args.contentToString() })
)
check(failure == null && resolved) { failure ?: "Tracking implementation did not acknowledge synchronously" }
}
promise.resolve(null)
} catch (exception: Exception) {
promise.reject("BATCH_TRACKING_FAILED", exception)
}
}

override fun runSync(request: ReadableMap): WritableMap = Arguments.makeNativeMap(execute(request.toHashMap()))

override fun runAsync(request: ReadableMap, promise: Promise) {
val input = request.toHashMap()
queue.execute { promise.resolve(Arguments.makeNativeMap(execute(input))) }
}

private fun execute(request: Map<String, Any?>): Map<String, Any?> = synchronized(lock) {
try {
// Never run a benchmark on a physical Android device through this prototype.
check(emulator) { "Android benchmark is emulator-only" }
when (request["op"]) {
"metadata" -> mapOf(
"platform" to "android", "os" to "Android ${Build.VERSION.RELEASE} / API ${Build.VERSION.SDK_INT}",
"machine" to Build.MODEL, "abis" to Build.SUPPORTED_ABIS.toList(),
"simulator" to true, "nativeDebug" to BuildConfig.DEBUG,
"newArchitecture" to BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
"protobufJava" to "3.25.5", "kotlin" to KotlinVersion.CURRENT.toString(), "datadogFlags" to "3.13.1",
"nativeWorkload" to "client-protobuf boolean benchmark subset",
"vm" to System.getProperty("java.vm.version"), "thermalState" to "not measured"
)
"experimentSettings" -> settings()
"experimentSnapshot" -> snapshot()
"experimentFlush" -> {
check(initialization != null) { "Native tracking not initialized" }
// Test-only drain after the aggregation timer; called on the worker queue.
Datadog._internalProxy().flushAndShutdownExecutors()
snapshot() + mapOf("initialization" to initialization)
}
"saveReport" -> {
val json = request["json"] as String
JSONObject(json)
val directory = requireNotNull(context.getExternalFilesDir(null))
val report = File(directory, "flags-benchmark-result.json")
val temporary = File(directory, "flags-benchmark-result.tmp")
temporary.writeText(json)
check(temporary.renameTo(report))
mapOf("path" to report.path)
}
else -> evaluator.run(request)
}
} catch (exception: Exception) {
mapOf("benchmarkError" to "${exception.javaClass.simpleName}: ${exception.message}")
}
}

override fun invalidate() {
queue.execute { synchronized(lock) { cache.delete() } }
queue.shutdown()
super.invalidate()
}

companion object { const val NAME = "FlagsBenchmark" }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2026-Present Datadog, Inc.
*/

package com.flagsbenchmark

import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider

class FlagsBenchmarkPackage : BaseReactPackage() {
override fun getModule(name: String, context: ReactApplicationContext): NativeModule? =
if (name == FlagsBenchmarkModule.NAME) FlagsBenchmarkModule(context) else null

override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(FlagsBenchmarkModule.NAME to ReactModuleInfo(
FlagsBenchmarkModule.NAME, FlagsBenchmarkModule::class.java.name,
false, false, false, true
))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">127.0.0.1</domain>
</domain-config>
</network-security-config>
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ class MainApplication : Application(), ReactApplication {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(BenchmarkVitalsPackage())
if (BuildConfig.DD_FLAGS_PROTOTYPE_ENABLED) {
// Optional local prototype; reflection happens once, never on the evaluation path.
add(Class.forName("com.flagsbenchmark.FlagsBenchmarkPackage")
.getDeclaredConstructor().newInstance() as ReactPackage)
}
}

override fun getJSMainModuleName(): String = "index"
Expand Down
5 changes: 5 additions & 0 deletions benchmarks/android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autoli
rootProject.name = 'BenchmarkRunner'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')

def flagsPrototype = System.getenv('DD_FLAGS_KOTLIN_PROTOTYPE_PATH')
if (flagsPrototype) {
includeBuild(flagsPrototype)
}
4 changes: 4 additions & 0 deletions benchmarks/flags-smoke.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Headless correctness run only, not a performance report.
BENCH_SCENARIO=flags
BENCH_RUN_TYPE=baseline
BENCH_FLAGS_AUTORUN=smoke
3 changes: 3 additions & 0 deletions benchmarks/flags.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Synthetic benchmark only. No Datadog credentials or telemetry initialization.
BENCH_SCENARIO=flags
BENCH_RUN_TYPE=baseline
21 changes: 21 additions & 0 deletions benchmarks/flags.jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2026-Present Datadog, Inc.
*/

module.exports = {
watchman: false,
testEnvironment: 'node',
testMatch: ['<rootDir>/src/flags/**/*.test.ts'],
transform: {
'^.+\\.[jt]sx?$': [
'babel-jest',
{
babelrc: false,
configFile: false,
presets: ['module:@react-native/babel-preset'],
},
],
},
};
19 changes: 19 additions & 0 deletions benchmarks/ios/BenchmarkRunner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
043403262DCB9CD700803AF8 /* BenchmarkVitals.mm in Sources */ = {isa = PBXBuildFile; fileRef = 043403242DCB9CD700803AF8 /* BenchmarkVitals.mm */; };
04576C962DCBAF900078DCAB /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 04576C952DCBAF900078DCAB /* main.m */; };
04726DD72F46402A008D1A48 /* TracerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04726DD62F46402A008D1A48 /* TracerProvider.swift */; };
0BFD883AEE0A41CC521C5662 /* FlagsBenchmark.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2FED7A81171836D1D57F12AB /* FlagsBenchmark.mm */; settings = {COMPILER_FLAGS = "-fmodules -fcxx-modules"; }; };
B7A222C1BA8C487EA3BD6001 /* FlagsExperimentSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A222C1BA8C487EA3BD6002 /* FlagsExperimentSupport.swift */; };
0C80B921A6F3F58F76C31292 /* libPods-BenchmarkRunner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-BenchmarkRunner.a */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
20A397FC280595A8A9851556 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
Expand All @@ -40,11 +42,14 @@
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = BenchmarkRunner/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = BenchmarkRunner/Info.plist; sourceTree = "<group>"; };
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = BenchmarkRunner/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
2FED7A81171836D1D57F12AB /* FlagsBenchmark.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = FlagsBenchmark.mm; sourceTree = "<group>"; };
B7A222C1BA8C487EA3BD6002 /* FlagsExperimentSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagsExperimentSupport.swift; sourceTree = "<group>"; };
3B4392A12AC88292D35C810B /* Pods-BenchmarkRunner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BenchmarkRunner.debug.xcconfig"; path = "Target Support Files/Pods-BenchmarkRunner/Pods-BenchmarkRunner.debug.xcconfig"; sourceTree = "<group>"; };
5709B34CF0A7D63546082F79 /* Pods-BenchmarkRunner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BenchmarkRunner.release.xcconfig"; path = "Target Support Files/Pods-BenchmarkRunner/Pods-BenchmarkRunner.release.xcconfig"; sourceTree = "<group>"; };
5DCACB8F33CDC322A6C60F78 /* libPods-BenchmarkRunner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-BenchmarkRunner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = BenchmarkRunner/AppDelegate.swift; sourceTree = "<group>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = BenchmarkRunner/LaunchScreen.storyboard; sourceTree = "<group>"; };
A3B9C892D474AFE597364EE6 /* FlagsBenchmark.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = FlagsBenchmark.h; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -131,6 +136,7 @@
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
BBD78D7AC51CEA395F1C20DB /* Pods */,
BEF778B92318D45B0AE84BA5 /* FlagsBenchmark */,
);
indentWidth = 2;
sourceTree = "<group>";
Expand All @@ -154,6 +160,17 @@
path = Pods;
sourceTree = "<group>";
};
BEF778B92318D45B0AE84BA5 /* FlagsBenchmark */ = {
isa = PBXGroup;
children = (
A3B9C892D474AFE597364EE6 /* FlagsBenchmark.h */,
2FED7A81171836D1D57F12AB /* FlagsBenchmark.mm */,
B7A222C1BA8C487EA3BD6002 /* FlagsExperimentSupport.swift */,
);
name = FlagsBenchmark;
path = FlagsBenchmark;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -313,6 +330,8 @@
04576C962DCBAF900078DCAB /* main.m in Sources */,
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
04726DD72F46402A008D1A48 /* TracerProvider.swift in Sources */,
0BFD883AEE0A41CC521C5662 /* FlagsBenchmark.mm in Sources */,
B7A222C1BA8C487EA3BD6001 /* FlagsExperimentSupport.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
2 changes: 2 additions & 0 deletions benchmarks/ios/BenchmarkRunner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class AppDelegate: RCTAppDelegate {
// They will be passed down to the ViewController used by React Native.
self.initialProps = [:]

FlagsExperimentSupport.prepare()

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

Expand Down
15 changes: 15 additions & 0 deletions benchmarks/ios/FlagsBenchmark/FlagsBenchmark.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2026-Present Datadog, Inc.
*/

#import <Foundation/Foundation.h>
#ifdef RCT_NEW_ARCH_ENABLED
#import "BenchmarkVitalsSpec.h"
@interface FlagsBenchmark : NSObject <NativeFlagsBenchmarkSpec>
#else
#import <React/RCTBridgeModule.h>
@interface FlagsBenchmark : NSObject <RCTBridgeModule>
#endif
@end
Loading
Loading