From 30bdad5d6e17822f05969028a28b3a186c0a7bf4 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Sun, 19 Jul 2026 13:21:47 -0400 Subject: [PATCH] demo: Add cache GC measurement harnesses across Node, browser, and Android Establish opt-in, provenance-verified baselines for data-client cache GC interaction cost before changing GCPolicy behavior. Co-authored-by: Cursor --- .circleci/config.yml | 1 + examples/benchmark-native/.eslintrc.js | 4 + examples/benchmark-native/.gitignore | 85 + examples/benchmark-native/.prettierrc.js | 5 + examples/benchmark-native/.watchmanconfig | 1 + examples/benchmark-native/App.tsx | 199 ++ examples/benchmark-native/README.md | 101 + .../__tests__/build-identity.test.ts | 127 + .../__tests__/frame-semantics.test.ts | 55 + .../__tests__/gcHarness.test.ts | 91 + .../__tests__/rafCollectSequence.test.ts | 77 + .../__tests__/readJsHeap.test.ts | 43 + .../__tests__/scenario-and-frames.test.ts | 100 + .../__tests__/summarize.test.ts | 86 + .../__tests__/validate-config.test.ts | 139 ++ .../benchmark-native/android/app/build.gradle | 118 + .../android/app/debug.keystore | Bin 0 -> 2257 bytes .../android/app/proguard-rules.pro | 10 + .../android/app/src/main/AndroidManifest.xml | 27 + .../benchmarknative/BenchNativeModule.kt | 385 +++ .../benchmarknative/BenchNativePackage.kt | 17 + .../benchmarknative/MainActivity.kt | 30 + .../benchmarknative/MainApplication.kt | 26 + .../res/drawable/rn_edit_text_material.xml | 37 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 9 + .../benchmark-native/android/build.gradle | 21 + .../android/gradle.properties | 44 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 46175 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + examples/benchmark-native/android/gradlew | 248 ++ examples/benchmark-native/android/gradlew.bat | 98 + .../benchmark-native/android/settings.gradle | 6 + examples/benchmark-native/app.json | 4 + examples/benchmark-native/babel.config.js | 3 + examples/benchmark-native/index.js | 9 + examples/benchmark-native/jest.config.js | 5 + examples/benchmark-native/metro.config.js | 23 + examples/benchmark-native/package.json | 44 + .../scripts/build-android-release.sh | 28 + .../scripts/build-identity.cjs | 101 + .../scripts/build-manifest.cjs | 245 ++ .../scripts/collect-report.sh | 211 ++ .../benchmark-native/scripts/run-matrix.sh | 94 + .../scripts/validate-config.sh | 55 + examples/benchmark-native/src/BenchNative.ts | 77 + examples/benchmark-native/src/frames.ts | 167 ++ examples/benchmark-native/src/gcHarness.ts | 382 +++ examples/benchmark-native/src/globals.d.ts | 17 + examples/benchmark-native/src/measure.ts | 226 ++ .../src/rafCollectSequence.ts | 169 ++ examples/benchmark-native/src/report.ts | 246 ++ .../benchmark-native/src/runOrchestration.ts | 94 + examples/benchmark-native/src/scenario.ts | 106 + examples/benchmark-native/src/types.ts | 189 ++ .../benchmark-native/src/validateConfig.ts | 182 ++ examples/benchmark-native/tsconfig.json | 12 + examples/benchmark-react/.gitignore | 2 + examples/benchmark-react/AGENTS.md | 6 +- examples/benchmark-react/README.md | 31 +- .../benchmark-react/bench/build-manifest.ts | 277 +++ .../bench/gc-interaction-metrics.test.ts | 291 +++ .../bench/gc-provenance.test.ts | 125 + examples/benchmark-react/bench/gc-report.ts | 273 +++ examples/benchmark-react/bench/runner.ts | 367 ++- examples/benchmark-react/bench/scenarios.ts | 69 +- examples/benchmark-react/bench/validate.ts | 182 ++ examples/benchmark-react/package.json | 7 +- .../src/data-client/gcBrowserHarness.ts | 548 +++++ .../src/data-client/gcInteractionMetrics.ts | 169 ++ .../src/data-client/gcInteractionProbe.ts | 235 ++ .../benchmark-react/src/data-client/index.tsx | 34 +- examples/benchmark-react/src/shared/types.ts | 91 +- examples/benchmark/README.md | 104 +- examples/benchmark/gc-build-manifest.js | 502 ++++ examples/benchmark/gc-policy-scenarios.js | 548 +++++ examples/benchmark/gc-policy.js | 401 ++++ examples/benchmark/package.json | 4 +- package.json | 1 + plans/garbage-collection.md | 130 +- yarn.lock | 2072 ++++++++++++++++- 91 files changed, 11235 insertions(+), 123 deletions(-) create mode 100644 examples/benchmark-native/.eslintrc.js create mode 100644 examples/benchmark-native/.gitignore create mode 100644 examples/benchmark-native/.prettierrc.js create mode 100644 examples/benchmark-native/.watchmanconfig create mode 100644 examples/benchmark-native/App.tsx create mode 100644 examples/benchmark-native/README.md create mode 100644 examples/benchmark-native/__tests__/build-identity.test.ts create mode 100644 examples/benchmark-native/__tests__/frame-semantics.test.ts create mode 100644 examples/benchmark-native/__tests__/gcHarness.test.ts create mode 100644 examples/benchmark-native/__tests__/rafCollectSequence.test.ts create mode 100644 examples/benchmark-native/__tests__/readJsHeap.test.ts create mode 100644 examples/benchmark-native/__tests__/scenario-and-frames.test.ts create mode 100644 examples/benchmark-native/__tests__/summarize.test.ts create mode 100644 examples/benchmark-native/__tests__/validate-config.test.ts create mode 100644 examples/benchmark-native/android/app/build.gradle create mode 100644 examples/benchmark-native/android/app/debug.keystore create mode 100644 examples/benchmark-native/android/app/proguard-rules.pro create mode 100644 examples/benchmark-native/android/app/src/main/AndroidManifest.xml create mode 100644 examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/BenchNativeModule.kt create mode 100644 examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/BenchNativePackage.kt create mode 100644 examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/MainActivity.kt create mode 100644 examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/MainApplication.kt create mode 100644 examples/benchmark-native/android/app/src/main/res/drawable/rn_edit_text_material.xml create mode 100644 examples/benchmark-native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 examples/benchmark-native/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 examples/benchmark-native/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 examples/benchmark-native/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 examples/benchmark-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 examples/benchmark-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 examples/benchmark-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 examples/benchmark-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 examples/benchmark-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 examples/benchmark-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 examples/benchmark-native/android/app/src/main/res/values/strings.xml create mode 100644 examples/benchmark-native/android/app/src/main/res/values/styles.xml create mode 100644 examples/benchmark-native/android/build.gradle create mode 100644 examples/benchmark-native/android/gradle.properties create mode 100644 examples/benchmark-native/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/benchmark-native/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 examples/benchmark-native/android/gradlew create mode 100644 examples/benchmark-native/android/gradlew.bat create mode 100644 examples/benchmark-native/android/settings.gradle create mode 100644 examples/benchmark-native/app.json create mode 100644 examples/benchmark-native/babel.config.js create mode 100644 examples/benchmark-native/index.js create mode 100644 examples/benchmark-native/jest.config.js create mode 100644 examples/benchmark-native/metro.config.js create mode 100644 examples/benchmark-native/package.json create mode 100644 examples/benchmark-native/scripts/build-android-release.sh create mode 100644 examples/benchmark-native/scripts/build-identity.cjs create mode 100644 examples/benchmark-native/scripts/build-manifest.cjs create mode 100644 examples/benchmark-native/scripts/collect-report.sh create mode 100644 examples/benchmark-native/scripts/run-matrix.sh create mode 100644 examples/benchmark-native/scripts/validate-config.sh create mode 100644 examples/benchmark-native/src/BenchNative.ts create mode 100644 examples/benchmark-native/src/frames.ts create mode 100644 examples/benchmark-native/src/gcHarness.ts create mode 100644 examples/benchmark-native/src/globals.d.ts create mode 100644 examples/benchmark-native/src/measure.ts create mode 100644 examples/benchmark-native/src/rafCollectSequence.ts create mode 100644 examples/benchmark-native/src/report.ts create mode 100644 examples/benchmark-native/src/runOrchestration.ts create mode 100644 examples/benchmark-native/src/scenario.ts create mode 100644 examples/benchmark-native/src/types.ts create mode 100644 examples/benchmark-native/src/validateConfig.ts create mode 100644 examples/benchmark-native/tsconfig.json create mode 100644 examples/benchmark-react/.gitignore create mode 100644 examples/benchmark-react/bench/build-manifest.ts create mode 100644 examples/benchmark-react/bench/gc-interaction-metrics.test.ts create mode 100644 examples/benchmark-react/bench/gc-provenance.test.ts create mode 100644 examples/benchmark-react/bench/gc-report.ts create mode 100644 examples/benchmark-react/src/data-client/gcBrowserHarness.ts create mode 100644 examples/benchmark-react/src/data-client/gcInteractionMetrics.ts create mode 100644 examples/benchmark-react/src/data-client/gcInteractionProbe.ts create mode 100644 examples/benchmark/gc-build-manifest.js create mode 100644 examples/benchmark/gc-policy-scenarios.js create mode 100644 examples/benchmark/gc-policy.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 84ef01f02d34..61998aa5babf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -90,6 +90,7 @@ jobs: - run: name: Add examples/* to yarn workspace command: | + npm pkg delete workspaces[7] npm pkg delete workspaces[6] npm pkg delete workspaces[5] npm pkg delete workspaces[4] diff --git a/examples/benchmark-native/.eslintrc.js b/examples/benchmark-native/.eslintrc.js new file mode 100644 index 000000000000..187894b6af25 --- /dev/null +++ b/examples/benchmark-native/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: '@react-native', +}; diff --git a/examples/benchmark-native/.gitignore b/examples/benchmark-native/.gitignore new file mode 100644 index 000000000000..70fbab4ddb4b --- /dev/null +++ b/examples/benchmark-native/.gitignore @@ -0,0 +1,85 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +**/.xcode.env.local + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ +*.keystore +!debug.keystore +.kotlin/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output + +# Bundle artifact +*.jsbundle + +# Ruby / CocoaPods +**/Pods/ +/vendor/bundle/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + +# testing +/coverage + +# Local measurement artifacts +/artifacts/ +*.hprof + +# Generated BuildManifest asset (prepare step); scripts remain tracked +android/app/src/main/assets/build-manifest.json + +# Ephemeral local JDK used only for CI-less verification (never commit) +/.jdk/ + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/examples/benchmark-native/.prettierrc.js b/examples/benchmark-native/.prettierrc.js new file mode 100644 index 000000000000..06860c8d1b12 --- /dev/null +++ b/examples/benchmark-native/.prettierrc.js @@ -0,0 +1,5 @@ +module.exports = { + arrowParens: 'avoid', + singleQuote: true, + trailingComma: 'all', +}; diff --git a/examples/benchmark-native/.watchmanconfig b/examples/benchmark-native/.watchmanconfig new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/examples/benchmark-native/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/examples/benchmark-native/App.tsx b/examples/benchmark-native/App.tsx new file mode 100644 index 000000000000..e4ac5f7e29ea --- /dev/null +++ b/examples/benchmark-native/App.tsx @@ -0,0 +1,199 @@ +/** + * Minimal Run/status UI + auto-run from Android intent extras. + * Prevents overlapping runs. Sustained visual update during measurement. + */ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Pressable, StyleSheet, Text, View, StatusBar } from 'react-native'; + +import BenchNative from './src/BenchNative'; +import { setAnimationTickListener } from './src/measure'; +import { executeMeasurement } from './src/runOrchestration'; +import { scenarioId } from './src/scenario'; +import type { GCScenarioConfig, LaunchConfig } from './src/types'; +import { parseLaunchConfig } from './src/validateConfig'; + +const DEFAULT_CONFIG: GCScenarioConfig = { + candidateKind: 'entity', + pattern: 'unique', + count: 1000, + control: 'gc', +}; + +export default function App() { + const [status, setStatus] = useState('Ready'); + const [lastId, setLastId] = useState(null); + const [animPhase, setAnimPhase] = useState(0); + const [config, setConfig] = useState(DEFAULT_CONFIG); + const [samples, setSamples] = useState(1); + const [label, setLabel] = useState(undefined); + const runningRef = useRef(false); + const autoStarted = useRef(false); + + const runWithConfig = useCallback( + async (cfg: GCScenarioConfig, sampleCount: number, runLabel?: string) => { + if (runningRef.current) { + setStatus('Busy — overlapping runs blocked'); + return; + } + runningRef.current = true; + setLastId(scenarioId(cfg)); + try { + await executeMeasurement({ + config: cfg, + samples: sampleCount, + label: runLabel, + onStatus: setStatus, + }); + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + setStatus(`Error: ${message}`); + } finally { + runningRef.current = false; + } + }, + [], + ); + + const runOnce = useCallback(() => { + void runWithConfig(config, samples, label); + }, [config, label, runWithConfig, samples]); + + useEffect(() => { + setAnimationTickListener(t => { + setAnimPhase(t); + }); + return () => setAnimationTickListener(null); + }, []); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const raw = await BenchNative.getLaunchConfig(); + if (cancelled) return; + let parsed: LaunchConfig; + try { + parsed = parseLaunchConfig(raw); + } catch (e) { + setStatus( + `Invalid launch config: ${e instanceof Error ? e.message : String(e)}`, + ); + return; + } + const cfg: GCScenarioConfig = { + candidateKind: parsed.candidateKind, + pattern: parsed.pattern, + count: parsed.count, + control: parsed.control, + }; + setConfig(cfg); + setSamples(parsed.samples); + setLabel(parsed.label); + if (parsed.autoRun && !autoStarted.current) { + autoStarted.current = true; + setTimeout(() => { + if (cancelled) return; + void runWithConfig(cfg, parsed.samples, parsed.label); + }, 50); + } + } catch (e) { + if (!cancelled) { + setStatus( + `Launch config unavailable: ${e instanceof Error ? e.message : String(e)}`, + ); + } + } + })(); + return () => { + cancelled = true; + }; + }, [runWithConfig]); + + const pulse = ((animPhase / 16) % 40) / 40; + + return ( + + + GC Bench (Hermes release) + + {config.candidateKind}/{config.pattern}/{config.count}/{config.control} + + {lastId ? {lastId} : null} + {status} + + + + [ + styles.button, + pressed && styles.buttonPressed, + ]} + onPress={runOnce} + accessibilityRole="button" + accessibilityLabel="Run GC benchmark" + > + Run + + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + padding: 24, + justifyContent: 'center', + backgroundColor: '#f2f4f7', + }, + title: { + fontSize: 22, + fontWeight: '600', + marginBottom: 8, + color: '#111', + }, + meta: { + fontSize: 14, + color: '#444', + marginBottom: 4, + }, + id: { + fontSize: 12, + color: '#666', + marginBottom: 12, + fontFamily: 'monospace', + }, + status: { + fontSize: 14, + color: '#222', + marginBottom: 24, + }, + anim: { + width: 48, + height: 48, + backgroundColor: '#2a6f97', + marginBottom: 32, + alignSelf: 'center', + }, + button: { + backgroundColor: '#1b4332', + paddingVertical: 14, + paddingHorizontal: 28, + alignSelf: 'flex-start', + }, + buttonPressed: { + opacity: 0.85, + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, +}); diff --git a/examples/benchmark-native/README.md b/examples/benchmark-native/README.md new file mode 100644 index 000000000000..d8745358bc50 --- /dev/null +++ b/examples/benchmark-native/README.md @@ -0,0 +1,101 @@ +# Release-Hermes Android GC benchmark + +Private monorepo workspace `example-benchmark-native`: Android-only React Native **0.86** app (React **19.2.3**, Hermes enabled). Measures `@data-client/core` cache GC interaction cost on a physical device in a **release** build. + +This phase is **manual**, not CI. Emulators are functional checks only — not authoritative. + +**APK shape:** only a **single** universal release APK (`app-release.apk`) is supported. Split / multi-APK installs are rejected by the collect script. + +## Prerequisites + +- Node `>= 22.11`, Yarn 4 (repo root) +- JDK compatible with the RN 0.86 Android Gradle Plugin +- Android SDK (`ANDROID_HOME`), platform tools (`adb`), and exactly one device (or set `ANDROID_SERIAL`) +- Built `@data-client/core` +- Physical mid-range Android device for authoritative runs + +## Install / build + +Authoritative release path (prepare BuildManifest → Gradle → sidecar): + +```bash +yarn install +yarn workspace @data-client/core run build:lib +yarn workspace example-benchmark-native typecheck +yarn workspace example-benchmark-native lint +yarn workspace example-benchmark-native test +yarn workspace example-benchmark-native build:android:release +``` + +This writes: + +- embedded asset `android/app/src/main/assets/build-manifest.json` (generated; gitignored) +- `android/app/build/outputs/apk/release/app-release.apk` +- `artifacts/build-sidecar.json` (APK sha256 + buildId; **collection authority**) + +## Provenance + +- **`buildId`** — source-build identity: deterministic `sha256` over `schemaVersion`, `gitCommit`, `gitDirty`, `sourceDigest` (not APK bytes, not timestamps). +- **`apkSha256`** — artifact identity: hash of the single `app-release.apk` bytes (sidecar + installed APK must match). +- **`sidecarId`** — artifact-aware: `sha256(buildId ∥ apkSha256)`. +- **Sidecar is authority**, not the live checkout. Collection verifies sidecar identity, source digest, local + installed APK sha256, and report embedded `buildId`. +- Intent `label` is optional metadata only. Commit is fingerprinting input to `buildId`, not a separate launch authority. +- Tampered metadata or a forged `buildId` fails `verifyManifestBuildId` / collect. + +## Scenario IDs + +`android/{entity|endpoint|mixed}/{unique|duplicate}/{1000|10000|100000}/interaction/{gc|no-gc}` + +- `duplicate` is entity-only; invalid axes fail validation (host + in-app) + +## Host collection + +```bash +# exactly one device, or: +# ANDROID_SERIAL=… CANDIDATE_KIND=entity … +CANDIDATE_KIND=entity PATTERN=unique COUNT=1000 CONTROL=gc SAMPLES=5 \ + yarn workspace example-benchmark-native collect +``` + +Authoritative **100k**: + +```bash +CANDIDATE_KIND=entity PATTERN=unique COUNT=100000 CONTROL=gc SAMPLES=5 \ + yarn workspace example-benchmark-native collect +CANDIDATE_KIND=entity PATTERN=unique COUNT=100000 CONTROL=no-gc SAMPLES=5 \ + yarn workspace example-benchmark-native collect +``` + +### Matrix + +Full canonical matrix via loops; 100k skipped unless `FULL=1` or filter selects them. + +```bash +SAMPLES=3 yarn workspace example-benchmark-native matrix +SAMPLES=5 yarn workspace example-benchmark-native matrix entity/unique/100000 +FULL=1 SAMPLES=5 yarn workspace example-benchmark-native matrix +``` + +## UI frame semantics + +| Source | Quantity | Missed-frame math | +| --- | --- | --- | +| FrameMetrics | `TOTAL_DURATION` (duration) | `max(0, ceil(duration/period) − 1)` | +| Choreographer | frame-time delta (interval) | `max(0, round(interval/period) − 1)` | +| JS rAF | timestamp intervals | same as Choreographer | + +Every sample records `uiCaptureSource`, frame count, max/total duration aggregates, missed frames, and refresh period/rate. Zero frames or invalid refresh period fail the run. Capture stop is idempotent; JS tears down in `finally`. + +## Memory caveat + +Without forced Hermes GC, JS heap and process PSS/RSS are observational/noisy — not a memory gate alone. Compare repeated `gc` vs `no-gc`. No `System.gc()`. + +## Verification without a device + +```bash +bash -n scripts/*.sh +yarn workspace example-benchmark-native test +yarn workspace example-benchmark-native typecheck +``` + +On-device install/start/pull remains unverified until a device is attached. diff --git a/examples/benchmark-native/__tests__/build-identity.test.ts b/examples/benchmark-native/__tests__/build-identity.test.ts new file mode 100644 index 000000000000..221196859428 --- /dev/null +++ b/examples/benchmark-native/__tests__/build-identity.test.ts @@ -0,0 +1,127 @@ +/** + * Deterministic buildId / sidecarId identity (source vs artifact). + */ +const { + computeBuildId, + computeSidecarId, + verifyManifestBuildId, + verifySidecarIdentity, +} = require('../scripts/build-identity.cjs'); + +const DIGEST_A = 'a'.repeat(64); +const DIGEST_B = 'b'.repeat(64); +const APK_A = 'c'.repeat(64); +const APK_B = 'd'.repeat(64); + +describe('computeBuildId', () => { + it('is deterministic over schemaVersion, gitCommit, gitDirty, sourceDigest', () => { + const a = computeBuildId({ + schemaVersion: 1, + gitCommit: 'abc', + gitDirty: false, + sourceDigest: DIGEST_A, + }); + const b = computeBuildId({ + schemaVersion: 1, + gitCommit: 'abc', + gitDirty: false, + sourceDigest: DIGEST_A, + }); + expect(a).toBe(b); + expect(a).toMatch(/^[0-9a-f]{64}$/); + }); + + it('changes when any canonical input changes', () => { + const base = { + schemaVersion: 1 as const, + gitCommit: 'abc', + gitDirty: false, + sourceDigest: DIGEST_A, + }; + const id = computeBuildId(base); + expect(computeBuildId({ ...base, gitCommit: 'xyz' })).not.toBe(id); + expect(computeBuildId({ ...base, gitDirty: true })).not.toBe(id); + expect(computeBuildId({ ...base, sourceDigest: DIGEST_B })).not.toBe(id); + }); +}); + +describe('verifyManifestBuildId', () => { + it('accepts a consistent manifest', () => { + const inputs = { + schemaVersion: 1, + gitCommit: 'deadbeef', + gitDirty: true, + sourceDigest: DIGEST_A, + }; + const buildId = computeBuildId(inputs); + expect(verifyManifestBuildId({ ...inputs, buildId })).toBe(buildId); + }); + + it('rejects forged buildId', () => { + expect(() => + verifyManifestBuildId({ + schemaVersion: 1, + buildId: '0'.repeat(64), + gitCommit: 'deadbeef', + gitDirty: false, + sourceDigest: DIGEST_A, + }), + ).toThrow(/buildId mismatch/); + }); + + it('rejects tampered metadata that no longer matches buildId', () => { + const inputs = { + schemaVersion: 1, + gitCommit: 'deadbeef', + gitDirty: false, + sourceDigest: DIGEST_A, + }; + const buildId = computeBuildId(inputs); + expect(() => + verifyManifestBuildId({ + ...inputs, + buildId, + sourceDigest: DIGEST_B, + }), + ).toThrow(/buildId mismatch/); + }); +}); + +describe('computeSidecarId / verifySidecarIdentity', () => { + it('binds source buildId to artifact apkSha256', () => { + const buildId = computeBuildId({ + schemaVersion: 1, + gitCommit: 'abc', + gitDirty: false, + sourceDigest: DIGEST_A, + }); + const id1 = computeSidecarId({ buildId, apkSha256: APK_A }); + const id2 = computeSidecarId({ buildId, apkSha256: APK_B }); + expect(id1).not.toBe(id2); + // Same APK + same source → same sidecarId + expect(computeSidecarId({ buildId, apkSha256: APK_A })).toBe(id1); + }); + + it('rejects tampered sidecarId while preserving buildId checks', () => { + const inputs = { + schemaVersion: 1, + gitCommit: 'abc', + gitDirty: false, + sourceDigest: DIGEST_A, + }; + const buildId = computeBuildId(inputs); + const apkSha256 = APK_A; + const sidecarId = computeSidecarId({ buildId, apkSha256 }); + expect( + verifySidecarIdentity({ ...inputs, buildId, apkSha256, sidecarId }), + ).toBe(sidecarId); + expect(() => + verifySidecarIdentity({ + ...inputs, + buildId, + apkSha256, + sidecarId: '0'.repeat(64), + }), + ).toThrow(/sidecarId mismatch/); + }); +}); diff --git a/examples/benchmark-native/__tests__/frame-semantics.test.ts b/examples/benchmark-native/__tests__/frame-semantics.test.ts new file mode 100644 index 000000000000..1919c854ae04 --- /dev/null +++ b/examples/benchmark-native/__tests__/frame-semantics.test.ts @@ -0,0 +1,55 @@ +import { + missedFramesFromDurationMs, + missedFramesFromIntervalMs, + validateUiFrameCapture, +} from '../src/frames'; + +const P = 16.67; + +describe('missedFramesFromDurationMs (FrameMetrics)', () => { + it('boundary table at .99x 1x 1.01x 2x 2.01x', () => { + expect(missedFramesFromDurationMs(0.99 * P, P)).toBe(0); + expect(missedFramesFromDurationMs(1.0 * P, P)).toBe(0); + // just over one period → ceil → 2 periods → 1 miss + expect(missedFramesFromDurationMs(1.01 * P, P)).toBe(1); + expect(missedFramesFromDurationMs(2.0 * P, P)).toBe(1); + expect(missedFramesFromDurationMs(2.01 * P, P)).toBe(2); + }); +}); + +describe('missedFramesFromIntervalMs (Choreographer / JS rAF)', () => { + it('boundary table at .99x 1x 1.01x 2x 2.01x', () => { + expect(missedFramesFromIntervalMs(0.99 * P, P)).toBe(0); + expect(missedFramesFromIntervalMs(1.0 * P, P)).toBe(0); + // 1.01× still rounds to 1 period → 0 miss + expect(missedFramesFromIntervalMs(1.01 * P, P)).toBe(0); + expect(missedFramesFromIntervalMs(2.0 * P, P)).toBe(1); + // 2.01× rounds to 2 → 1 miss (nearest-period) + expect(missedFramesFromIntervalMs(2.01 * P, P)).toBe(1); + }); +}); + +describe('validateUiFrameCapture', () => { + const ok = { + source: 'FrameMetrics' as const, + frameCount: 3, + maxFrameDurationMs: 20, + totalFrameDurationMs: 50, + missedFrames: 0, + refreshPeriodMs: P, + refreshRateHz: 60, + }; + + it('accepts a valid aggregate', () => { + expect(() => validateUiFrameCapture(ok)).not.toThrow(); + }); + + it('rejects invalid refresh period and zero frames', () => { + expect(() => validateUiFrameCapture({ ...ok, refreshPeriodMs: 0 })).toThrow( + /refreshPeriodMs/, + ); + expect(() => validateUiFrameCapture({ ...ok, frameCount: 0 })).toThrow( + /insufficient ui frames/, + ); + }); +}); diff --git a/examples/benchmark-native/__tests__/gcHarness.test.ts b/examples/benchmark-native/__tests__/gcHarness.test.ts new file mode 100644 index 000000000000..79043478b4b4 --- /dev/null +++ b/examples/benchmark-native/__tests__/gcHarness.test.ts @@ -0,0 +1,91 @@ +/** + * Real harness tests against createHarness / queue / reducer (not reimplemented math). + */ +import { + countRemaining, + createHarness, + validateMeasurement, + GC, +} from '../src/gcHarness'; +import type { GCScenarioConfig } from '../src/types'; + +const SMALL = 32; + +function runSweepSample(config: GCScenarioConfig) { + const harness = createHarness(config); + const { policy, expected } = harness; + + expect(policy.queueEntries).toBe(expected.queueEntries); + expect(expected.uniqueTargets).toBe( + config.pattern === 'duplicate' ? 1 : config.count, + ); + + if (config.control === 'no-gc') { + validateMeasurement(config, harness, { + actionCount: 0, + deletionCount: 0, + queueEntries: expected.queueEntries, + uniqueTargets: expected.uniqueTargets, + actionTargetCount: 0, + }); + harness.dispose(); + return; + } + + policy.sweep(); + const action = harness.getCapturedAction(); + expect(action).not.toBeNull(); + expect(action!.type).toBe(GC); + expect(action!.entities.length).toBe(expected.expectedEntitiesInAction); + expect(action!.endpoints.length).toBe(expected.expectedEndpointsInAction); + + if (config.pattern === 'duplicate') { + const unique = new Set(action!.entities.map(p => `${p.key}:${p.pk}`)); + expect(unique.size).toBe(1); + } + + const remaining = countRemaining(harness.getState(), config, expected); + expect(remaining.entityRemaining).toBe(0); + expect(remaining.endpointRemaining).toBe(0); + expect(policy.queueEntries).toBe(0); + + const deletionCount = remaining.entityDeleted + remaining.endpointDeleted; + validateMeasurement(config, harness, { + actionCount: 1, + deletionCount, + queueEntries: expected.queueEntries, + uniqueTargets: expected.uniqueTargets, + actionTargetCount: + expected.expectedEntitiesInAction + expected.expectedEndpointsInAction, + }); + + harness.dispose(); +} + +describe('createHarness end-to-end', () => { + const uniqueKinds = ['entity', 'endpoint', 'mixed'] as const; + + for (const candidateKind of uniqueKinds) { + for (const control of ['gc', 'no-gc'] as const) { + it(`${candidateKind}/unique/${SMALL}/${control}`, () => { + runSweepSample({ + candidateKind, + pattern: 'unique', + count: SMALL, + control, + }); + }); + } + } + + for (const control of ['gc', 'no-gc'] as const) { + it(`entity/duplicate/${SMALL}/${control}`, () => { + runSweepSample({ + candidateKind: 'entity', + pattern: 'duplicate', + count: SMALL, + control, + }); + }); + } +}); diff --git a/examples/benchmark-native/__tests__/rafCollectSequence.test.ts b/examples/benchmark-native/__tests__/rafCollectSequence.test.ts new file mode 100644 index 000000000000..44ffee9e2547 --- /dev/null +++ b/examples/benchmark-native/__tests__/rafCollectSequence.test.ts @@ -0,0 +1,77 @@ +import { + createManualFrameScheduler, + runRafCollectSequence, +} from '../src/rafCollectSequence'; + +describe('runRafCollectSequence failure handling', () => { + it('rejects promptly when collectWork throws and no-ops queued frames', async () => { + const scheduler = createManualFrameScheduler(16.67); + let terminateCalls = 0; + let collectCalls = 0; + const nowMs = 0; + + const pending = runRafCollectSequence({ + scheduleFrame: scheduler.scheduleFrame, + now: () => nowMs, + scheduleTimeout0: cb => { + // Run macrotask probe synchronously for determinism. + cb(); + }, + postFrames: 2, + collectWork: () => { + collectCalls++; + throw new Error('sweep failed'); + }, + onTerminateError: () => { + terminateCalls++; + }, + }); + + // pre → collect (queues post, then throws) + scheduler.flush(1); // pre + expect(scheduler.pending()).toBe(1); + scheduler.flush(1); // collect — throws after queueing post + + // Promise rejects promptly so outer `finally` can stop animation/native capture. + await expect(pending).rejects.toThrow('sweep failed'); + expect(collectCalls).toBe(1); + expect(terminateCalls).toBe(1); + + // Post frame was queued before throw; flushing it must no-op (no second reject / terminate). + const pendingAfter = scheduler.pending(); + expect(pendingAfter).toBeGreaterThanOrEqual(1); + expect(() => scheduler.flush(5)).not.toThrow(); + expect(terminateCalls).toBe(1); + expect(collectCalls).toBe(1); + }); + + it('resolves with timestamps when collectWork succeeds', async () => { + const scheduler = createManualFrameScheduler(10); + let nowMs = 1000; + + const pending = runRafCollectSequence({ + scheduleFrame: scheduler.scheduleFrame, + now: () => { + nowMs += 1; + return nowMs; + }, + scheduleTimeout0: cb => cb(), + postFrames: 2, + collectWork: () => ({ + totalMs: 12.5, + actionCount: 1, + }), + }); + + // pre, collect, post, post-final → resolve via timeout0 + scheduler.flush(1); + scheduler.flush(1); + scheduler.flush(1); + scheduler.flush(1); + + const result = await pending; + expect(result.totalMs).toBe(12.5); + expect(result.actionCount).toBe(1); + expect(result.frameTimestamps.length).toBeGreaterThanOrEqual(3); + }); +}); diff --git a/examples/benchmark-native/__tests__/readJsHeap.test.ts b/examples/benchmark-native/__tests__/readJsHeap.test.ts new file mode 100644 index 000000000000..faa630f2679f --- /dev/null +++ b/examples/benchmark-native/__tests__/readJsHeap.test.ts @@ -0,0 +1,43 @@ +import { readJsHeapBytes } from '../src/BenchNative'; + +describe('readJsHeapBytes', () => { + const g = globalThis as typeof globalThis & { + performance?: BenchPerformance; + }; + const original = g.performance; + + afterEach(() => { + Object.defineProperty(g, 'performance', { + value: original, + configurable: true, + writable: true, + }); + }); + + it('returns undefined when performance is absent', () => { + Object.defineProperty(g, 'performance', { + value: undefined, + configurable: true, + writable: true, + }); + expect(readJsHeapBytes()).toBeUndefined(); + }); + + it('returns undefined when memory is absent', () => { + Object.defineProperty(g, 'performance', { + value: { now: () => 0 }, + configurable: true, + writable: true, + }); + expect(readJsHeapBytes()).toBeUndefined(); + }); + + it('returns usedJSHeapSize when exposed', () => { + Object.defineProperty(g, 'performance', { + value: { now: () => 0, memory: { usedJSHeapSize: 12345 } }, + configurable: true, + writable: true, + }); + expect(readJsHeapBytes()).toBe(12345); + }); +}); diff --git a/examples/benchmark-native/__tests__/scenario-and-frames.test.ts b/examples/benchmark-native/__tests__/scenario-and-frames.test.ts new file mode 100644 index 000000000000..0c4ef0f4657a --- /dev/null +++ b/examples/benchmark-native/__tests__/scenario-and-frames.test.ts @@ -0,0 +1,100 @@ +import { + computeMaxInputDelayMs, + excessMissedFramesFromIntervals, + frameIntervalsFromTimestamps, + median, + missedFramesFromTimestamps, +} from '../src/frames'; +import { + splitMixedCount, + scenarioId, + parseScenarioId, + listScenarios, +} from '../src/scenario'; + +describe('scenarioId', () => { + it('builds stable android interaction ids', () => { + expect( + scenarioId({ + candidateKind: 'entity', + pattern: 'unique', + count: 1000, + control: 'gc', + }), + ).toBe('android/entity/unique/1000/interaction/gc'); + }); + + it('round-trips parseScenarioId', () => { + const id = 'android/mixed/unique/10000/interaction/no-gc'; + expect(scenarioId(parseScenarioId(id))).toBe(id); + }); + + it('lists duplicate only for entity', () => { + const ids = listScenarios().map(scenarioId); + expect(ids.some(id => id.includes('/duplicate/'))).toBe(true); + expect(ids.some(id => id.startsWith('android/endpoint/duplicate'))).toBe( + false, + ); + }); +}); + +describe('splitMixedCount', () => { + it('splits evenly with remainder to entities', () => { + expect(splitMixedCount(1000)).toEqual({ entities: 500, endpoints: 500 }); + expect(splitMixedCount(1001)).toEqual({ entities: 501, endpoints: 500 }); + }); +}); + +describe('excessMissedFramesFromIntervals', () => { + it('counts nearest-period excess stalls', () => { + expect(excessMissedFramesFromIntervals([16.6], 16.6)).toBe(0); + expect(excessMissedFramesFromIntervals([33.2], 16.6)).toBe(1); + // slightly under 2× still rounds to 2 periods → 1 missed + expect(excessMissedFramesFromIntervals([1.98 * 16.6], 16.6)).toBe(1); + }); + + it('returns 0 for non-positive period', () => { + expect(excessMissedFramesFromIntervals([50], 0)).toBe(0); + }); +}); + +describe('missedFramesFromTimestamps', () => { + it('detects a multi-period stall in a pre→collect→post rAF sequence', () => { + const period = 16.67; + // pre @0, collect @period, then post only after ~3 periods (2 missed) + // This is what queueing the next rAF *before* blocking sweep enables. + const timestamps = [ + 0, + period, + period + 3 * period, // collect→post gap spans 3 periods + period + 3 * period + period, + ]; + expect(frameIntervalsFromTimestamps(timestamps)).toEqual([ + period, + 3 * period, + period, + ]); + expect(missedFramesFromTimestamps(timestamps, period)).toBe(2); + }); + + it('reports 0 missed when every interval is one display period', () => { + const period = 16.67; + const timestamps = [0, period, 2 * period, 3 * period]; + expect(missedFramesFromTimestamps(timestamps, period)).toBe(0); + }); +}); + +describe('computeMaxInputDelayMs', () => { + it('is max of timer delay and frame excess (not pointer latency)', () => { + expect(computeMaxInputDelayMs(5, [16.6, 40], 16.6)).toBe( + Math.max(5, 40 - 16.6), + ); + }); +}); + +describe('median', () => { + it('handles even and odd lengths', () => { + expect(median([1, 3, 2])).toBe(2); + expect(median([1, 2, 3, 4])).toBe(2.5); + }); +}); diff --git a/examples/benchmark-native/__tests__/summarize.test.ts b/examples/benchmark-native/__tests__/summarize.test.ts new file mode 100644 index 000000000000..feab5db2f7e7 --- /dev/null +++ b/examples/benchmark-native/__tests__/summarize.test.ts @@ -0,0 +1,86 @@ +import { summarizeGCSamples } from '../src/report'; +import type { GCAndroidMeasurement } from '../src/types'; + +function baseSample( + overrides: Partial = {}, +): GCAndroidMeasurement { + return { + schemaVersion: 1, + totalMs: 10, + sliceDurationsMs: [10], + actionCount: 1, + queueEntries: 1000, + uniqueTargets: 1000, + actionTargetCount: 1000, + deletionCount: 1000, + timerDelayMs: 1, + frameIntervalsMs: [16], + displayPeriodMs: 16, + missedFrames: 0, + maxInputDelayMs: 1, + uiCaptureSource: 'FrameMetrics', + uiFrameCount: 10, + uiMaxFrameDurationMs: 16, + uiTotalFrameDurationMs: 160, + uiMissedFrames: 0, + uiRefreshPeriodMs: 16.67, + uiRefreshRateHz: 60, + ...overrides, + }; +} + +describe('summarizeGCSamples', () => { + it('summarizes UI frame and memory delta fields when present', () => { + const summary = summarizeGCSamples([ + baseSample({ + uiFrameCount: 10, + uiMaxFrameDurationMs: 20, + uiTotalFrameDurationMs: 160, + uiMissedFrames: 1, + processPssBeforeKb: 100, + processPssAfterKb: 90, + processPssDeltaKb: -10, + processRssBeforeKb: 200, + processRssAfterKb: 180, + processRssDeltaKb: -20, + jsHeapBeforeBytes: 1000, + jsHeapAfterBytes: 800, + jsHeapDeltaBytes: -200, + }), + baseSample({ + uiFrameCount: 12, + uiMaxFrameDurationMs: 22, + uiTotalFrameDurationMs: 180, + uiMissedFrames: 0, + processPssBeforeKb: 110, + processPssAfterKb: 95, + processPssDeltaKb: -15, + processRssBeforeKb: 210, + processRssAfterKb: 185, + processRssDeltaKb: -25, + jsHeapBeforeBytes: 1100, + jsHeapAfterBytes: 850, + jsHeapDeltaBytes: -250, + }), + ]); + + expect(summary.uiFrameCount?.median).toBe(11); + expect(summary.uiTotalFrameDurationMs?.min).toBe(160); + expect(summary.uiMaxFrameDurationMs?.max).toBe(22); + expect(summary.uiMissedFrames?.median).toBe(0.5); + expect(summary.processPssBeforeKb?.median).toBe(105); + expect(summary.processPssAfterKb?.median).toBe(92.5); + expect(summary.processPssDeltaKb?.median).toBe(-12.5); + expect(summary.processRssDeltaKb?.median).toBe(-22.5); + expect(summary.jsHeapDeltaBytes?.median).toBe(-225); + }); + + it('omits optional memory summaries when unavailable', () => { + const summary = summarizeGCSamples([baseSample()]); + expect(summary.processPssDeltaKb).toBeUndefined(); + expect(summary.processRssBeforeKb).toBeUndefined(); + expect(summary.jsHeapBeforeBytes).toBeUndefined(); + // UI aggregates are required on every sample + expect(summary.uiFrameCount?.median).toBe(10); + }); +}); diff --git a/examples/benchmark-native/__tests__/validate-config.test.ts b/examples/benchmark-native/__tests__/validate-config.test.ts new file mode 100644 index 000000000000..1428bbfe2b4e --- /dev/null +++ b/examples/benchmark-native/__tests__/validate-config.test.ts @@ -0,0 +1,139 @@ +import { + ConfigValidationError, + MAX_SAMPLES, + parseHostEnvConfig, + parseLaunchConfig, + validateSampleCount, + validateScenarioConfig, +} from '../src/validateConfig'; + +describe('validateScenarioConfig', () => { + it('accepts canonical axes', () => { + expect(() => + validateScenarioConfig({ + candidateKind: 'mixed', + pattern: 'unique', + count: 100000, + control: 'no-gc', + }), + ).not.toThrow(); + }); + + it('rejects non-canonical counts', () => { + expect(() => + validateScenarioConfig({ + candidateKind: 'entity', + pattern: 'unique', + count: 500, + control: 'gc', + }), + ).toThrow(ConfigValidationError); + }); + + it('rejects duplicate with non-entity', () => { + expect(() => + validateScenarioConfig({ + candidateKind: 'endpoint', + pattern: 'duplicate', + count: 1000, + control: 'gc', + }), + ).toThrow(/duplicate pattern only supports/); + }); + + it('rejects invalid enums', () => { + expect(() => + validateScenarioConfig({ + candidateKind: 'widget' as any, + pattern: 'unique', + count: 1000, + control: 'gc', + }), + ).toThrow(/candidateKind/); + expect(() => + validateScenarioConfig({ + candidateKind: 'entity', + pattern: 'unique', + count: 1000, + control: 'maybe' as any, + }), + ).toThrow(/control/); + }); +}); + +describe('validateSampleCount', () => { + it('accepts 1..MAX_SAMPLES', () => { + expect(() => validateSampleCount(1)).not.toThrow(); + expect(() => validateSampleCount(MAX_SAMPLES)).not.toThrow(); + }); + + it('rejects 0, negative, and oversized', () => { + expect(() => validateSampleCount(0)).toThrow(ConfigValidationError); + expect(() => validateSampleCount(-1)).toThrow(ConfigValidationError); + expect(() => validateSampleCount(MAX_SAMPLES + 1)).toThrow( + ConfigValidationError, + ); + }); +}); + +describe('parseLaunchConfig', () => { + it('applies defaults then validates', () => { + const parsed = parseLaunchConfig({ autoRun: true }); + expect(parsed).toMatchObject({ + autoRun: true, + candidateKind: 'entity', + pattern: 'unique', + count: 1000, + control: 'gc', + samples: 1, + }); + expect(parsed.label).toBeUndefined(); + }); + + it('accepts optional label without commit authority', () => { + const parsed = parseLaunchConfig({ + autoRun: false, + label: 'baseline', + count: 1000, + }); + expect(parsed.label).toBe('baseline'); + expect((parsed as { commit?: string }).commit).toBeUndefined(); + }); + + it('fails clearly on invalid intent-like input', () => { + expect(() => parseLaunchConfig({ autoRun: true, count: 999 })).toThrow( + /invalid count/, + ); + expect(() => + parseLaunchConfig({ + autoRun: true, + candidateKind: 'endpoint', + pattern: 'duplicate', + count: 1000, + }), + ).toThrow(/duplicate/); + expect(() => parseLaunchConfig({ autoRun: true, samples: 0 })).toThrow( + /samples/, + ); + }); +}); + +describe('parseHostEnvConfig', () => { + it('parses shell-like strings', () => { + expect( + parseHostEnvConfig({ + candidateKind: 'entity', + pattern: 'duplicate', + count: '100000', + control: 'gc', + samples: '5', + }), + ).toEqual({ + candidateKind: 'entity', + pattern: 'duplicate', + count: 100000, + control: 'gc', + samples: 5, + }); + }); +}); diff --git a/examples/benchmark-native/android/app/build.gradle b/examples/benchmark-native/android/app/build.gradle new file mode 100644 index 000000000000..4f4a4ae2a171 --- /dev/null +++ b/examples/benchmark-native/android/app/build.gradle @@ -0,0 +1,118 @@ +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.facebook.react" + +/** + * This is the configuration block to customize your React Native Android app. + * By default you don't need to apply any configuration, just uncomment the lines you need. + */ +react { + /* Folders */ + // The root of your project, i.e. where "package.json" lives. Default is '../..' + root = file("../../") + // Monorepo: react-native is hoisted to the workspace root node_modules. + reactNativeDir = file("../../../../node_modules/react-native") + codegenDir = file("../../../../node_modules/@react-native/codegen") + cliFile = file("../../../../node_modules/react-native/cli.js") + + /* Variants */ + // The list of variants to that are debuggable. For those we're going to + // skip the bundling of the JS bundle and the assets. Default is "debug", "debugOptimized". + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "liteDebugOptimized", "prodDebug", "prodDebugOptimized"] + + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + // nodeExecutableAndArgs = ["node"] + // + // The command to run when bundling. Default is 'bundle' + // bundleCommand = "ram-bundle" + // + // The path to the CLI configuration file. Default is empty. + // bundleConfig = file(../rn-cli.config.js) + // + // The name of the generated asset file containing your JS bundle + // bundleAssetName = "MyApplication.android.bundle" + // + // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' + // entryFile = file("../js/MyApplication.android.js") + // + // A list of extra flags to pass to the 'bundle' commands. + // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle + // extraPackagerArgs = [] + + /* Hermes Commands */ + // Monorepo: hermesc lives in hermes-compiler (absolute + %OS-BIN% token). + // projectDir is android/app; rootDir is android/ — use projectDir for path math. + hermesCommand = "${new File(projectDir, '../../../../node_modules/hermes-compiler/hermesc').canonicalPath}/%OS-BIN%/hermesc" + // + // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" + // hermesFlags = ["-O", "-output-source-map"] + + /* Autolinking */ + autolinkLibrariesWithApp() +} + +/** + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = false + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' + +android { + ndkVersion rootProject.ext.ndkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion + + namespace "com.dataclient.benchmarknative" + defaultConfig { + applicationId "com.dataclient.benchmarknative" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } +} diff --git a/examples/benchmark-native/android/app/debug.keystore b/examples/benchmark-native/android/app/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..364e105ed39fbfd62001429a68140672b06ec0de GIT binary patch literal 2257 zcmchYXEfYt8;7T1^dLH$VOTZ%2NOdOH5j5LYLtZ0q7x-V8_6gU5)#7dkq{HTmsfNq zB3ZqcAxeY^G10@?efK?Q&)M(qInVv!xjx+IKEL}p*K@LYvIzo#AZG>st5|P)KF1_Z;y){W{<7K{nl!CPuE z_^(!C(Ol0n8 zK13*rzAtW>(wULKPRYLd7G18F8#1P`V*9`(Poj26eOXYyBVZPno~Cvvhx7vPjAuZo zF?VD!zB~QG(!zbw#qsxT8%BSpqMZ4f70ZPn-3y$L8{EVbbN9$H`B&Z1quk9tgp5FM zuxp3pJ0b8u|3+#5bkJ4SRnCF2l7#DyLYXYY8*?OuAwK4E6J{0N=O3QNVzQ$L#FKkR zi-c@&!nDvezOV$i$Lr}iF$XEcwnybQ6WZrMKuw8gCL^U#D;q3t&HpTbqyD%vG=TeDlzCT~MXUPC|Leb-Uk+ z=vnMd(|>ld?Fh>V8poP;q;;nc@en$|rnP0ytzD&fFkCeUE^kG9Kx4wUh!!rpjwKDP zyw_e|a^x_w3E zP}}@$g>*LLJ4i0`Gx)qltL}@;mDv}D*xR^oeWcWdPkW@Uu)B^X&4W1$p6}ze!zudJ zyiLg@uggoMIArBr*27EZV7djDg@W1MaL+rcZ-lrANJQ%%>u8)ZMWU@R2qtnmG(acP z0d_^!t>}5W zpT`*2NR+0+SpTHb+6Js4b;%LJB;B_-ChhnU5py}iJtku*hm5F0!iql8Hrpcy1aYbT z1*dKC5ua6pMX@@iONI?Hpr%h;&YaXp9n!ND7-=a%BD7v&g zOO41M6EbE24mJ#S$Ui0-brR5ML%@|ndz^)YLMMV1atna{Fw<;TF@>d&F|!Z>8eg>>hkFrV)W+uv=`^F9^e zzzM2*oOjT9%gLoub%(R57p-`TXFe#oh1_{&N-YN z<}artH|m=d8TQuKSWE)Z%puU|g|^^NFwC#N=@dPhasyYjoy(fdEVfKR@cXKHZV-`06HsP`|Ftx;8(YD$fFXumLWbGnu$GMqRncXYY9mwz9$ap zQtfZB^_BeNYITh^hA7+(XNFox5WMeG_LtJ%*Q}$8VKDI_p8^pqX)}NMb`0e|wgF7D zuQACY_Ua<1ri{;Jwt@_1sW9zzdgnyh_O#8y+C;LcZq6=4e^cs6KvmK@$vVpKFGbQ= z$)Eux5C|Fx;Gtmv9^#Y-g@7Rt7*eLp5n!gJmn7&B_L$G?NCN`AP>cXQEz}%F%K;vUs{+l4Q{}eWW;ATe2 zqvXzxoIDy(u;F2q1JH7Sf;{jy_j})F+cKlIOmNfjBGHoG^CN zM|Ho&&X|L-36f}Q-obEACz`sI%2f&k>z5c$2TyTSj~vmO)BW~+N^kt`Jt@R|s!){H ze1_eCrlNaPkJQhL$WG&iRvF*YG=gXd1IyYQ9ew|iYn7r~g!wOnw;@n42>enAxBv*A zEmV*N#sxdicyNM=A4|yaOC5MByts}s_Hpfj|y<6G=o=!3S@eIFKDdpR7|FY>L&Wat&oW&cm&X~ z5Bt>Fcq(fgnvlvLSYg&o6>&fY`ODg4`V^lWWD=%oJ#Kbad2u~! zLECFS*??>|vDsNR&pH=Ze0Eo`sC_G`OjoEKVHY|wmwlX&(XBE<@sx3Hd^gtd-fNwUHsylg06p`U2y_={u}Bc + + + + + + + + + + + + diff --git a/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/BenchNativeModule.kt b/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/BenchNativeModule.kt new file mode 100644 index 000000000000..b812926ecf72 --- /dev/null +++ b/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/BenchNativeModule.kt @@ -0,0 +1,385 @@ +package com.dataclient.benchmarknative + +import android.app.ActivityManager +import android.content.Context +import android.os.Build +import android.os.Bundle +import android.os.Debug +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.Choreographer +import android.view.FrameMetrics +import android.view.Window +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.WritableMap +import java.io.File +import java.lang.ref.WeakReference +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.roundToInt + +/** + * Aggregate-only native probes: UI frame timing (no per-frame bridge calls), + * process memory snapshots, launch extras, report I/O, and embedded build manifest. + * + * FrameMetrics TOTAL_DURATION is a duration → ceil(duration/period)−1. + * Choreographer deltas are intervals → round(interval/period)−1. + */ +class BenchNativeModule( + private val reactContext: ReactApplicationContext, +) : ReactContextBaseJavaModule(reactContext) { + + override fun getName(): String = NAME + + private val mainHandler = Handler(Looper.getMainLooper()) + private var capturing = false + private var captureSource: String = "none" + private var frameMetricsListener: Window.OnFrameMetricsAvailableListener? = null + private var captureWindowRef: WeakReference? = null + private var choreographerCallback: Choreographer.FrameCallback? = null + private var lastChoreographerNs: Long = 0L + + private var frameCount = 0 + private var maxFrameDurationNs = 0L + private var totalFrameDurationNs = 0L + private var missedFrames = 0 + private var refreshPeriodNs = 16_666_666L + private var refreshRateHz = 60.0 + + override fun invalidate() { + // Tear down listeners on the main thread so activity recreation cannot leak. + mainHandler.post { stopCaptureInternal(idempotent = true) } + super.invalidate() + } + + @ReactMethod + fun getLaunchConfig(promise: Promise) { + try { + val extras = MainActivity.launchExtras + val map = Arguments.createMap() + map.putBoolean("autoRun", extras?.getBoolean("autoRun", false) ?: false) + putExtraString(map, extras, "candidateKind") + putExtraString(map, extras, "pattern") + putExtraString(map, extras, "control") + putExtraString(map, extras, "label") + if (extras != null && extras.containsKey("count")) { + map.putInt("count", extras.getInt("count", 1000)) + } + if (extras != null && extras.containsKey("samples")) { + map.putInt("samples", extras.getInt("samples", 1)) + } + promise.resolve(map) + } catch (e: Exception) { + promise.reject("LAUNCH_CONFIG", e) + } + } + + @ReactMethod + fun getBuildManifest(promise: Promise) { + try { + val json = + reactContext.assets.open(BUILD_MANIFEST_ASSET).bufferedReader().use { it.readText() } + val map = Arguments.createMap() + map.putString("json", json) + promise.resolve(map) + } catch (e: Exception) { + promise.reject( + "BUILD_MANIFEST", + "build-manifest.json missing — run yarn build:android:release (prepare→gradle→finalize)", + e, + ) + } + } + + @ReactMethod + fun getEnvironment(promise: Promise) { + try { + val activity = reactContext.currentActivity + val display = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity?.display + } else { + @Suppress("DEPRECATION") + activity?.windowManager?.defaultDisplay + } + val hz = display?.refreshRate?.toDouble() ?: 60.0 + val periodMs = if (hz > 0) 1000.0 / hz else 1000.0 / 60.0 + + val map = Arguments.createMap() + map.putInt("apiLevel", Build.VERSION.SDK_INT) + map.putString("release", Build.VERSION.RELEASE ?: "") + map.putString("manufacturer", Build.MANUFACTURER ?: "") + map.putString("model", Build.MODEL ?: "") + map.putString("device", Build.DEVICE ?: "") + map.putString("brand", Build.BRAND ?: "") + map.putString( + "buildType", + if (BuildConfig.DEBUG) "debug" else "release", + ) + map.putString("applicationId", BuildConfig.APPLICATION_ID) + map.putBoolean("hermesEnabled", isHermesEnabled()) + map.putDouble("refreshRateHz", hz) + map.putDouble("refreshPeriodMs", periodMs) + + val hermesProps = Arguments.createMap() + hermesProps.putString("BuildConfig.IS_HERMES_ENABLED", isHermesEnabled().toString()) + try { + val clazz = Class.forName("com.facebook.react.common.build.ReactBuildConfig") + val field = clazz.getField("IS_HERMES_ENABLED") + hermesProps.putString("ReactBuildConfig.IS_HERMES_ENABLED", field.getBoolean(null).toString()) + } catch (_: Throwable) { + // optional + } + map.putMap("hermesRuntimeProperties", hermesProps) + + promise.resolve(map) + } catch (e: Exception) { + promise.reject("ENVIRONMENT", e) + } + } + + @ReactMethod + fun getMemorySnapshot(promise: Promise) { + try { + val info = Debug.MemoryInfo() + Debug.getMemoryInfo(info) + val map = Arguments.createMap() + map.putInt("totalPssKb", info.totalPss) + map.putInt("totalPrivateDirtyKb", info.totalPrivateDirty) + if (Build.VERSION.SDK_INT >= 34) { + try { + val rssBytes = + android.os.Process::class.java + .getMethod("getRssInBytes") + .invoke(null) as Long + map.putDouble("rssKb", rssBytes / 1024.0) + } catch (_: Throwable) { + // omit rssKb + } + } + val am = reactContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + map.putInt("memoryClassMb", am.memoryClass) + promise.resolve(map) + } catch (e: Exception) { + promise.reject("MEMORY", e) + } + } + + @ReactMethod + fun startUiFrameCapture(promise: Promise) { + mainHandler.post { + try { + if (capturing) { + promise.reject("ALREADY_CAPTURING", "UI frame capture already running") + return@post + } + resetCaptureCounters() + val activity = reactContext.currentActivity + if (activity == null) { + promise.reject("NO_ACTIVITY", "No current activity for frame capture") + return@post + } + updateRefreshPeriod(activity) + if (refreshPeriodNs <= 0L || refreshRateHz <= 0.0) { + promise.reject("INVALID_REFRESH", "refresh period/rate must be positive") + return@post + } + + val window = activity.window + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && window != null) { + val listener = + Window.OnFrameMetricsAvailableListener { _, frameMetrics, _ -> + val totalNs = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION) + recordFrameMetricsDuration(totalNs) + } + window.addOnFrameMetricsAvailableListener(listener, mainHandler) + frameMetricsListener = listener + captureWindowRef = WeakReference(window) + captureSource = "FrameMetrics" + capturing = true + val result = Arguments.createMap() + result.putBoolean("started", true) + result.putString("source", captureSource) + promise.resolve(result) + } else { + startChoreographerCapture(promise) + } + } catch (e: Exception) { + try { + startChoreographerCapture(promise) + } catch (e2: Exception) { + promise.reject("FRAME_CAPTURE_START", e2) + } + } + } + } + + @ReactMethod + fun stopUiFrameCapture(promise: Promise) { + mainHandler.post { + try { + // Idempotent: safe for finally/cleanup when already stopped. + val map = stopCaptureInternal(idempotent = true) + promise.resolve(map) + } catch (e: Exception) { + capturing = false + promise.reject("FRAME_CAPTURE_STOP", e) + } + } + } + + @ReactMethod + fun writeReport(json: String, promise: Promise) { + try { + val file = File(reactContext.filesDir, REPORT_FILE) + file.writeText(json) + Log.i(LOG_TAG, "REPORT_READY path=${file.absolutePath} bytes=${json.length}") + val map = Arguments.createMap() + map.putString("path", file.absolutePath) + promise.resolve(map) + } catch (e: Exception) { + promise.reject("WRITE_REPORT", e) + } + } + + private fun stopCaptureInternal(idempotent: Boolean): WritableMap { + if (capturing) { + val listener = frameMetricsListener + val window = captureWindowRef?.get() ?: reactContext.currentActivity?.window + if (listener != null && window != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + try { + window.removeOnFrameMetricsAvailableListener(listener) + } catch (_: Throwable) { + // ignore — activity may already be gone + } + } + frameMetricsListener = null + captureWindowRef = null + + val cb = choreographerCallback + if (cb != null) { + try { + Choreographer.getInstance().removeFrameCallback(cb) + } catch (_: Throwable) { + // ignore + } + } + choreographerCallback = null + capturing = false + } else if (!idempotent) { + throw IllegalStateException("UI frame capture is not running") + } + + val map = Arguments.createMap() + map.putString("source", captureSource) + map.putInt("frameCount", frameCount) + map.putDouble("maxFrameDurationMs", maxFrameDurationNs / 1_000_000.0) + map.putDouble("totalFrameDurationMs", totalFrameDurationNs / 1_000_000.0) + map.putInt("missedFrames", missedFrames) + map.putDouble("refreshPeriodMs", refreshPeriodNs / 1_000_000.0) + map.putDouble("refreshRateHz", refreshRateHz) + map.putBoolean("wasCapturing", frameCount > 0 || captureSource != "none") + return map + } + + private fun startChoreographerCapture(promise: Promise) { + lastChoreographerNs = 0L + val callback = + object : Choreographer.FrameCallback { + override fun doFrame(frameTimeNanos: Long) { + if (!capturing) return + if (lastChoreographerNs > 0L) { + recordChoreographerInterval(frameTimeNanos - lastChoreographerNs) + } + lastChoreographerNs = frameTimeNanos + Choreographer.getInstance().postFrameCallback(this) + } + } + choreographerCallback = callback + captureSource = "Choreographer" + capturing = true + Choreographer.getInstance().postFrameCallback(callback) + val result = Arguments.createMap() + result.putBoolean("started", true) + result.putString("source", captureSource) + promise.resolve(result) + } + + /** FrameMetrics TOTAL_DURATION — duration semantics (ceil). */ + private fun recordFrameMetricsDuration(durationNs: Long) { + if (durationNs <= 0L) return + frameCount++ + totalFrameDurationNs += durationNs + if (durationNs > maxFrameDurationNs) { + maxFrameDurationNs = durationNs + } + if (refreshPeriodNs > 0L) { + val periods = ceil(durationNs.toDouble() / refreshPeriodNs.toDouble()).toInt() + missedFrames += max(0, periods - 1) + } + } + + /** Choreographer frame-time delta — interval semantics (round). */ + private fun recordChoreographerInterval(intervalNs: Long) { + if (intervalNs <= 0L) return + frameCount++ + totalFrameDurationNs += intervalNs + if (intervalNs > maxFrameDurationNs) { + maxFrameDurationNs = intervalNs + } + if (refreshPeriodNs > 0L) { + val periods = (intervalNs.toDouble() / refreshPeriodNs.toDouble()).roundToInt() + missedFrames += max(0, periods - 1) + } + } + + private fun resetCaptureCounters() { + frameCount = 0 + maxFrameDurationNs = 0L + totalFrameDurationNs = 0L + missedFrames = 0 + lastChoreographerNs = 0L + captureSource = "none" + captureWindowRef = null + } + + private fun updateRefreshPeriod(activity: android.app.Activity) { + val display = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity.display + } else { + @Suppress("DEPRECATION") + activity.windowManager?.defaultDisplay + } + val hz = display?.refreshRate?.toDouble() ?: 60.0 + refreshRateHz = if (hz > 0) hz else 60.0 + refreshPeriodNs = (1_000_000_000.0 / refreshRateHz).toLong() + } + + private fun isHermesEnabled(): Boolean { + return try { + BuildConfig::class.java.getField("IS_HERMES_ENABLED").getBoolean(null) + } catch (_: Throwable) { + true + } + } + + private fun putExtraString(map: WritableMap, extras: Bundle?, key: String) { + val value = extras?.getString(key) + if (value != null) { + map.putString(key, value) + } + } + + companion object { + const val NAME = "BenchNative" + const val LOG_TAG = "BenchNative" + const val REPORT_FILE = "gc-report.json" + const val BUILD_MANIFEST_ASSET = "build-manifest.json" + } +} diff --git a/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/BenchNativePackage.kt b/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/BenchNativePackage.kt new file mode 100644 index 000000000000..44afba0725ef --- /dev/null +++ b/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/BenchNativePackage.kt @@ -0,0 +1,17 @@ +package com.dataclient.benchmarknative + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +class BenchNativePackage : ReactPackage { + @Deprecated("ReactPackage.createNativeModules is deprecated") + override fun createNativeModules( + reactContext: ReactApplicationContext, + ): List = listOf(BenchNativeModule(reactContext)) + + override fun createViewManagers( + reactContext: ReactApplicationContext, + ): List> = emptyList() +} diff --git a/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/MainActivity.kt b/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/MainActivity.kt new file mode 100644 index 000000000000..d3fbafe41304 --- /dev/null +++ b/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/MainActivity.kt @@ -0,0 +1,30 @@ +package com.dataclient.benchmarknative + +import android.os.Bundle +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +class MainActivity : ReactActivity() { + + override fun getMainComponentName(): String = "BenchmarkNative" + + override fun onCreate(savedInstanceState: Bundle?) { + launchExtras = intent?.extras + super.onCreate(savedInstanceState) + } + + override fun onNewIntent(intent: android.content.Intent) { + super.onNewIntent(intent) + setIntent(intent) + launchExtras = intent.extras + } + + override fun createReactActivityDelegate(): ReactActivityDelegate = + DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) + + companion object { + @JvmField var launchExtras: Bundle? = null + } +} diff --git a/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/MainApplication.kt b/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/MainApplication.kt new file mode 100644 index 000000000000..b5ba9ae76bcb --- /dev/null +++ b/examples/benchmark-native/android/app/src/main/java/com/dataclient/benchmarknative/MainApplication.kt @@ -0,0 +1,26 @@ +package com.dataclient.benchmarknative + +import android.app.Application +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost + +class MainApplication : Application(), ReactApplication { + + override val reactHost: ReactHost by lazy { + getDefaultReactHost( + context = applicationContext, + packageList = + PackageList(this).packages.apply { + add(BenchNativePackage()) + }, + ) + } + + override fun onCreate() { + super.onCreate() + loadReactNative(this) + } +} diff --git a/examples/benchmark-native/android/app/src/main/res/drawable/rn_edit_text_material.xml b/examples/benchmark-native/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 000000000000..5c25e728ea2c --- /dev/null +++ b/examples/benchmark-native/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/examples/benchmark-native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/benchmark-native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f5908281d070150700378b64a84c7db1f97aa1 GIT binary patch literal 3056 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 axPAxe%7rDusX+w*00006jj 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⁣ zwiG+x#%j}{0gU~J8;<|!B1@-XaB@{KORFwrYg_8rOv({b0EO#DbeQRm;B6_9=mXGf z-x|VL{zd`)#@yN}HkCSJbjbNlE|zL3Wm9Q8HY`sV)}3%pgN>cL^67{Z;PPL(*wT8N zUjXU{@|*hvm}({wsAC=x0^ok0%UAz0;sogW{B!nDqk|JJ5x~4NfTDgP49^zeu`csl?5mY@JdQdISc zFs!E{^grmkLnUk9 zny~m)1vws@5BFI<-0Tuo2JWX(0v`W|t(wg;s--L47WTvTMz-8l#TL^=OJNRS2?_Qj z3AKT+gvbyBi#H*-tJ%tWD|>EV3wy|8qxfzS!5RW;Jpl5*zo&^UBU=fG#2}UvRyNkK zA06Dy9;K1ca@r2T>yThYgI!ont$(G{6q#2QT+00r_x0(b)gsE`lBB?2gr55gq^D3Fi&p%E(p9>U%bv zkg1Jco(RbyTX7FDHOnl7-O@ zI$AaIl?9NJKPm(WiBP`1-#CB1QzU>&hKm)fpa5DKE{2$X0hGz-0uZ?cyTk(YC!Y&| zL=1VrNERSA5NA2jq7FACfX4JfPyj5XXl1yv0>~s;eF7L2$>&oMqeTFT2m$y7FlkON z_yurD1yIOvA;5C6016pyxBznGUt0kJ&k5r#;&>Jow`r)sp9R~PmK~lz$3xH%LT*1U zJdOyABZ3!FvNoR*vN$5ykHS8f`jA4zV+|L}i1C4`B2c{R0;UdYxaU|H)2avz@ z=mEYc|2S<+(B2Tj+FkX+2D+yFI!k9lWMA61DJ{)e;lum$(;O87?vGJJe!KtK04+N_ zI*P~t@dUb>9Xh{dbyl{-ZQ(UMgz7$|QfL5XSPkskt^NgctYC#;4WcZB1@%@wy@2t3 z2z0DI7&%b$*Aw~abe?GxE`ez@+6hOh-6*8fHRV{1os$EL@}uUZeG4h1&Be`98q*7j z=3-v+lhIjfWVo12!<>%V^a6lTgW3+_#W6n|p*~==zOH7z$0{LSZk(Tpd7EaD04hnA zL;#fxS0aD{`5^&D`}>0Uq?byDD-l2=!wm_bLcUl4gc(% za1p|itVANvFF>hghAS07Im1;IK;|b*W)}VDyI;BIp2=K*yu2a)j?B|f<44NI$NbmJ z#dE0>jI$fMr&@>4kN8MLFb4&2O9fEKaQg%(QO$4_1rVQywG^CmBLh#}_7gKW3vd?| z2?1^&KWq8}8I^_S0|)MowU_pw$q@nl@Nkn$z>BQq_KA^9yaR`(R3u{{Ig;cwt z@AJ^{ODQCm^neroM9nKNUAXi9RCK`OsP_LuR0PUR(YZCCX5dNF6VzcoK&=b^r`W?ltt|*F zpkoae%ZT{C1h~EcFui~b7fF`vb<<~j_VquuUA$}QqIKYELPp#;{u?q8Dz}WAG-(3; zjrm$i%7UbyZMM(Y{>!uJ#vNB?R~B{6Htp=>e*<{fQQ5W7V(1coCWlOON!MzZxhum| ztZBQpGR z;~#ur^&PockKdV{Q6R>o`Pl{0x!DEbpZ7y9Y;*ZvE!*gU`V1W3znva{f=?WO5I&>B z&hw6}tjECtaghm5z|C#%M;Yf_*pI^};h}Vl=^r9EN=tVDj86D;C$jIJ?K7VP+00000NkvXXu0mjf D5i!M* literal 0 HcmV?d00001 diff --git a/examples/benchmark-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/examples/benchmark-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..459ca609d3ae0d3943ab44cdc27feef9256dc6d7 GIT binary patch literal 7098 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 literal 0 HcmV?d00001 diff --git a/examples/benchmark-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/benchmark-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca12fe024be86e868d14e91120a6902f8e88ac6 GIT binary patch literal 6464 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 B0*L?s literal 0 HcmV?d00001 diff --git a/examples/benchmark-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/examples/benchmark-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..8e19b410a1b15ff180f3dacac19395fe3046cdec GIT binary patch literal 10676 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 literal 0 HcmV?d00001 diff --git a/examples/benchmark-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/examples/benchmark-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4c19a13c239cb67b8a2134ddd5f325db1d2d5bee GIT binary patch literal 15523 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 BIP(Ai literal 0 HcmV?d00001 diff --git a/examples/benchmark-native/android/app/src/main/res/values/strings.xml b/examples/benchmark-native/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000000..a3a26959f049 --- /dev/null +++ b/examples/benchmark-native/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + BenchmarkNative + diff --git a/examples/benchmark-native/android/app/src/main/res/values/styles.xml b/examples/benchmark-native/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..7ba83a2ad5a2 --- /dev/null +++ b/examples/benchmark-native/android/app/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/examples/benchmark-native/android/build.gradle b/examples/benchmark-native/android/build.gradle new file mode 100644 index 000000000000..dad99b022ac9 --- /dev/null +++ b/examples/benchmark-native/android/build.gradle @@ -0,0 +1,21 @@ +buildscript { + ext { + buildToolsVersion = "36.0.0" + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + ndkVersion = "27.1.12297006" + kotlinVersion = "2.1.20" + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle") + classpath("com.facebook.react:react-native-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") + } +} + +apply plugin: "com.facebook.react.rootproject" diff --git a/examples/benchmark-native/android/gradle.properties b/examples/benchmark-native/android/gradle.properties new file mode 100644 index 000000000000..9afe61598fc5 --- /dev/null +++ b/examples/benchmark-native/android/gradle.properties @@ -0,0 +1,44 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -PreactNativeArchitectures=x86_64 +reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=true + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true + +# Use this property to enable edge-to-edge display support. +# This allows your app to draw behind system bars for an immersive UI. +# Note: Only works with ReactActivity and should not be used with custom Activity. +edgeToEdgeEnabled=false diff --git a/examples/benchmark-native/android/gradle/wrapper/gradle-wrapper.jar b/examples/benchmark-native/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..61285a659d17295f1de7c53e24fdf13ad755c379 GIT binary patch literal 46175 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

pVX@HRij*jw$g4?}Pn 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 literal 0 HcmV?d00001 diff --git a/examples/benchmark-native/android/gradle/wrapper/gradle-wrapper.properties b/examples/benchmark-native/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..37f78a6af837 --- /dev/null +++ b/examples/benchmark-native/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/benchmark-native/android/gradlew b/examples/benchmark-native/android/gradlew new file mode 100644 index 000000000000..adff685a0348 --- /dev/null +++ b/examples/benchmark-native/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/benchmark-native/android/gradlew.bat b/examples/benchmark-native/android/gradlew.bat new file mode 100644 index 000000000000..39baf4d68461 --- /dev/null +++ b/examples/benchmark-native/android/gradlew.bat @@ -0,0 +1,98 @@ +@REM Copyright (c) Meta Platforms, Inc. and affiliates. +@REM +@REM This source code is licensed under the MIT license found in the +@REM LICENSE file in the root directory of this source tree. + +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/benchmark-native/android/settings.gradle b/examples/benchmark-native/android/settings.gradle new file mode 100644 index 000000000000..dbaef8911829 --- /dev/null +++ b/examples/benchmark-native/android/settings.gradle @@ -0,0 +1,6 @@ +pluginManagement { includeBuild("../../../node_modules/@react-native/gradle-plugin") } +plugins { id("com.facebook.react.settings") } +extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } +rootProject.name = 'BenchmarkNative' +include ':app' +includeBuild('../../../node_modules/@react-native/gradle-plugin') diff --git a/examples/benchmark-native/app.json b/examples/benchmark-native/app.json new file mode 100644 index 000000000000..70edac4556d2 --- /dev/null +++ b/examples/benchmark-native/app.json @@ -0,0 +1,4 @@ +{ + "name": "BenchmarkNative", + "displayName": "BenchmarkNative" +} diff --git a/examples/benchmark-native/babel.config.js b/examples/benchmark-native/babel.config.js new file mode 100644 index 000000000000..f7b3da3b33d1 --- /dev/null +++ b/examples/benchmark-native/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], +}; diff --git a/examples/benchmark-native/index.js b/examples/benchmark-native/index.js new file mode 100644 index 000000000000..9b7393291400 --- /dev/null +++ b/examples/benchmark-native/index.js @@ -0,0 +1,9 @@ +/** + * @format + */ + +import { AppRegistry } from 'react-native'; +import App from './App'; +import { name as appName } from './app.json'; + +AppRegistry.registerComponent(appName, () => App); diff --git a/examples/benchmark-native/jest.config.js b/examples/benchmark-native/jest.config.js new file mode 100644 index 000000000000..1c2b0e3c866c --- /dev/null +++ b/examples/benchmark-native/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'react-native', + testPathIgnorePatterns: ['/node_modules/', '/android/'], + modulePathIgnorePatterns: ['/android/'], +}; diff --git a/examples/benchmark-native/metro.config.js b/examples/benchmark-native/metro.config.js new file mode 100644 index 000000000000..fdf64ff51740 --- /dev/null +++ b/examples/benchmark-native/metro.config.js @@ -0,0 +1,23 @@ +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const path = require('path'); + +const projectRoot = __dirname; +const monorepoRoot = path.resolve(projectRoot, '../..'); + +/** + * Metro configuration for yarn workspace monorepo. + * https://reactnative.dev/docs/metro + * + * @type {import('@react-native/metro-config').MetroConfig} + */ +const config = { + watchFolders: [monorepoRoot], + resolver: { + nodeModulesPaths: [ + path.resolve(projectRoot, 'node_modules'), + path.resolve(monorepoRoot, 'node_modules'), + ], + }, +}; + +module.exports = mergeConfig(getDefaultConfig(projectRoot), config); diff --git a/examples/benchmark-native/package.json b/examples/benchmark-native/package.json new file mode 100644 index 000000000000..802bf9fe6d13 --- /dev/null +++ b/examples/benchmark-native/package.json @@ -0,0 +1,44 @@ +{ + "name": "example-benchmark-native", + "version": "0.0.1", + "private": true, + "scripts": { + "android": "react-native run-android --mode=release", + "android:debug": "react-native run-android", + "build:android:release": "bash scripts/build-android-release.sh", + "build:android:release:gradle": "cd android && ./gradlew assembleRelease", + "lint": "ESLINT_USE_FLAT_CONFIG=false eslint --config .eslintrc.js --ignore-pattern android --ignore-pattern artifacts .", + "start": "react-native start", + "test": "jest", + "typecheck": "tsc --noEmit", + "collect": "bash scripts/collect-report.sh", + "matrix": "bash scripts/run-matrix.sh" + }, + "dependencies": { + "@data-client/core": "workspace:*", + "react": "19.2.3", + "react-native": "0.86.0" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "20.1.0", + "@react-native-community/cli-platform-android": "20.1.0", + "@react-native/babel-preset": "0.86.0", + "@react-native/eslint-config": "0.86.0", + "@react-native/metro-config": "0.86.0", + "@react-native/typescript-config": "0.86.0", + "@types/jest": "^29.5.13", + "@types/react": "^19.2.0", + "@types/react-test-renderer": "^19.1.0", + "eslint": "^8.19.0", + "jest": "^29.6.3", + "prettier": "2.8.8", + "react-test-renderer": "19.2.3", + "typescript": "^5.8.3" + }, + "engines": { + "node": ">= 22.11.0" + } +} diff --git a/examples/benchmark-native/scripts/build-android-release.sh b/examples/benchmark-native/scripts/build-android-release.sh new file mode 100644 index 000000000000..07d4692f5f82 --- /dev/null +++ b/examples/benchmark-native/scripts/build-android-release.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Authoritative release build: prepare BuildManifest → Gradle assembleRelease → sidecar. +# Split APKs are unsupported; requires single app-release.apk. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +APK="${ROOT}/android/app/build/outputs/apk/release/app-release.apk" + +echo "== prepare BuildManifest v1 ==" +node "${ROOT}/scripts/build-manifest.cjs" prepare + +echo "== gradle assembleRelease ==" +( + cd "${ROOT}/android" + ./gradlew assembleRelease +) + +if [[ ! -f "${APK}" ]]; then + echo "error: expected single release APK at ${APK}" >&2 + echo "split / multi-APK installs are unsupported" >&2 + exit 1 +fi + +echo "== finalize sidecar ==" +node "${ROOT}/scripts/build-manifest.cjs" finalize "${APK}" + +echo "Release build complete: ${APK}" +echo "Sidecar: ${ROOT}/artifacts/build-sidecar.json" diff --git a/examples/benchmark-native/scripts/build-identity.cjs b/examples/benchmark-native/scripts/build-identity.cjs new file mode 100644 index 000000000000..c11d24178906 --- /dev/null +++ b/examples/benchmark-native/scripts/build-identity.cjs @@ -0,0 +1,101 @@ +/** + * Deterministic BuildManifest / sidecar identity helpers (pure). + * + * buildId — source-build identity: sha256 over canonical v1 inputs + * (schemaVersion, gitCommit, gitDirty, sourceDigest). + * Does NOT include createdAt or artifact hashes. + * sidecarId — artifact-aware identity: sha256(buildId || apkSha256). + */ +const crypto = require('crypto'); + +/** + * @param {{ schemaVersion: number, gitCommit: string, gitDirty: boolean, sourceDigest: string }} inputs + * @returns {string} hex sha256 + */ +function computeBuildId(inputs) { + if (inputs.schemaVersion !== 1) { + throw new Error(`unsupported schemaVersion=${inputs.schemaVersion}`); + } + if (typeof inputs.gitCommit !== 'string' || !inputs.gitCommit) { + throw new Error('gitCommit required'); + } + if (typeof inputs.gitDirty !== 'boolean') { + throw new Error('gitDirty must be boolean'); + } + if (typeof inputs.sourceDigest !== 'string' || !/^[0-9a-f]{64}$/.test(inputs.sourceDigest)) { + throw new Error('sourceDigest must be 64-char hex sha256'); + } + // Canonical encoding: fixed field order, no timestamps/artifacts. + const payload = [ + String(inputs.schemaVersion), + inputs.gitCommit, + inputs.gitDirty ? '1' : '0', + inputs.sourceDigest, + ].join('\0'); + return crypto.createHash('sha256').update(payload, 'utf8').digest('hex'); +} + +/** + * @param {{ buildId: string, apkSha256: string }} inputs + * @returns {string} hex sha256 + */ +function computeSidecarId(inputs) { + if (typeof inputs.buildId !== 'string' || !/^[0-9a-f]{64}$/.test(inputs.buildId)) { + throw new Error('buildId must be 64-char hex sha256'); + } + if (typeof inputs.apkSha256 !== 'string' || !/^[0-9a-f]{64}$/.test(inputs.apkSha256)) { + throw new Error('apkSha256 must be 64-char hex sha256'); + } + return crypto + .createHash('sha256') + .update(`${inputs.buildId}\0${inputs.apkSha256}`, 'utf8') + .digest('hex'); +} + +/** + * Recompute buildId from manifest fields and ensure it matches. + * Catches tampered metadata or a forged buildId. + * @param {{ schemaVersion: number, buildId: string, gitCommit: string, gitDirty: boolean, sourceDigest: string }} manifest + */ +function verifyManifestBuildId(manifest) { + const expected = computeBuildId({ + schemaVersion: manifest.schemaVersion, + gitCommit: manifest.gitCommit, + gitDirty: manifest.gitDirty, + sourceDigest: manifest.sourceDigest, + }); + if (manifest.buildId !== expected) { + throw new Error( + `buildId mismatch: embedded=${manifest.buildId} expected=${expected} (tampered metadata or forged buildId)`, + ); + } + return expected; +} + +/** + * Verify sidecar buildId + sidecarId against fields + apkSha256. + * @param {{ schemaVersion: number, buildId: string, gitCommit: string, gitDirty: boolean, sourceDigest: string, apkSha256: string, sidecarId?: string }} sidecar + */ +function verifySidecarIdentity(sidecar) { + verifyManifestBuildId(sidecar); + const expectedSidecarId = computeSidecarId({ + buildId: sidecar.buildId, + apkSha256: sidecar.apkSha256, + }); + if (sidecar.sidecarId == null || sidecar.sidecarId === '') { + throw new Error('sidecarId required (artifact-aware identity)'); + } + if (sidecar.sidecarId !== expectedSidecarId) { + throw new Error( + `sidecarId mismatch: embedded=${sidecar.sidecarId} expected=${expectedSidecarId}`, + ); + } + return expectedSidecarId; +} + +module.exports = { + computeBuildId, + computeSidecarId, + verifyManifestBuildId, + verifySidecarIdentity, +}; diff --git a/examples/benchmark-native/scripts/build-manifest.cjs b/examples/benchmark-native/scripts/build-manifest.cjs new file mode 100644 index 000000000000..dc6087ed9450 --- /dev/null +++ b/examples/benchmark-native/scripts/build-manifest.cjs @@ -0,0 +1,245 @@ +#!/usr/bin/env node +/** + * BuildManifest v1 prepare / finalize for release-Hermes Android GC bench. + * + * prepare — write android/app/src/main/assets/build-manifest.json + * buildId = deterministic sha256(schemaVersion, gitCommit, gitDirty, sourceDigest) + * finalize — verify embedded buildId, write artifacts/build-sidecar.json with + * apkSha256 (artifact identity) + sidecarId (buildId∥apkSha256) + * digest — print current sourceDigest (for stale checks) + * verify — verify manifest and/or sidecar identity + * + * Inputs: sorted paths under this app (tracked+untracked contents) plus + * packages/core/src, excluding build/node_modules/artifacts/.jdk/generated. + */ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const { + computeBuildId, + computeSidecarId, + verifyManifestBuildId, + verifySidecarIdentity, +} = require('./build-identity.cjs'); + +const ROOT = path.resolve(__dirname, '..'); +const REPO = path.resolve(ROOT, '../..'); +const ASSET_DIR = path.join(ROOT, 'android/app/src/main/assets'); +const MANIFEST_PATH = path.join(ASSET_DIR, 'build-manifest.json'); +const SIDECAR_PATH = path.join(ROOT, 'artifacts/build-sidecar.json'); +const DEFAULT_APK = path.join( + ROOT, + 'android/app/build/outputs/apk/release/app-release.apk', +); + +const IGNORE_DIR_NAMES = new Set([ + 'node_modules', + 'build', + 'artifacts', + '.jdk', + '.git', + '.gradle', + '.idea', + 'coverage', + 'Pods', +]); + +function shouldSkipDir(name) { + return IGNORE_DIR_NAMES.has(name) || name.startsWith('.'); +} + +function walkFiles(dir, out) { + if (!fs.existsSync(dir)) return; + for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { + if (ent.name === 'build-manifest.json') continue; // generated + const full = path.join(dir, ent.name); + if (ent.isDirectory()) { + if (shouldSkipDir(ent.name)) continue; + walkFiles(full, out); + } else if (ent.isFile()) { + out.push(full); + } + } +} + +function collectInputFiles() { + const files = []; + walkFiles(ROOT, files); + walkFiles(path.join(REPO, 'packages/core/src'), files); + return files + .map(f => path.resolve(f)) + .filter( + f => + !f.includes(`${path.sep}android${path.sep}app${path.sep}build${path.sep}`), + ) + .sort((a, b) => a.localeCompare(b)); +} + +function sha256File(filePath) { + const h = crypto.createHash('sha256'); + h.update(fs.readFileSync(filePath)); + return h.digest('hex'); +} + +function sourceDigest() { + const h = crypto.createHash('sha256'); + for (const file of collectInputFiles()) { + const rel = path.relative(REPO, file).split(path.sep).join('/'); + h.update(rel); + h.update('\0'); + h.update(fs.readFileSync(file)); + h.update('\0'); + } + return h.digest('hex'); +} + +function gitMeta() { + try { + const commit = execSync('git rev-parse HEAD', { + cwd: REPO, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + const dirty = + execSync('git status --porcelain', { + cwd: REPO, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim().length > 0; + return { gitCommit: commit, gitDirty: dirty }; + } catch { + return { gitCommit: 'unknown', gitDirty: true }; + } +} + +function prepare() { + const digest = sourceDigest(); + const { gitCommit, gitDirty } = gitMeta(); + const schemaVersion = 1; + const buildId = computeBuildId({ + schemaVersion, + gitCommit, + gitDirty, + sourceDigest: digest, + }); + const manifest = { + schemaVersion, + buildId, + gitCommit, + gitDirty, + sourceDigest: digest, + createdAt: new Date().toISOString(), + }; + verifyManifestBuildId(manifest); + fs.mkdirSync(ASSET_DIR, { recursive: true }); + fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n'); + console.log(`Wrote ${MANIFEST_PATH}`); + console.log(`buildId=${buildId} (source-build identity)`); + console.log(`sourceDigest=${digest}`); + return manifest; +} + +function finalize(apkPath = DEFAULT_APK) { + if (!fs.existsSync(MANIFEST_PATH)) { + throw new Error('missing build-manifest.json — run prepare first'); + } + if (!fs.existsSync(apkPath)) { + throw new Error(`APK not found: ${apkPath}`); + } + const splitsDir = path.join( + ROOT, + 'android/app/build/outputs/apk/release/splits', + ); + if (fs.existsSync(splitsDir)) { + throw new Error( + 'split APKs unsupported; use a single release APK (assembleRelease app-release.apk)', + ); + } + if (path.basename(apkPath) !== 'app-release.apk') { + console.warn( + `warning: expected app-release.apk, got ${path.basename(apkPath)}`, + ); + } + + const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8')); + verifyManifestBuildId(manifest); + + // Stale prepare: source tree changed after prepare but before finalize. + const currentDigest = sourceDigest(); + if (currentDigest !== manifest.sourceDigest) { + throw new Error( + `stale BuildManifest: sourceDigest ${manifest.sourceDigest} != current ${currentDigest}; re-run prepare`, + ); + } + + const apkSha256 = sha256File(apkPath); + const apkSizeBytes = fs.statSync(apkPath).size; + const sidecarId = computeSidecarId({ + buildId: manifest.buildId, + apkSha256, + }); + const sidecar = { + schemaVersion: 1, + buildId: manifest.buildId, + sourceDigest: manifest.sourceDigest, + gitCommit: manifest.gitCommit, + gitDirty: manifest.gitDirty, + apkPath: path.resolve(apkPath), + apkSha256, + apkSizeBytes, + sidecarId, + builtAt: new Date().toISOString(), + }; + verifySidecarIdentity(sidecar); + fs.mkdirSync(path.dirname(SIDECAR_PATH), { recursive: true }); + fs.writeFileSync(SIDECAR_PATH, JSON.stringify(sidecar, null, 2) + '\n'); + console.log(`Wrote ${SIDECAR_PATH}`); + console.log(`apkSha256=${apkSha256} (artifact identity)`); + console.log(`sidecarId=${sidecarId}`); + return sidecar; +} + +function verify() { + if (fs.existsSync(MANIFEST_PATH)) { + const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8')); + verifyManifestBuildId(manifest); + console.log(`manifest buildId ok: ${manifest.buildId}`); + } else { + console.log('no manifest present'); + } + if (fs.existsSync(SIDECAR_PATH)) { + const sidecar = JSON.parse(fs.readFileSync(SIDECAR_PATH, 'utf8')); + verifySidecarIdentity(sidecar); + console.log(`sidecar identity ok: sidecarId=${sidecar.sidecarId}`); + } else { + console.log('no sidecar present'); + } +} + +function main() { + const cmd = process.argv[2] || 'digest'; + if (cmd === 'prepare') { + prepare(); + } else if (cmd === 'finalize') { + finalize(process.argv[3] || DEFAULT_APK); + } else if (cmd === 'digest') { + console.log(sourceDigest()); + } else if (cmd === 'hash') { + const filePath = process.argv[3]; + if (!filePath) { + console.error('usage: build-manifest.cjs hash '); + process.exit(1); + } + console.log(sha256File(filePath)); + } else if (cmd === 'verify') { + verify(); + } else { + console.error( + 'usage: build-manifest.cjs prepare|finalize [apk]|digest|hash |verify', + ); + process.exit(1); + } +} + +main(); diff --git a/examples/benchmark-native/scripts/collect-report.sh b/examples/benchmark-native/scripts/collect-report.sh new file mode 100644 index 000000000000..7c182641697a --- /dev/null +++ b/examples/benchmark-native/scripts/collect-report.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +# Install/start a single release APK, pass scenario axes, pull report, verify provenance. +# +# Requires exactly one adb device unless ANDROID_SERIAL is set and valid. +# Split APKs unsupported. Sidecar (artifacts/build-sidecar.json) is authority — +# never the live checkout. Intent label is optional; commit is not authority. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=validate-config.sh +source "${ROOT}/scripts/validate-config.sh" + +APP_ID="${APP_ID:-com.dataclient.benchmarknative}" +ACTIVITY="${ACTIVITY:-${APP_ID}/.MainActivity}" +APK="${APK:-${ROOT}/android/app/build/outputs/apk/release/app-release.apk}" +SIDECAR="${SIDECAR:-${ROOT}/artifacts/build-sidecar.json}" +OUT="${OUT:-${ROOT}/artifacts/gc-report.json}" +TIMEOUT_SEC="${TIMEOUT_SEC:-600}" + +CANDIDATE_KIND="${CANDIDATE_KIND:-entity}" +PATTERN="${PATTERN:-unique}" +COUNT="${COUNT:-1000}" +CONTROL="${CONTROL:-gc}" +SAMPLES="${SAMPLES:-1}" +LABEL="${LABEL:-}" +INSTALL="${INSTALL:-1}" + +# Validate axes before any install/device work. +validate_host_config "${CANDIDATE_KIND}" "${PATTERN}" "${COUNT}" "${CONTROL}" "${SAMPLES}" + +mkdir -p "$(dirname "${OUT}")" + +if ! command -v adb >/dev/null 2>&1; then + echo "error: adb not found on PATH" >&2 + exit 1 +fi + +if [[ ! -f "${SIDECAR}" ]]; then + echo "error: missing sidecar ${SIDECAR}" >&2 + echo "Build with: yarn workspace example-benchmark-native build:android:release" >&2 + exit 1 +fi + +if [[ ! -f "${APK}" ]]; then + echo "error: APK not found at ${APK}" >&2 + echo "Split APKs are unsupported; need single app-release.apk" >&2 + exit 1 +fi + +if [[ "$(basename "${APK}")" != "app-release.apk" ]]; then + echo "error: only single release APK app-release.apk is supported (got $(basename "${APK}"))" >&2 + exit 1 +fi + +# --- device selection --- +mapfile -t _DEVICES < <(adb devices | awk 'NR>1 && $2=="device" {print $1}') +if [[ -n "${ANDROID_SERIAL:-}" ]]; then + FOUND=0 + for d in "${_DEVICES[@]:-}"; do + if [[ "${d}" == "${ANDROID_SERIAL}" ]]; then + FOUND=1 + break + fi + done + if [[ "${FOUND}" != "1" ]]; then + echo "error: ANDROID_SERIAL=${ANDROID_SERIAL} is not an eligible 'device'" >&2 + adb devices >&2 || true + exit 1 + fi + SERIAL="${ANDROID_SERIAL}" +else + if [[ "${#_DEVICES[@]}" -ne 1 ]]; then + echo "error: need exactly one eligible adb device (found ${#_DEVICES[@]}); set ANDROID_SERIAL" >&2 + adb devices >&2 || true + exit 1 + fi + SERIAL="${_DEVICES[0]}" +fi +ADB=(adb -s "${SERIAL}") +echo "Using device ${SERIAL}" + +# --- provenance: local APK must match sidecar (authority) --- +node -e ' +const fs=require("fs"); +const { verifySidecarIdentity } = require(process.argv[2]); +const sidecar=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); +if (sidecar.schemaVersion!==1) { console.error("bad sidecar schema"); process.exit(2); } +verifySidecarIdentity(sidecar); +console.log("sidecar identity verified buildId="+sidecar.buildId+" sidecarId="+sidecar.sidecarId); +' "${SIDECAR}" "${ROOT}/scripts/build-identity.cjs" + +SIDECAR_BUILD_ID="$(node -e 'console.log(JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).buildId)' "${SIDECAR}")" +SIDECAR_DIGEST="$(node -e 'console.log(JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).sourceDigest)' "${SIDECAR}")" +SIDECAR_APK_SHA="$(node -e 'console.log(JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).apkSha256)' "${SIDECAR}")" +SIDECAR_ID="$(node -e 'console.log(JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).sidecarId)' "${SIDECAR}")" + +LOCAL_APK_SHA="$(node "${ROOT}/scripts/build-manifest.cjs" hash "${APK}")" +if [[ "${LOCAL_APK_SHA}" != "${SIDECAR_APK_SHA}" ]]; then + echo "error: local APK sha256 ${LOCAL_APK_SHA} != sidecar ${SIDECAR_APK_SHA} (stale artifact)" >&2 + exit 1 +fi + +CURRENT_DIGEST="$(node "${ROOT}/scripts/build-manifest.cjs" digest)" +if [[ "${CURRENT_DIGEST}" != "${SIDECAR_DIGEST}" ]]; then + echo "error: current sourceDigest ${CURRENT_DIGEST} != sidecar ${SIDECAR_DIGEST}" >&2 + echo "Rebuild with yarn workspace example-benchmark-native build:android:release" >&2 + exit 1 +fi + +if [[ "${INSTALL}" == "1" ]]; then + echo "Installing single APK ${APK}…" + "${ADB[@]}" install -r "${APK}" +fi + +# Hash the installed base APK (stream via adb exec-out / pull path). +TMP_INSTALLED="$(mktemp)" +cleanup() { rm -f "${TMP_INSTALLED}"; } +trap cleanup EXIT + +# Prefer `pm path` → pull the single base APK. +INSTALLED_PATH="$("${ADB[@]}" shell pm path "${APP_ID}" | tr -d '\r' | awk -F: '/^package:/{print $2; exit}')" +if [[ -z "${INSTALLED_PATH}" ]]; then + echo "error: could not resolve installed package path for ${APP_ID}" >&2 + exit 1 +fi +PATH_COUNT="$("${ADB[@]}" shell pm path "${APP_ID}" | tr -d '\r' | grep -c '^package:' || true)" +if [[ "${PATH_COUNT}" -ne 1 ]]; then + echo "error: expected exactly one installed APK path (split APKs unsupported); got ${PATH_COUNT}" >&2 + "${ADB[@]}" shell pm path "${APP_ID}" >&2 || true + exit 1 +fi + +"${ADB[@]}" pull "${INSTALLED_PATH}" "${TMP_INSTALLED}" >/dev/null +INSTALLED_SHA="$(node "${ROOT}/scripts/build-manifest.cjs" hash "${TMP_INSTALLED}")" +if [[ "${INSTALLED_SHA}" != "${SIDECAR_APK_SHA}" ]]; then + echo "error: installed APK sha256 ${INSTALLED_SHA} != sidecar ${SIDECAR_APK_SHA}" >&2 + exit 1 +fi +echo "installedApkSha256=${INSTALLED_SHA}" + +# Clear previous report +"${ADB[@]}" shell "run-as ${APP_ID} rm -f files/gc-report.json" 2>/dev/null || true + +echo "Starting ${ACTIVITY} with axes ${CANDIDATE_KIND}/${PATTERN}/${COUNT}/interaction/${CONTROL}…" +"${ADB[@]}" logcat -c || true +"${ADB[@]}" shell am force-stop "${APP_ID}" || true + +START_ARGS=( + am start -n "${ACTIVITY}" + --ez autoRun true + --es candidateKind "${CANDIDATE_KIND}" + --es pattern "${PATTERN}" + --ei count "${COUNT}" + --es control "${CONTROL}" + --ei samples "${SAMPLES}" +) +if [[ -n "${LABEL}" ]]; then + START_ARGS+=(--es label "${LABEL}") +fi +"${ADB[@]}" shell "${START_ARGS[@]}" + +echo "Waiting for REPORT_READY (timeout ${TIMEOUT_SEC}s)…" +deadline=$((SECONDS + TIMEOUT_SEC)) +found=0 +while (( SECONDS < deadline )); do + if "${ADB[@]}" logcat -d -s BenchNative:I | grep -q 'REPORT_READY'; then + found=1 + break + fi + if "${ADB[@]}" shell "run-as ${APP_ID} ls files/gc-report.json" >/dev/null 2>&1; then + found=1 + break + fi + sleep 2 +done + +if [[ "${found}" != "1" ]]; then + echo "error: timed out waiting for report" >&2 + "${ADB[@]}" logcat -d -s BenchNative:I ReactNativeJS:E AndroidRuntime:E | tail -n 80 >&2 || true + exit 1 +fi + +"${ADB[@]}" shell "run-as ${APP_ID} cat files/gc-report.json" > "${OUT}" +echo "Wrote ${OUT}" + +# Verify embedded buildId matches sidecar; attach sidecar provenance + installed hash. +node -e ' +const fs=require("fs"); +const out=process.argv[1], sidecarPath=process.argv[2], installedSha=process.argv[3]; +const report=JSON.parse(fs.readFileSync(out,"utf8")); +const sidecar=JSON.parse(fs.readFileSync(sidecarPath,"utf8")); +const embedded=report.build && report.build.buildId; +if (!embedded || embedded !== sidecar.buildId) { + console.error("error: report buildId", embedded, "!= sidecar", sidecar.buildId); + process.exit(1); +} +report.build = report.build || {}; +report.build.sidecar = { + buildId: sidecar.buildId, + sourceDigest: sidecar.sourceDigest, + apkSha256: sidecar.apkSha256, + apkPath: sidecar.apkPath, + sidecarId: sidecar.sidecarId, +}; +report.build.installedApkSha256 = installedSha; +report.build.apkSizeBytes = sidecar.apkSizeBytes; +fs.writeFileSync(out, JSON.stringify(report, null, 2)); +console.log("provenance ok buildId="+sidecar.buildId+" sidecarId="+sidecar.sidecarId); +' "${OUT}" "${SIDECAR}" "${INSTALLED_SHA}" + +echo "Done." diff --git a/examples/benchmark-native/scripts/run-matrix.sh b/examples/benchmark-native/scripts/run-matrix.sh new file mode 100644 index 000000000000..c55d1d831963 --- /dev/null +++ b/examples/benchmark-native/scripts/run-matrix.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Matrix runner over the full canonical android interaction axes. +# +# Generates every supported scenario: +# entity|endpoint|mixed × unique × {1000,10000,100000} × {gc,no-gc} +# entity × duplicate × {1000,10000,100000} × {gc,no-gc} +# +# Safety: count=100000 is skipped unless FULL=1 or an explicit filter selects it. +# +# Usage: +# bash scripts/run-matrix.sh +# bash scripts/run-matrix.sh entity/unique/1000 +# bash scripts/run-matrix.sh entity/unique/100000 # 100k via filter +# FULL=1 SAMPLES=5 bash scripts/run-matrix.sh # entire matrix incl. 100k +# FULL=1 bash scripts/run-matrix.sh /100000/ # only 100k rows +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=validate-config.sh +source "${ROOT}/scripts/validate-config.sh" + +FILTER="${1:-}" +OUT_DIR="${OUT_DIR:-${ROOT}/artifacts/matrix}" +SAMPLES="${SAMPLES:-1}" +INSTALL_FIRST="${INSTALL_FIRST:-1}" +FULL="${FULL:-0}" + +mkdir -p "${OUT_DIR}" + +KINDS=(entity endpoint mixed) +COUNTS=(1000 10000 100000) +CONTROLS=(gc no-gc) + +SCENARIOS=() + +for kind in "${KINDS[@]}"; do + for count in "${COUNTS[@]}"; do + for control in "${CONTROLS[@]}"; do + SCENARIOS+=("${kind}/unique/${count}/${control}") + done + done +done + +for count in "${COUNTS[@]}"; do + for control in "${CONTROLS[@]}"; do + SCENARIOS+=("entity/duplicate/${count}/${control}") + done +done + +install_flag=1 +if [[ "${INSTALL_FIRST}" != "1" ]]; then + install_flag=0 +fi + +ran=0 +skipped_100k=0 +for spec in "${SCENARIOS[@]}"; do + if [[ -n "${FILTER}" && "${spec}" != *"${FILTER}"* ]]; then + continue + fi + IFS='/' read -r kind pattern count control <<<"${spec}" + + # 100k safety: require FULL=1, or a non-empty filter that already matched this row. + if [[ "${count}" == "100000" && "${FULL}" != "1" && -z "${FILTER}" ]]; then + skipped_100k=$((skipped_100k + 1)) + continue + fi + + validate_host_config "${kind}" "${pattern}" "${count}" "${control}" "${SAMPLES}" + + echo "=== matrix ${kind}/${pattern}/${count}/interaction/${control} ===" + OUT="${OUT_DIR}/android-${kind}-${pattern}-${count}-interaction-${control}.json" \ + CANDIDATE_KIND="${kind}" \ + PATTERN="${pattern}" \ + COUNT="${count}" \ + CONTROL="${control}" \ + SAMPLES="${SAMPLES}" \ + INSTALL="${install_flag}" \ + bash "${ROOT}/scripts/collect-report.sh" + install_flag=0 + ran=$((ran + 1)) +done + +if [[ "${ran}" -eq 0 ]]; then + echo "error: no scenarios matched (filter=${FILTER:-} FULL=${FULL})" >&2 + echo "hint: FULL=1 includes all 100k rows; or filter e.g. entity/unique/100000" >&2 + exit 1 +fi + +if [[ "${skipped_100k}" -gt 0 ]]; then + echo "note: skipped ${skipped_100k} × 100k scenarios (set FULL=1 or pass a 100k filter)" +fi + +echo "Matrix complete (${ran} scenarios) → ${OUT_DIR}" diff --git a/examples/benchmark-native/scripts/validate-config.sh b/examples/benchmark-native/scripts/validate-config.sh new file mode 100644 index 000000000000..50c5be1df891 --- /dev/null +++ b/examples/benchmark-native/scripts/validate-config.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Shared host-side validation for collect-report / run-matrix. +# Usage: source scripts/validate-config.sh && validate_host_config KIND PATTERN COUNT CONTROL SAMPLES + +validate_host_config() { + local kind="${1:-}" + local pattern="${2:-}" + local count="${3:-}" + local control="${4:-}" + local samples="${5:-1}" + + case "${kind}" in + entity|endpoint|mixed) ;; + *) + echo "error: invalid CANDIDATE_KIND=${kind}; expected entity|endpoint|mixed" >&2 + return 1 + ;; + esac + + case "${pattern}" in + unique|duplicate) ;; + *) + echo "error: invalid PATTERN=${pattern}; expected unique|duplicate" >&2 + return 1 + ;; + esac + + case "${control}" in + gc|no-gc) ;; + *) + echo "error: invalid CONTROL=${control}; expected gc|no-gc" >&2 + return 1 + ;; + esac + + case "${count}" in + 1000|10000|100000) ;; + *) + echo "error: invalid COUNT=${count}; expected 1000|10000|100000" >&2 + return 1 + ;; + esac + + if [[ "${pattern}" == "duplicate" && "${kind}" != "entity" ]]; then + echo "error: duplicate pattern only supports CANDIDATE_KIND=entity (got ${kind})" >&2 + return 1 + fi + + if ! [[ "${samples}" =~ ^[0-9]+$ ]] || [[ "${samples}" -lt 1 ]] || [[ "${samples}" -gt 50 ]]; then + echo "error: invalid SAMPLES=${samples}; expected integer 1..50" >&2 + return 1 + fi + + return 0 +} diff --git a/examples/benchmark-native/src/BenchNative.ts b/examples/benchmark-native/src/BenchNative.ts new file mode 100644 index 000000000000..9923ec8471c9 --- /dev/null +++ b/examples/benchmark-native/src/BenchNative.ts @@ -0,0 +1,77 @@ +import { NativeModules, Platform } from 'react-native'; + +import type { UiCaptureSource } from './types'; + +export interface MemorySnapshot { + totalPssKb: number; + totalPrivateDirtyKb?: number; + rssKb?: number; +} + +export interface UiFrameCaptureResult { + source: UiCaptureSource; + frameCount: number; + maxFrameDurationMs: number; + totalFrameDurationMs: number; + missedFrames: number; + refreshPeriodMs: number; + refreshRateHz: number; + wasCapturing?: boolean; +} + +export interface NativeEnvironment { + apiLevel: number; + release: string; + manufacturer: string; + model: string; + device: string; + brand: string; + buildType: string; + applicationId: string; + hermesEnabled: boolean; + hermesRuntimeProperties?: Record; + refreshRateHz: number; + refreshPeriodMs: number; +} + +export interface NativeLaunchConfig { + autoRun: boolean; + candidateKind?: string; + pattern?: string; + count?: number; + control?: string; + samples?: number; + label?: string; +} + +interface BenchNativeNativeModule { + getLaunchConfig(): Promise; + getBuildManifest(): Promise<{ json: string }>; + getEnvironment(): Promise; + getMemorySnapshot(): Promise; + startUiFrameCapture(): Promise<{ started: boolean; source: string }>; + stopUiFrameCapture(): Promise; + writeReport(json: string): Promise<{ path: string }>; +} + +const LINKING_ERROR = `BenchNative native module is not linked. Rebuild the Android app.`; + +const BenchNative: BenchNativeNativeModule = + Platform.OS === 'android' && NativeModules.BenchNative != null + ? NativeModules.BenchNative + : new Proxy({} as BenchNativeNativeModule, { + get() { + throw new Error(LINKING_ERROR); + }, + }); + +export default BenchNative; + +/** Optional JS heap when Hermes/RN exposes performance.memory. */ +export function readJsHeapBytes(): number | undefined { + if (typeof performance === 'undefined') return undefined; + const mem = performance.memory; + if (mem == null) return undefined; + const used = mem.usedJSHeapSize; + return typeof used === 'number' && Number.isFinite(used) ? used : undefined; +} diff --git a/examples/benchmark-native/src/frames.ts b/examples/benchmark-native/src/frames.ts new file mode 100644 index 000000000000..c68527c5c0d7 --- /dev/null +++ b/examples/benchmark-native/src/frames.ts @@ -0,0 +1,167 @@ +/** + * JS frame / responsiveness helpers (rAF + timers; not pointer latency). + * + * Native UI missed-frame math differs by capture source: + * - FrameMetrics TOTAL_DURATION is a *duration* → ceil(duration/period) − 1 + * - Choreographer deltas are *intervals* → round(interval/period) − 1 (same as JS rAF) + */ +import type { UiCaptureSource } from './types'; + +export function median(values: number[]): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 + ? (sorted[mid - 1] + sorted[mid]) / 2 + : sorted[mid]; +} + +/** Adjacent differences of rAF timestamps (ms). */ +export function frameIntervalsFromTimestamps(timestampsMs: number[]): number[] { + const intervals: number[] = []; + for (let i = 1; i < timestampsMs.length; i++) { + intervals.push(timestampsMs[i] - timestampsMs[i - 1]); + } + return intervals; +} + +/** + * FrameMetrics TOTAL_DURATION → excess missed frames. + * Conservative: ceil so a duration just over one period counts as a miss. + */ +export function missedFramesFromDurationMs( + durationMs: number, + refreshPeriodMs: number, +): number { + if (!(refreshPeriodMs > 0) || !(durationMs > 0)) return 0; + return Math.max(0, Math.ceil(durationMs / refreshPeriodMs) - 1); +} + +/** + * Choreographer / JS rAF *interval* → excess missed frames (nearest-period). + */ +export function missedFramesFromIntervalMs( + intervalMs: number, + refreshPeriodMs: number, +): number { + if (!(refreshPeriodMs > 0) || !(intervalMs > 0)) return 0; + return Math.max(0, Math.round(intervalMs / refreshPeriodMs) - 1); +} + +/** Sum FrameMetrics-style duration misses. */ +export function excessMissedFramesFromDurations( + durationsMs: number[], + refreshPeriodMs: number, +): number { + let missed = 0; + for (const d of durationsMs) { + missed += missedFramesFromDurationMs(d, refreshPeriodMs); + } + return missed; +} + +/** + * Sum interval-style misses (JS rAF / Choreographer). + */ +export function excessMissedFramesFromIntervals( + intervalsMs: number[], + displayPeriodMs: number, +): number { + let missed = 0; + for (const interval of intervalsMs) { + missed += missedFramesFromIntervalMs(interval, displayPeriodMs); + } + return missed; +} + +export function missedFramesFromTimestamps( + timestampsMs: number[], + displayPeriodMs: number, +): number { + return excessMissedFramesFromIntervals( + frameIntervalsFromTimestamps(timestampsMs), + displayPeriodMs, + ); +} + +export function computeMaxInputDelayMs( + timerDelayMs: number, + frameIntervalsMs: number[], + displayPeriodMs: number, +): number { + let maxExcessFrame = 0; + if (displayPeriodMs > 0) { + for (const interval of frameIntervalsMs) { + maxExcessFrame = Math.max( + maxExcessFrame, + Math.max(0, interval - displayPeriodMs), + ); + } + } + return Math.max(timerDelayMs, maxExcessFrame); +} + +export async function measureDisplayPeriodMs(samples = 8): Promise { + const intervals: number[] = []; + await new Promise(resolve => { + let last = 0; + let n = 0; + const frame = (now: number) => { + if (n > 0) intervals.push(now - last); + last = now; + n++; + if (n <= samples) requestAnimationFrame(frame); + else resolve(); + }; + requestAnimationFrame(frame); + }); + return median(intervals); +} + +export interface UiFrameCaptureAggregate { + source: UiCaptureSource; + frameCount: number; + maxFrameDurationMs: number; + totalFrameDurationMs: number; + missedFrames: number; + refreshPeriodMs: number; + refreshRateHz: number; +} + +/** + * Reject invalid refresh period or zero/insufficient native frames. + * Missed-frame math must match capture source semantics. + */ +export function validateUiFrameCapture( + result: UiFrameCaptureAggregate, + options: { minFrames?: number } = {}, +): void { + const minFrames = options.minFrames ?? 1; + if ( + !(result.refreshPeriodMs > 0) || + !Number.isFinite(result.refreshPeriodMs) + ) { + throw new Error( + `invalid ui refreshPeriodMs=${String(result.refreshPeriodMs)}`, + ); + } + if (!(result.refreshRateHz > 0) || !Number.isFinite(result.refreshRateHz)) { + throw new Error(`invalid ui refreshRateHz=${String(result.refreshRateHz)}`); + } + if (!Number.isInteger(result.frameCount) || result.frameCount < minFrames) { + throw new Error( + `insufficient ui frames: frameCount=${result.frameCount} (need >= ${minFrames}); source=${result.source}`, + ); + } + if ( + !Number.isFinite(result.maxFrameDurationMs) || + result.maxFrameDurationMs < 0 || + !Number.isFinite(result.totalFrameDurationMs) || + result.totalFrameDurationMs < 0 + ) { + throw new Error('invalid ui frame duration aggregates'); + } + if (result.source !== 'FrameMetrics' && result.source !== 'Choreographer') { + throw new Error(`unknown ui capture source: ${String(result.source)}`); + } +} diff --git a/examples/benchmark-native/src/gcHarness.ts b/examples/benchmark-native/src/gcHarness.ts new file mode 100644 index 000000000000..4ddae735c469 --- /dev/null +++ b/examples/benchmark-native/src/gcHarness.ts @@ -0,0 +1,382 @@ +/** + * Isolated Android GC harness — independent of DataProvider. + * + * Builds a benchmark-only Controller + createReducer + timerless GCPolicy with + * raw deterministic state, queues via createCountRef, mode=end-to-end. + * Setup/queue outside timing. Same axes as Node/browser. + */ +import { + Controller, + GCPolicy, + actionTypes, + createReducer, + initialState, +} from '@data-client/core'; +import type { State } from '@data-client/core'; + +import { splitMixedCount } from './scenario'; +import type { GCAndroidMeasurement, GCScenarioConfig } from './types'; + +const { GC } = actionTypes; + +type EntityPath = { key: string; pk: string }; + +export const ENTITY_KEY = 'BenchEntity'; +export const ZERO_META = Object.freeze({ + date: 0, + fetchedAt: 0, + expiresAt: 0, +}); + +/** Timerless explicit policy: no intervals; public sweep → protected runSweep. */ +class AndroidBenchmarkGCPolicy extends GCPolicy { + constructor() { + super({ expiresAt: () => 0 }); + } + + init(controller: Controller) { + this.controller = controller; + } + + cleanup(): void { + // Timerless policy — nothing to clear. + } + + sweep() { + this.runSweep(); + } + + get entityQueueLength() { + return this.entitiesQ.length; + } + + get endpointQueueSize() { + return this.endpointsQ.size; + } + + get queueEntries() { + return this.entityQueueLength + this.endpointQueueSize; + } +} + +export interface ExpectedScalars { + queueEntries: number; + uniqueTargets: number; + expectedEntitiesInAction: number; + expectedEndpointsInAction: number; + expectedUniqueEntityDeletions: number; + expectedEndpointDeletions: number; +} + +export interface Harness { + policy: AndroidBenchmarkGCPolicy; + expected: ExpectedScalars; + getState: () => State; + getCapturedAction: () => { + type: string; + entities: EntityPath[]; + endpoints: string[]; + } | null; + clearCapturedAction: () => void; + dispose: () => void; +} + +function entityPath(pk: number): EntityPath { + return { key: ENTITY_KEY, pk: String(pk) }; +} + +function endpointKey(i: number) { + return `bench-endpoint-${i}`; +} + +function buildEntityState(count: number): State { + const entities = { [ENTITY_KEY]: {} as Record }; + const entitiesMeta = { + [ENTITY_KEY]: {} as Record, + }; + for (let i = 0; i < count; i++) { + const pk = String(i); + entities[ENTITY_KEY][pk] = { id: pk }; + entitiesMeta[ENTITY_KEY][pk] = { ...ZERO_META }; + } + return { ...initialState, entities, entitiesMeta }; +} + +function buildEndpointState(count: number): State { + const endpoints: Record = {}; + const meta: Record = {}; + for (let i = 0; i < count; i++) { + const key = endpointKey(i); + endpoints[key] = String(i); + meta[key] = { ...ZERO_META }; + } + return { ...initialState, endpoints, meta }; +} + +function buildMixedState( + entityCount: number, + endpointCount: number, +): State { + const entities = buildEntityState(entityCount); + const endpoints = buildEndpointState(endpointCount); + return { + ...entities, + endpoints: endpoints.endpoints, + meta: endpoints.meta, + }; +} + +function buildStateForSpec(config: GCScenarioConfig): State { + const { candidateKind, pattern, count } = config; + if (pattern === 'duplicate') return buildEntityState(1); + if (candidateKind === 'entity') return buildEntityState(count); + if (candidateKind === 'endpoint') return buildEndpointState(count); + if (candidateKind === 'mixed') { + const { entities, endpoints } = splitMixedCount(count); + return buildMixedState(entities, endpoints); + } + throw new Error(`unknown candidateKind: ${candidateKind}`); +} + +export function queueCandidates( + policy: AndroidBenchmarkGCPolicy, + config: GCScenarioConfig, +): ExpectedScalars { + const { candidateKind, pattern, count } = config; + + if (pattern === 'duplicate') { + if (candidateKind !== 'entity') { + throw new Error( + `duplicate pattern only supports candidateKind=entity (got ${candidateKind})`, + ); + } + const path = entityPath(0); + const countRef = policy.createCountRef({ paths: [path] }); + for (let i = 0; i < count; i++) { + const release = countRef(); + release(); + } + return { + queueEntries: count, + uniqueTargets: 1, + expectedEntitiesInAction: count, + expectedEndpointsInAction: 0, + expectedUniqueEntityDeletions: 1, + expectedEndpointDeletions: 0, + }; + } + + if (candidateKind === 'entity') { + for (let i = 0; i < count; i++) { + const release = policy.createCountRef({ paths: [entityPath(i)] })(); + release(); + } + return { + queueEntries: count, + uniqueTargets: count, + expectedEntitiesInAction: count, + expectedEndpointsInAction: 0, + expectedUniqueEntityDeletions: count, + expectedEndpointDeletions: 0, + }; + } + + if (candidateKind === 'endpoint') { + for (let i = 0; i < count; i++) { + const release = policy.createCountRef({ key: endpointKey(i) })(); + release(); + } + return { + queueEntries: count, + uniqueTargets: count, + expectedEntitiesInAction: 0, + expectedEndpointsInAction: count, + expectedUniqueEntityDeletions: 0, + expectedEndpointDeletions: count, + }; + } + + if (candidateKind === 'mixed') { + const { entities, endpoints } = splitMixedCount(count); + for (let i = 0; i < entities; i++) { + const release = policy.createCountRef({ paths: [entityPath(i)] })(); + release(); + } + for (let i = 0; i < endpoints; i++) { + const release = policy.createCountRef({ key: endpointKey(i) })(); + release(); + } + return { + queueEntries: count, + uniqueTargets: count, + expectedEntitiesInAction: entities, + expectedEndpointsInAction: endpoints, + expectedUniqueEntityDeletions: entities, + expectedEndpointDeletions: endpoints, + }; + } + + throw new Error(`unknown candidateKind: ${candidateKind}`); +} + +/** End-to-end harness: sweep dispatches and reduces synchronously. */ +export function createHarness(config: GCScenarioConfig): Harness { + const state = buildStateForSpec(config); + const policy = new AndroidBenchmarkGCPolicy(); + const controller = new Controller({ gcPolicy: policy }); + const reducer = createReducer(controller); + + let capturedAction: { + type: string; + entities: EntityPath[]; + endpoints: string[]; + } | null = null; + let workingState: State = state; + + controller.getState = () => workingState; + controller.dispatch = ((action: any) => { + capturedAction = action; + workingState = reducer(workingState, action); + }) as typeof controller.dispatch; + + policy.init(controller); + const expected = queueCandidates(policy, config); + + if (policy.queueEntries !== expected.queueEntries) { + throw new Error( + `fixture queue cardinality ${policy.queueEntries} !== expected ${expected.queueEntries}`, + ); + } + + return { + policy, + expected, + getState: () => workingState, + getCapturedAction: () => capturedAction, + clearCapturedAction() { + capturedAction = null; + }, + dispose() { + capturedAction = null; + controller.getState = () => initialState; + controller.dispatch = (() => + Promise.resolve()) as typeof controller.dispatch; + policy.cleanup(); + }, + }; +} + +export function countRemaining( + state: State, + config: GCScenarioConfig, + expected: ExpectedScalars, +) { + const bucket = state.entities?.[ENTITY_KEY]; + const entityRemaining = bucket ? Object.keys(bucket).length : 0; + const endpointRemaining = state.endpoints + ? Object.keys(state.endpoints).length + : 0; + + const startedEntities = + config.pattern === 'duplicate' ? 1 : expected.expectedUniqueEntityDeletions; + const startedEndpoints = expected.expectedEndpointDeletions; + + return { + entityDeleted: startedEntities - entityRemaining, + endpointDeleted: startedEndpoints - endpointRemaining, + entityRemaining, + endpointRemaining, + }; +} + +export function validateMeasurement( + config: GCScenarioConfig, + harness: Harness, + sample: Pick< + GCAndroidMeasurement, + | 'actionCount' + | 'deletionCount' + | 'queueEntries' + | 'uniqueTargets' + | 'actionTargetCount' + >, +) { + const { expected, policy, getCapturedAction, getState } = harness; + + if (config.control === 'no-gc') { + if (sample.actionCount !== 0) { + throw new Error( + `no-gc control expected actionCount 0, got ${sample.actionCount}`, + ); + } + if (sample.deletionCount !== 0) { + throw new Error( + `no-gc control expected deletionCount 0, got ${sample.deletionCount}`, + ); + } + return; + } + + if (sample.queueEntries !== expected.queueEntries) { + throw new Error( + `queueEntries ${sample.queueEntries} !== expected ${expected.queueEntries}`, + ); + } + if (sample.uniqueTargets !== expected.uniqueTargets) { + throw new Error( + `uniqueTargets ${sample.uniqueTargets} !== expected ${expected.uniqueTargets}`, + ); + } + + const actionTargetCount = + expected.expectedEntitiesInAction + expected.expectedEndpointsInAction; + if (sample.actionTargetCount !== actionTargetCount) { + throw new Error( + `actionTargetCount ${sample.actionTargetCount} !== expected ${actionTargetCount}`, + ); + } + + const action = getCapturedAction(); + if (!action || action.type !== GC) { + throw new Error('expected a GC action to be dispatched'); + } + if (action.entities.length !== expected.expectedEntitiesInAction) { + throw new Error( + `action.entities.length ${action.entities.length} !== ${expected.expectedEntitiesInAction}`, + ); + } + if (action.endpoints.length !== expected.expectedEndpointsInAction) { + throw new Error( + `action.endpoints.length ${action.endpoints.length} !== ${expected.expectedEndpointsInAction}`, + ); + } + if (config.pattern === 'duplicate') { + const unique = new Set(action.entities.map(p => `${p.key}:${p.pk}`)); + if (unique.size !== 1) { + throw new Error( + `duplicate pattern expected 1 unique entity path in action, got ${unique.size}`, + ); + } + } + + const expectedDeletions = + expected.expectedUniqueEntityDeletions + expected.expectedEndpointDeletions; + if (sample.deletionCount !== expectedDeletions) { + throw new Error( + `deletionCount ${sample.deletionCount} !== expected ${expectedDeletions}`, + ); + } + const remaining = countRemaining(getState(), config, expected); + if (remaining.entityRemaining !== 0 || remaining.endpointRemaining !== 0) { + throw new Error( + `expected empty GC targets after deletion; remaining entities=${remaining.entityRemaining} endpoints=${remaining.endpointRemaining}`, + ); + } + if (policy.queueEntries !== 0) { + throw new Error( + `expected empty queues after sweep, got ${policy.queueEntries}`, + ); + } +} + +export { GC }; diff --git a/examples/benchmark-native/src/globals.d.ts b/examples/benchmark-native/src/globals.d.ts new file mode 100644 index 000000000000..98863b0ee40e --- /dev/null +++ b/examples/benchmark-native/src/globals.d.ts @@ -0,0 +1,17 @@ +/** React Native / Hermes globals used by the benchmark harness. */ +interface PerformanceMemory { + usedJSHeapSize?: number; +} + +interface BenchPerformance { + now(): number; + memory?: PerformanceMemory; +} + +declare const performance: BenchPerformance; + +declare namespace NodeJS { + interface Global { + performance: BenchPerformance; + } +} diff --git a/examples/benchmark-native/src/measure.ts b/examples/benchmark-native/src/measure.ts new file mode 100644 index 000000000000..f0d37848e933 --- /dev/null +++ b/examples/benchmark-native/src/measure.ts @@ -0,0 +1,226 @@ +/** + * Interaction measurement: quiet display period (prepare), UI frame capture + + * sustained JS animation, explicit cache GC inside an rAF, post frames, timer + * probe. Aggregate only — no per-candidate logs or bridge calls per frame. + * + * Engine GC must not run inside interaction timing. + * Captured GC action is released before after-memory; live store retained until dispose. + * UI frame capture / JS animation always torn down in finally (including rAF failures). + */ +import BenchNative, { readJsHeapBytes } from './BenchNative'; +import type { MemorySnapshot, UiFrameCaptureResult } from './BenchNative'; +import { + computeMaxInputDelayMs, + excessMissedFramesFromIntervals, + frameIntervalsFromTimestamps, + measureDisplayPeriodMs, + validateUiFrameCapture, +} from './frames'; +import { + countRemaining, + createHarness, + validateMeasurement, + GC, + type Harness, +} from './gcHarness'; +import { runRafCollectSequence } from './rafCollectSequence'; +import type { + GCAndroidMeasurement, + GCPreparedSummary, + GCScenarioConfig, +} from './types'; + +interface Session { + config: GCScenarioConfig; + harness: Harness; + displayPeriodMs: number; +} + +let session: Session | null = null; +let animationHandle: number | null = null; +let onAnimationTick: ((t: number) => void) | null = null; + +export function setAnimationTickListener( + listener: ((t: number) => void) | null, +) { + onAnimationTick = listener; +} + +function startSustainedAnimation() { + stopSustainedAnimation(); + const step = () => { + onAnimationTick?.(performance.now()); + animationHandle = requestAnimationFrame(step); + }; + animationHandle = requestAnimationFrame(step); +} + +function stopSustainedAnimation() { + if (animationHandle != null) { + cancelAnimationFrame(animationHandle); + animationHandle = null; + } +} + +async function stopUiCaptureSafe(): Promise { + try { + return await BenchNative.stopUiFrameCapture(); + } catch { + return null; + } +} + +export async function prepareGCScenario( + config: GCScenarioConfig, +): Promise { + if (session) { + session.harness.dispose(); + session = null; + } + + const harness = createHarness(config); + const displayPeriodMs = await measureDisplayPeriodMs(); + + session = { config, harness, displayPeriodMs }; + + return { + queueEntries: harness.expected.queueEntries, + uniqueTargets: harness.expected.uniqueTargets, + }; +} + +export async function runGCScenario(): Promise { + if (!session) { + throw new Error( + 'prepareGCScenario() must be called before runGCScenario()', + ); + } + const { config, harness, displayPeriodMs } = session; + const { policy, expected } = harness; + + harness.clearCapturedAction(); + + const memBefore: MemorySnapshot = await BenchNative.getMemorySnapshot(); + const jsHeapBeforeBytes = readJsHeapBytes(); + + let uiFrames: UiFrameCaptureResult | null = null; + + try { + await BenchNative.startUiFrameCapture(); + startSustainedAnimation(); + + const sequence = await runRafCollectSequence({ + collectWork: () => { + const t0 = performance.now(); + let actionCount = 0; + if (config.control === 'gc') { + policy.sweep(); + const action = harness.getCapturedAction(); + actionCount = action && action.type === GC ? 1 : 0; + } + return { + totalMs: performance.now() - t0, + actionCount, + }; + }, + }); + + // Deletion accounting outside timed collect work (matches Node/browser) + let deletionCount = 0; + if (config.control === 'gc') { + const remaining = countRemaining(harness.getState(), config, expected); + deletionCount = remaining.entityDeleted + remaining.endpointDeleted; + } + + uiFrames = await BenchNative.stopUiFrameCapture(); + validateUiFrameCapture(uiFrames); + + const frameIntervalsMs = frameIntervalsFromTimestamps( + sequence.frameTimestamps, + ); + + const actionTargetCount = + config.control === 'gc' + ? expected.expectedEntitiesInAction + expected.expectedEndpointsInAction + : 0; + + const missedFrames = excessMissedFramesFromIntervals( + frameIntervalsMs, + displayPeriodMs, + ); + const maxInputDelayMs = computeMaxInputDelayMs( + sequence.timerDelayMs, + frameIntervalsMs, + displayPeriodMs, + ); + + const measurement: GCAndroidMeasurement = { + schemaVersion: 1, + totalMs: sequence.totalMs, + sliceDurationsMs: config.control === 'gc' ? [sequence.totalMs] : [], + actionCount: sequence.actionCount, + queueEntries: expected.queueEntries, + uniqueTargets: expected.uniqueTargets, + actionTargetCount, + deletionCount, + timerDelayMs: sequence.timerDelayMs, + frameIntervalsMs, + displayPeriodMs, + missedFrames, + maxInputDelayMs, + uiCaptureSource: uiFrames.source, + uiFrameCount: uiFrames.frameCount, + uiMaxFrameDurationMs: uiFrames.maxFrameDurationMs, + uiTotalFrameDurationMs: uiFrames.totalFrameDurationMs, + uiMissedFrames: uiFrames.missedFrames, + uiRefreshPeriodMs: uiFrames.refreshPeriodMs, + uiRefreshRateHz: uiFrames.refreshRateHz, + }; + + validateMeasurement(config, harness, measurement); + + // Drop captured GC action arrays before heap snapshot; keep the live store. + harness.clearCapturedAction(); + + const memAfter: MemorySnapshot = await BenchNative.getMemorySnapshot(); + const jsHeapAfterBytes = readJsHeapBytes(); + + measurement.processPssBeforeKb = memBefore.totalPssKb; + measurement.processPssAfterKb = memAfter.totalPssKb; + measurement.processPssDeltaKb = memAfter.totalPssKb - memBefore.totalPssKb; + if (memBefore.rssKb != null) { + measurement.processRssBeforeKb = memBefore.rssKb; + } + if (memAfter.rssKb != null) { + measurement.processRssAfterKb = memAfter.rssKb; + } + if (memBefore.rssKb != null && memAfter.rssKb != null) { + measurement.processRssDeltaKb = memAfter.rssKb - memBefore.rssKb; + } + if (jsHeapBeforeBytes != null) { + measurement.jsHeapBeforeBytes = jsHeapBeforeBytes; + } + if (jsHeapAfterBytes != null) { + measurement.jsHeapAfterBytes = jsHeapAfterBytes; + } + if (jsHeapBeforeBytes != null && jsHeapAfterBytes != null) { + measurement.jsHeapDeltaBytes = jsHeapAfterBytes - jsHeapBeforeBytes; + } + + return measurement; + } finally { + // Always stop animation; stop native capture only if the success path + // did not already take the snapshot (collectWork may reject mid-sequence). + stopSustainedAnimation(); + if (uiFrames == null) { + await stopUiCaptureSafe(); + } + } +} + +export function disposeGCScenario(): void { + stopSustainedAnimation(); + if (!session) return; + session.harness.dispose(); + session = null; +} diff --git a/examples/benchmark-native/src/rafCollectSequence.ts b/examples/benchmark-native/src/rafCollectSequence.ts new file mode 100644 index 000000000000..d2841b74e770 --- /dev/null +++ b/examples/benchmark-native/src/rafCollectSequence.ts @@ -0,0 +1,169 @@ +/** + * Timed collect sequence inside rAF callbacks with fail-closed termination. + * + * If collect work (e.g. policy.sweep) throws, the Promise rejects once and any + * already-queued frame callbacks no-op so the outer finally can tear down + * animation / native capture. + */ + +export type FrameScheduler = (cb: (nowMs: number) => void) => void; + +export interface CollectWorkResult { + totalMs: number; + actionCount: number; +} + +export interface RafCollectSequenceResult { + frameTimestamps: number[]; + timerDelayMs: number; + totalMs: number; + actionCount: number; +} + +export interface RafCollectSequenceOptions { + /** Schedule a frame callback (defaults to requestAnimationFrame). */ + scheduleFrame?: FrameScheduler; + /** performance.now (or test double). */ + now?: () => number; + /** setTimeout(0) probe (or test double). */ + scheduleTimeout0?: (cb: () => void) => void; + postFrames?: number; + /** + * Synchronous collect work invoked inside the collect frame *after* the next + * rAF is queued. May throw — that rejects the sequence promptly. + */ + collectWork: () => CollectWorkResult; + /** Invoked exactly once when the sequence terminates due to error. */ + onTerminateError?: (error: unknown) => void; +} + +/** + * Manual scheduler for deterministic tests: callbacks are queued and flushed + * with synthetic timestamps. + */ +export function createManualFrameScheduler(periodMs = 16.67): { + scheduleFrame: FrameScheduler; + flush: (count?: number) => void; + pending: () => number; +} { + const queue: Array<(nowMs: number) => void> = []; + let nowMs = 0; + return { + scheduleFrame(cb) { + queue.push(cb); + }, + flush(count = 1) { + for (let i = 0; i < count; i++) { + const cb = queue.shift(); + if (!cb) return; + nowMs += periodMs; + cb(nowMs); + } + }, + pending() { + return queue.length; + }, + }; +} + +function defaultScheduleFrame(cb: (nowMs: number) => void) { + requestAnimationFrame(cb); +} + +function defaultTimeout0(cb: () => void) { + setTimeout(cb, 0); +} + +/** + * Run pre → collect → post rAF sequence. Queues the post frame *before* + * collectWork so stalls are visible; wraps work in try/catch with reject-once. + */ +export function runRafCollectSequence( + options: RafCollectSequenceOptions, +): Promise { + const scheduleFrame = options.scheduleFrame ?? defaultScheduleFrame; + const now = options.now ?? (() => performance.now()); + const scheduleTimeout0 = options.scheduleTimeout0 ?? defaultTimeout0; + const POST_FRAMES = options.postFrames ?? 6; + + return new Promise((resolve, reject) => { + /** Once true, no callback may mutate probe state or settle again. */ + let closed = false; + let phase: 'pre' | 'collect' | 'post' = 'pre'; + let postRemaining = POST_FRAMES; + const frameTimestamps: number[] = []; + let timerDelayMs = 0; + let totalMs = 0; + let actionCount = 0; + + const settleReject = (error: unknown) => { + if (closed) return; + closed = true; + try { + options.onTerminateError?.(error); + } catch { + // ignore cleanup listener failures + } + reject(error); + }; + + const settleResolve = () => { + if (closed) return; + closed = true; + resolve({ + frameTimestamps, + timerDelayMs, + totalMs, + actionCount, + }); + }; + + const frame = (frameNow: number) => { + if (closed) return; + + try { + frameTimestamps.push(frameNow); + + if (phase === 'pre') { + phase = 'collect'; + scheduleFrame(frame); + return; + } + + if (phase === 'collect') { + // Queue next rAF BEFORE collect work so a stall is observable. + phase = 'post'; + postRemaining = POST_FRAMES; + scheduleFrame(frame); + + const beforeCollection = now(); + scheduleTimeout0(() => { + if (closed) return; + timerDelayMs = now() - beforeCollection; + }); + + const result = options.collectWork(); + totalMs = result.totalMs; + actionCount = result.actionCount; + return; + } + + // post + postRemaining--; + if (postRemaining > 0) { + scheduleFrame(frame); + return; + } + + scheduleTimeout0(() => { + if (closed) return; + settleResolve(); + }); + } catch (error) { + settleReject(error); + } + }; + + scheduleFrame(frame); + }); +} diff --git a/examples/benchmark-native/src/report.ts b/examples/benchmark-native/src/report.ts new file mode 100644 index 000000000000..8fde0d627113 --- /dev/null +++ b/examples/benchmark-native/src/report.ts @@ -0,0 +1,246 @@ +import { scenarioId } from './scenario'; +import type { + AndroidEnvironment, + BuildManifestV1, + BuildSidecarV1, + GCAndroidMeasurement, + GCMeasurementReport, + GCScenarioConfig, + GCScenarioReport, + NumberSummary, +} from './types'; + +/** Caller must pass a non-empty sorted array. */ +function percentile(sorted: number[], p: number): number { + if (sorted.length === 1) return sorted[0]!; + const idx = (p / 100) * (sorted.length - 1); + const lo = Math.floor(idx); + const hi = Math.ceil(idx); + if (lo === hi) return sorted[lo]!; + const w = idx - lo; + return sorted[lo]! * (1 - w) + sorted[hi]! * w; +} + +export function summarizeNumbers(values: number[]): NumberSummary | null { + if (values.length === 0) return null; + const sorted = values.slice().sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + const median = + sorted.length % 2 === 0 + ? (sorted[mid - 1]! + sorted[mid]!) / 2 + : sorted[mid]!; + return { + median, + min: sorted[0]!, + max: sorted[sorted.length - 1]!, + p95: percentile(sorted, 95), + p99: percentile(sorted, 99), + }; +} + +function optionalField( + samples: GCAndroidMeasurement[], + key: keyof GCAndroidMeasurement, +): number[] { + return samples + .map(s => s[key]) + .filter((v): v is number => typeof v === 'number' && Number.isFinite(v)); +} + +function putOptionalSummary( + summary: Record, + key: string, + values: number[], +) { + if (values.length) { + summary[key] = summarizeNumbers(values); + } +} + +export function summarizeGCSamples( + samples: GCAndroidMeasurement[], +): Record { + const summary: Record = { + totalMs: summarizeNumbers(samples.map(s => s.totalMs)), + actionCount: summarizeNumbers(samples.map(s => s.actionCount)), + queueEntries: summarizeNumbers(samples.map(s => s.queueEntries)), + uniqueTargets: summarizeNumbers(samples.map(s => s.uniqueTargets)), + actionTargetCount: summarizeNumbers(samples.map(s => s.actionTargetCount)), + deletionCount: summarizeNumbers(samples.map(s => s.deletionCount)), + timerDelayMs: summarizeNumbers(samples.map(s => s.timerDelayMs)), + displayPeriodMs: summarizeNumbers(samples.map(s => s.displayPeriodMs)), + missedFrames: summarizeNumbers(samples.map(s => s.missedFrames)), + maxInputDelayMs: summarizeNumbers(samples.map(s => s.maxInputDelayMs)), + uiFrameCount: summarizeNumbers(samples.map(s => s.uiFrameCount)), + uiMaxFrameDurationMs: summarizeNumbers( + samples.map(s => s.uiMaxFrameDurationMs), + ), + uiTotalFrameDurationMs: summarizeNumbers( + samples.map(s => s.uiTotalFrameDurationMs), + ), + uiMissedFrames: summarizeNumbers(samples.map(s => s.uiMissedFrames)), + uiRefreshPeriodMs: summarizeNumbers(samples.map(s => s.uiRefreshPeriodMs)), + }; + + const slices = samples.flatMap(s => s.sliceDurationsMs ?? []); + if (slices.length) { + summary.sliceDurationsMs = summarizeNumbers(slices); + } + const frameIntervals = samples.flatMap(s => s.frameIntervalsMs ?? []); + if (frameIntervals.length) { + summary.frameIntervalsMs = summarizeNumbers(frameIntervals); + } + + putOptionalSummary( + summary, + 'processPssBeforeKb', + optionalField(samples, 'processPssBeforeKb'), + ); + putOptionalSummary( + summary, + 'processPssAfterKb', + optionalField(samples, 'processPssAfterKb'), + ); + putOptionalSummary( + summary, + 'processPssDeltaKb', + optionalField(samples, 'processPssDeltaKb'), + ); + putOptionalSummary( + summary, + 'processRssBeforeKb', + optionalField(samples, 'processRssBeforeKb'), + ); + putOptionalSummary( + summary, + 'processRssAfterKb', + optionalField(samples, 'processRssAfterKb'), + ); + putOptionalSummary( + summary, + 'processRssDeltaKb', + optionalField(samples, 'processRssDeltaKb'), + ); + putOptionalSummary( + summary, + 'jsHeapBeforeBytes', + optionalField(samples, 'jsHeapBeforeBytes'), + ); + putOptionalSummary( + summary, + 'jsHeapAfterBytes', + optionalField(samples, 'jsHeapAfterBytes'), + ); + putOptionalSummary( + summary, + 'jsHeapDeltaBytes', + optionalField(samples, 'jsHeapDeltaBytes'), + ); + + return summary; +} + +export const REPORT_UNITS: Record = { + totalMs: 'milliseconds', + sliceDurationsMs: 'milliseconds', + timerDelayMs: 'milliseconds', + frameIntervalsMs: 'milliseconds', + displayPeriodMs: 'milliseconds', + maxInputDelayMs: 'milliseconds (proxy; not pointer latency)', + missedFrames: 'count (JS rAF interval nearest-period excess)', + uiCaptureSource: + 'FrameMetrics (duration/ceil) | Choreographer (interval/round)', + uiFrameCount: 'count', + uiMaxFrameDurationMs: + 'milliseconds (FrameMetrics duration or Choreographer interval)', + uiTotalFrameDurationMs: + 'milliseconds (sum of FrameMetrics durations or Choreographer intervals)', + uiMissedFrames: + 'count (FrameMetrics: ceil(duration/period)−1; Choreographer: round(interval/period)−1)', + uiRefreshPeriodMs: 'milliseconds', + uiRefreshRateHz: 'hertz', + processPssBeforeKb: 'kilobytes (Android Debug.MemoryInfo totalPss)', + processPssAfterKb: 'kilobytes (Android Debug.MemoryInfo totalPss)', + processPssDeltaKb: 'kilobytes (after − before)', + processRssBeforeKb: 'kilobytes (when API exposes RSS)', + processRssAfterKb: 'kilobytes (when API exposes RSS)', + processRssDeltaKb: 'kilobytes (after − before; when RSS available)', + jsHeapBeforeBytes: 'bytes (performance.memory.usedJSHeapSize when exposed)', + jsHeapAfterBytes: 'bytes (performance.memory.usedJSHeapSize when exposed)', + jsHeapDeltaBytes: 'bytes (after − before; when JS heap available)', + actionCount: 'count', + queueEntries: 'count', + uniqueTargets: 'count', + actionTargetCount: 'count', + deletionCount: 'count', +}; + +export const MEMORY_SEMANTICS_DESCRIPTION = + 'After interaction timing, release captured GC action arrays, keep the live store until dispose, then snapshot process/JS memory. System.gc() is not used — it does not force Hermes collection. Engine GC must not run inside interaction timing. Without a forced Hermes GC, before/after JS heap and process PSS/RSS are observational and noisy: they are not sufficient alone for a memory gate. Compare repeated gc vs no-gc controls on the same device/build series; treat deltas as supporting evidence beside interaction/frame metrics.'; + +export function buildScenarioReport( + config: GCScenarioConfig, + samples: GCAndroidMeasurement[], +): GCScenarioReport { + return { + id: scenarioId(config), + platform: 'android', + candidateKind: config.candidateKind, + pattern: config.pattern, + count: config.count, + mode: 'interaction', + control: config.control, + samples, + summary: summarizeGCSamples(samples), + }; +} + +export function buildMeasurementReport(args: { + environment: AndroidEnvironment; + config: GCScenarioConfig; + samples: GCAndroidMeasurement[]; + manifest: BuildManifestV1; + label?: string; + apkSizeBytes?: number; + hermesBytecodeBytes?: number; + hermesAssetsBytes?: number; + sidecar?: BuildSidecarV1; + installedApkSha256?: string; +}): GCMeasurementReport { + const scenario = buildScenarioReport(args.config, args.samples); + return { + schemaVersion: 1, + units: { ...REPORT_UNITS }, + memorySemantics: { + model: 'keep-store-drop-observer', + description: MEMORY_SEMANTICS_DESCRIPTION, + }, + build: { + buildId: args.manifest.buildId, + sourceDigest: args.manifest.sourceDigest, + gitCommit: args.manifest.gitCommit, + gitDirty: args.manifest.gitDirty, + label: args.label, + apkSizeBytes: args.apkSizeBytes, + hermesBytecodeBytes: args.hermesBytecodeBytes, + hermesAssetsBytes: args.hermesAssetsBytes, + sidecar: args.sidecar + ? { + buildId: args.sidecar.buildId, + sourceDigest: args.sidecar.sourceDigest, + apkSha256: args.sidecar.apkSha256, + apkPath: args.sidecar.apkPath, + sidecarId: args.sidecar.sidecarId, + } + : undefined, + installedApkSha256: args.installedApkSha256, + }, + environment: args.environment, + config: { + samplesPerScenario: args.samples.length, + filter: null, + scenarioId: scenario.id, + }, + scenarios: [scenario], + }; +} diff --git a/examples/benchmark-native/src/runOrchestration.ts b/examples/benchmark-native/src/runOrchestration.ts new file mode 100644 index 000000000000..3f28490f0521 --- /dev/null +++ b/examples/benchmark-native/src/runOrchestration.ts @@ -0,0 +1,94 @@ +/** + * Shared measurement orchestration for manual Run and intent auto-run. + */ +import BenchNative from './BenchNative'; +import { disposeGCScenario, prepareGCScenario, runGCScenario } from './measure'; +import { buildMeasurementReport } from './report'; +import { scenarioId } from './scenario'; +import type { + AndroidEnvironment, + BuildManifestV1, + GCAndroidMeasurement, + GCMeasurementReport, + GCScenarioConfig, +} from './types'; +import { validateSampleCount, validateScenarioConfig } from './validateConfig'; + +export interface ExecuteMeasurementArgs { + config: GCScenarioConfig; + samples: number; + label?: string; + onStatus?: (message: string) => void; +} + +export interface ExecuteMeasurementResult { + report: GCMeasurementReport; + path: string; + scenarioId: string; + samples: GCAndroidMeasurement[]; +} + +async function readEnvironment(): Promise { + const envRaw = await BenchNative.getEnvironment(); + return { platform: 'android', ...envRaw }; +} + +export async function readEmbeddedBuildManifest(): Promise { + const raw = await BenchNative.getBuildManifest(); + const text = raw.json; + if (!text) { + throw new Error('embedded build-manifest.json empty'); + } + const parsed = JSON.parse(text) as BuildManifestV1; + if (parsed.schemaVersion !== 1 || !parsed.buildId || !parsed.sourceDigest) { + throw new Error('invalid embedded BuildManifest v1'); + } + return parsed; +} + +export async function executeMeasurement( + args: ExecuteMeasurementArgs, +): Promise { + validateScenarioConfig(args.config); + validateSampleCount(args.samples); + + const id = scenarioId(args.config); + args.onStatus?.(`Running ${id}…`); + + const environment = await readEnvironment(); + const manifest = await readEmbeddedBuildManifest(); + const collected: GCAndroidMeasurement[] = []; + + try { + for (let i = 0; i < args.samples; i++) { + args.onStatus?.(`Sample ${i + 1}/${args.samples}: prepare…`); + await prepareGCScenario(args.config); + // Let the React status commit paint before capture so it does not + // contaminate UI frame / interaction measurement (outside timing). + args.onStatus?.(`Sample ${i + 1}/${args.samples}: measure…`); + await new Promise(resolve => { + requestAnimationFrame(() => resolve()); + }); + collected.push(await runGCScenario()); + disposeGCScenario(); + } + + const report = buildMeasurementReport({ + environment, + config: args.config, + samples: collected, + manifest, + label: args.label, + }); + + const { path } = await BenchNative.writeReport(JSON.stringify(report)); + args.onStatus?.( + `Done — wrote ${path} (buildId=${manifest.buildId.slice(0, 8)}… totalMs median≈${report.scenarios[0].summary.totalMs?.median?.toFixed?.(2) ?? 'n/a'})`, + ); + + return { report, path, scenarioId: id, samples: collected }; + } catch (e) { + disposeGCScenario(); + throw e; + } +} diff --git a/examples/benchmark-native/src/scenario.ts b/examples/benchmark-native/src/scenario.ts new file mode 100644 index 000000000000..7546e440c254 --- /dev/null +++ b/examples/benchmark-native/src/scenario.ts @@ -0,0 +1,106 @@ +import type { + CandidateKind, + Control, + GCScenarioConfig, + Pattern, +} from './types'; +import { CANONICAL_COUNTS } from './types'; +import { validateScenarioConfig } from './validateConfig'; + +export interface ScenarioAxes { + platform: 'android'; + candidateKind: CandidateKind; + pattern: Pattern; + count: number; + mode: 'interaction'; + control: Control; +} + +/** Stable scenario ID: `android/{kind}/{pattern}/{count}/interaction/{control}`. */ +export function scenarioId(axes: ScenarioAxes | GCScenarioConfig): string { + return [ + 'android', + axes.candidateKind, + axes.pattern, + String(axes.count), + 'interaction', + axes.control, + ].join('/'); +} + +/** + * Split mixed total into entity + endpoint counts (entities get the remainder + * when odd so entityCount + endpointCount === total). + */ +export function splitMixedCount(total: number): { + entities: number; + endpoints: number; +} { + const endpoints = Math.floor(total / 2); + return { entities: total - endpoints, endpoints }; +} + +export function parseScenarioId(id: string): ScenarioAxes { + const parts = id.split('/'); + if ( + parts.length !== 6 || + parts[0] !== 'android' || + parts[4] !== 'interaction' + ) { + throw new Error(`invalid android scenario id: ${id}`); + } + const config: GCScenarioConfig = { + candidateKind: parts[1] as CandidateKind, + pattern: parts[2] as Pattern, + count: Number(parts[3]), + control: parts[5] as Control, + }; + validateScenarioConfig(config); + return { + platform: 'android', + ...config, + mode: 'interaction', + }; +} + +export function listScenarios(filter?: string): ScenarioAxes[] { + const kinds: CandidateKind[] = ['entity', 'endpoint', 'mixed']; + const controls: Control[] = ['gc', 'no-gc']; + const out: ScenarioAxes[] = []; + + for (const candidateKind of kinds) { + for (const count of CANONICAL_COUNTS) { + for (const control of controls) { + out.push({ + platform: 'android', + candidateKind, + pattern: 'unique', + count, + mode: 'interaction', + control, + }); + } + } + } + + for (const count of CANONICAL_COUNTS) { + for (const control of controls) { + out.push({ + platform: 'android', + candidateKind: 'entity', + pattern: 'duplicate', + count, + mode: 'interaction', + control, + }); + } + } + + if (!filter) return out; + const prefix = filter.startsWith('^'); + const needle = prefix ? filter.slice(1) : filter; + return out.filter(s => { + const id = scenarioId(s); + return prefix ? id.startsWith(needle) : id.includes(needle); + }); +} diff --git a/examples/benchmark-native/src/types.ts b/examples/benchmark-native/src/types.ts new file mode 100644 index 000000000000..f8d64e4867ee --- /dev/null +++ b/examples/benchmark-native/src/types.ts @@ -0,0 +1,189 @@ +/** + * Local schemaVersion 1 types mirroring the documented GC measurement protocol. + * No shared runtime package — semantic compatibility only. + */ + +export type CandidateKind = 'entity' | 'endpoint' | 'mixed'; +export type Pattern = 'unique' | 'duplicate'; +export type Control = 'gc' | 'no-gc'; +export type Mode = 'interaction'; +export type UiCaptureSource = 'FrameMetrics' | 'Choreographer'; + +/** Canonical scenario counts. */ +export const CANONICAL_COUNTS = [1_000, 10_000, 100_000] as const; +export type CanonicalCount = (typeof CANONICAL_COUNTS)[number]; + +/** Axes for Android GC scenarios. */ +export interface GCScenarioConfig { + candidateKind: CandidateKind; + /** `duplicate` is entity-only (one path released `count` times). */ + pattern: Pattern; + count: number; + control: Control; +} + +export interface GCPreparedSummary { + queueEntries: number; + uniqueTargets: number; +} + +/** + * JS-side interaction measurement (schemaVersion 1). + * Native UI-frame and process-memory fields are attached by the runner. + */ +export interface GCAndroidMeasurement { + schemaVersion: 1; + totalMs: number; + /** Monolithic baseline: exactly `[totalMs]` when control is `gc`; empty for `no-gc`. */ + sliceDurationsMs: number[]; + actionCount: number; + queueEntries: number; + uniqueTargets: number; + actionTargetCount: number; + deletionCount: number; + timerDelayMs: number; + frameIntervalsMs: number[]; + displayPeriodMs: number; + missedFrames: number; + maxInputDelayMs: number; + /** Native UI capture source — required when UI metrics are present. */ + uiCaptureSource: UiCaptureSource; + uiFrameCount: number; + /** Aggregate max single-frame duration/interval (ms). */ + uiMaxFrameDurationMs: number; + /** Aggregate sum of frame durations/intervals (ms). */ + uiTotalFrameDurationMs: number; + uiMissedFrames: number; + uiRefreshPeriodMs: number; + uiRefreshRateHz: number; + processPssBeforeKb?: number; + processPssAfterKb?: number; + processPssDeltaKb?: number; + processRssBeforeKb?: number; + processRssAfterKb?: number; + processRssDeltaKb?: number; + jsHeapBeforeBytes?: number; + jsHeapAfterBytes?: number; + jsHeapDeltaBytes?: number; +} + +export interface NumberSummary { + median: number; + min: number; + max: number; + p95: number; + p99: number; +} + +export interface GCScenarioReport { + id: string; + platform: 'android'; + candidateKind: CandidateKind; + pattern: Pattern; + count: number; + mode: Mode; + control: Control; + samples: GCAndroidMeasurement[]; + summary: Record; +} + +export interface AndroidEnvironment { + platform: 'android'; + apiLevel: number; + release: string; + manufacturer: string; + model: string; + device: string; + brand: string; + buildType: string; + applicationId: string; + hermesEnabled: boolean; + hermesRuntimeProperties?: Record; + refreshRateHz: number; + refreshPeriodMs: number; +} + +/** Embedded BuildManifest v1 (baked into APK assets at prepare time). */ +export interface BuildManifestV1 { + schemaVersion: 1; + /** + * Source-build identity: deterministic sha256 over + * schemaVersion, gitCommit, gitDirty, sourceDigest (not APK bytes). + */ + buildId: string; + gitCommit: string; + gitDirty: boolean; + /** Content digest of sorted app + packages/core/src inputs. */ + sourceDigest: string; + createdAt: string; +} + +/** Host sidecar written after APK assemble (authority for collection). */ +export interface BuildSidecarV1 { + schemaVersion: 1; + /** Same source-build identity as the embedded BuildManifest. */ + buildId: string; + sourceDigest: string; + gitCommit: string; + gitDirty: boolean; + apkPath: string; + /** Artifact identity: sha256 of the single release APK bytes. */ + apkSha256: string; + apkSizeBytes: number; + /** Artifact-aware id: sha256(buildId ∥ apkSha256). */ + sidecarId: string; + builtAt: string; +} + +export interface GCMeasurementReport { + schemaVersion: 1; + units: Record; + memorySemantics: { + model: 'keep-store-drop-observer'; + description: string; + }; + build: { + /** + * Source-build identity from the embedded BuildManifest + * (deterministic over schemaVersion/gitCommit/gitDirty/sourceDigest). + * Matched to sidecar.buildId at collection — not live checkout authority. + */ + buildId: string; + sourceDigest: string; + gitCommit: string; + gitDirty: boolean; + label?: string; + apkSizeBytes?: number; + hermesBytecodeBytes?: number; + hermesAssetsBytes?: number; + /** Host sidecar fields attached at collection — not live checkout. */ + sidecar?: { + buildId: string; + sourceDigest: string; + /** Artifact identity of the release APK. */ + apkSha256: string; + apkPath: string; + sidecarId: string; + }; + /** sha256 of the APK actually installed on device (must equal sidecar.apkSha256). */ + installedApkSha256?: string; + }; + environment: AndroidEnvironment; + config: { + samplesPerScenario: number; + filter: string | null; + scenarioId: string; + }; + scenarios: GCScenarioReport[]; +} + +export interface LaunchConfig { + autoRun: boolean; + candidateKind: CandidateKind; + pattern: Pattern; + count: number; + control: Control; + samples: number; + /** Optional human label only — not provenance authority. */ + label?: string; +} diff --git a/examples/benchmark-native/src/validateConfig.ts b/examples/benchmark-native/src/validateConfig.ts new file mode 100644 index 000000000000..9d58a66420cf --- /dev/null +++ b/examples/benchmark-native/src/validateConfig.ts @@ -0,0 +1,182 @@ +import { + CANONICAL_COUNTS, + type CandidateKind, + type Control, + type GCScenarioConfig, + type LaunchConfig, + type Pattern, +} from './types'; + +export const CANDIDATE_KINDS: readonly CandidateKind[] = [ + 'entity', + 'endpoint', + 'mixed', +] as const; + +export const PATTERNS: readonly Pattern[] = ['unique', 'duplicate'] as const; + +export const CONTROLS: readonly Control[] = ['gc', 'no-gc'] as const; + +/** Positive sample counts capped to keep accidental huge runs from launching. */ +export const MAX_SAMPLES = 50; + +export class ConfigValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'ConfigValidationError'; + } +} + +function isCanonicalCount(count: number): boolean { + return (CANONICAL_COUNTS as readonly number[]).includes(count); +} + +/** + * Validate scenario axes. Throws ConfigValidationError on invalid input. + * Does not coerce — callers must pass already-parsed values or use parseLaunchConfig. + */ +export function validateScenarioConfig(config: GCScenarioConfig): void { + if (!CANDIDATE_KINDS.includes(config.candidateKind)) { + throw new ConfigValidationError( + `invalid candidateKind=${String(config.candidateKind)}; expected one of ${CANDIDATE_KINDS.join('|')}`, + ); + } + if (!PATTERNS.includes(config.pattern)) { + throw new ConfigValidationError( + `invalid pattern=${String(config.pattern)}; expected one of ${PATTERNS.join('|')}`, + ); + } + if (!CONTROLS.includes(config.control)) { + throw new ConfigValidationError( + `invalid control=${String(config.control)}; expected one of ${CONTROLS.join('|')}`, + ); + } + if (!Number.isInteger(config.count) || !isCanonicalCount(config.count)) { + throw new ConfigValidationError( + `invalid count=${String(config.count)}; expected one of ${CANONICAL_COUNTS.join('|')}`, + ); + } + if (config.pattern === 'duplicate' && config.candidateKind !== 'entity') { + throw new ConfigValidationError( + `duplicate pattern only supports candidateKind=entity (got ${config.candidateKind})`, + ); + } +} + +export function validateSampleCount(samples: number): void { + if ( + !Number.isInteger(samples) || + samples < 1 || + samples > MAX_SAMPLES || + !Number.isFinite(samples) + ) { + throw new ConfigValidationError( + `invalid samples=${String(samples)}; expected integer 1..${MAX_SAMPLES}`, + ); + } +} + +export function validateLaunchConfig(config: LaunchConfig): void { + validateScenarioConfig({ + candidateKind: config.candidateKind, + pattern: config.pattern, + count: config.count, + control: config.control, + }); + validateSampleCount(config.samples); +} + +/** + * Parse raw launch/intent/env fields into a LaunchConfig. + * Missing optional fields get safe defaults; invalid values throw. + */ +export function parseLaunchConfig(raw: { + autoRun?: unknown; + candidateKind?: unknown; + pattern?: unknown; + count?: unknown; + control?: unknown; + samples?: unknown; + label?: unknown; +}): LaunchConfig { + const autoRun = Boolean(raw.autoRun); + + const candidateKindRaw = + raw.candidateKind === undefined || raw.candidateKind === null + ? 'entity' + : String(raw.candidateKind); + const patternRaw = + raw.pattern === undefined || raw.pattern === null + ? 'unique' + : String(raw.pattern); + const controlRaw = + raw.control === undefined || raw.control === null + ? 'gc' + : String(raw.control); + + if (raw.count !== undefined && raw.count !== null && raw.count !== '') { + const countNum = Number(raw.count); + if (!Number.isInteger(countNum)) { + throw new ConfigValidationError( + `invalid count=${String(raw.count)}; expected integer canonical count`, + ); + } + } + const count = + raw.count === undefined || raw.count === null || raw.count === '' + ? 1000 + : Number(raw.count); + + const samplesRaw = + raw.samples === undefined || raw.samples === null || raw.samples === '' + ? 1 + : Number(raw.samples); + if (!Number.isFinite(samplesRaw)) { + throw new ConfigValidationError( + `invalid samples=${String(raw.samples)}; expected integer 1..${MAX_SAMPLES}`, + ); + } + const samples = Math.floor(samplesRaw); + + const label = + typeof raw.label === 'string' && raw.label.length > 0 + ? raw.label + : undefined; + + const config: LaunchConfig = { + autoRun, + candidateKind: candidateKindRaw as CandidateKind, + pattern: patternRaw as Pattern, + count, + control: controlRaw as Control, + samples, + label, + }; + validateLaunchConfig(config); + return config; +} + +/** Host-env shape used by collect-report / matrix (strings from the shell). */ +export function parseHostEnvConfig(env: { + candidateKind?: string; + pattern?: string; + count?: string | number; + control?: string; + samples?: string | number; +}): GCScenarioConfig & { samples: number } { + const parsed = parseLaunchConfig({ + autoRun: false, + candidateKind: env.candidateKind, + pattern: env.pattern, + count: env.count, + control: env.control, + samples: env.samples, + }); + return { + candidateKind: parsed.candidateKind, + pattern: parsed.pattern, + count: parsed.count, + control: parsed.control, + samples: parsed.samples, + }; +} diff --git a/examples/benchmark-native/tsconfig.json b/examples/benchmark-native/tsconfig.json new file mode 100644 index 000000000000..66a2c0a97212 --- /dev/null +++ b/examples/benchmark-native/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@react-native/typescript-config", + "compilerOptions": { + "types": ["jest"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["**/*.ts", "**/*.tsx", "src/globals.d.ts"], + "exclude": ["**/node_modules", "**/Pods", "android", "artifacts"] +} diff --git a/examples/benchmark-react/.gitignore b/examples/benchmark-react/.gitignore new file mode 100644 index 000000000000..faf1b1e9bc7f --- /dev/null +++ b/examples/benchmark-react/.gitignore @@ -0,0 +1,2 @@ +# Generated GC detailed report (default path; override with BENCH_GC_OUTPUT) +gc-measurement-output.json diff --git a/examples/benchmark-react/AGENTS.md b/examples/benchmark-react/AGENTS.md index bbce217dcc65..62d305e7784c 100644 --- a/examples/benchmark-react/AGENTS.md +++ b/examples/benchmark-react/AGENTS.md @@ -32,7 +32,7 @@ Filtering: `yarn bench --lib data-client --size small --action update` ## Scenario System -`BASE_SCENARIOS` in `bench/scenarios.ts` × `LIBRARIES` via `flatMap`. `onlyLibs` restricts to specific libs. CI runs data-client hot-path only (no memory/startup/deterministic). Memory is opt-in locally (`--action memory`). Convergent timing uses single page load with adaptive iterations and early stopping on statistical convergence. Ref-stability scenarios run once (deterministic count, not ops/s). +`BASE_SCENARIOS` in `bench/scenarios.ts` × `LIBRARIES` via `flatMap`. `onlyLibs` restricts to specific libs. CI runs data-client hot-path only (no memory/startup/deterministic/gc). Memory and GC are opt-in locally (`--action memory` / `--action gc` / `yarn bench:gc`). Convergent timing uses single page load with adaptive iterations and early stopping on statistical convergence. Ref-stability scenarios run once (deterministic count, not ops/s). GC uses a dedicated fixed-sample phase with an isolated Controller harness (`src/data-client/gcBrowserHarness.ts`), not the DataProvider `benchGC` singleton. ## Update Data Flow @@ -75,3 +75,7 @@ Filtering: `yarn bench --lib data-client --size small --action update` | `BENCH_TRACE=true` | Chrome tracing for duration scenarios | | `BENCH_V8_TRACE=true` | Launch Chromium with `--trace-opt --trace-deopt`; output to `v8-trace.log` | | `BENCH_V8_DEOPT=true` | Launch Chromium with `--prof`; V8 logs to `v8-logs/` | +| `BENCH_GC_SAMPLES` | Samples per GC scenario (default 5; also `--samples`) | +| `BENCH_GC_OUTPUT` | Override path for detailed GC JSON (default `gc-measurement-output.json`, gitignored) | + +BuildManifest v1 is written to `dist/gc-build-manifest.json` after webpack (`yarn build`). `buildId` digests schemaVersion, commit, dirty, sourceDigest, and sorted artifact hashes. GC runs require a matching served manifest. diff --git a/examples/benchmark-react/README.md b/examples/benchmark-react/README.md index 6cb1d7d29a47..e81081b8db5b 100644 --- a/examples/benchmark-react/README.md +++ b/examples/benchmark-react/README.md @@ -30,6 +30,7 @@ All implementations share presentational components, fixture data, fetch functio - **With network (local)** — Same shared-author update but with simulated network delay (consistent ms per "request"). Normalized caches propagate via a single store update; query-keyed caches invalidate and refetch affected queries. **Not run in CI** — run locally with `yarn bench` (no `CI` env) to include these. - **Memory (local only)** — Heap delta after repeated mount/unmount cycles. - **Startup (local only)** — FCP and task duration via CDP `Performance.getMetrics`. +- **GC (local only)** — Isolated data-client cache GC interaction + heap deltas on Chromium. Excluded by default and from CI; opt in with `--action gc`. ## Scenarios @@ -52,6 +53,29 @@ All implementations share presentational components, fixture data, fetch functio - **Memory mount/unmount cycle** (`memory-mount-unmount-cycle`) — Mount 500 issues, unmount, repeat 10 times; report JS heap delta (bytes) via CDP. Surfaces leaks or unbounded growth. +**GC (local only, data-client)** + +Isolated monolithic cache-GC baseline (not V8/engine GC). Builds a separate Controller + timerless GCPolicy with raw deterministic state — no 100k React rows, no network. Mode is always end-to-end (sweep → reduce). + +- **Axes:** `candidateKind` entity|endpoint|mixed × `pattern` unique|duplicate (duplicate is entity-only) × `count` 1k/10k/100k × `control` gc|no-gc +- **Timing boundaries:** prepare (fixtures + queue + quiet `displayPeriodMs` + dynamic harness import) is untimed; `runGCScenario` measures only sweep/no-op inside a Chromium-calibrated probe (rAF registers next rAF, then `setTimeout(0)` runs collection post-paint while a frame is pending — Chromium-specific calibration, not a web guarantee); engine GC for heap snapshots is outside interaction timing +- **Probes:** wall-clock frame timestamps, `setTimeout(0)` `timerDelayMs`, `missedFrames` via nearest-period excess over measured `displayPeriodMs`, long-task overlap filter + `takeRecords`. `maxInputDelayMs` is a responsiveness proxy (not pointer input). Validation runs a synthetic 40–50 ms block calibration confirming the probe spans pending frames +- **Stable IDs:** detailed report scenario ids are `browser/{kind}/{pattern}/{count}/end-to-end/{control}` (not display names) +- **Failures:** incomplete runs write `complete:false` with failure records and exit nonzero +- **Bundle fairness:** GC harness is a dynamic `import()` chunk loaded during prepare — not in the initial data-client bundle +- **Provenance:** `yarn build` writes `dist/gc-build-manifest.json` (BuildManifest v1). GC runner verifies local source digest + artifact hashes and matching served manifest `buildId` before measuring; report embeds verified provenance (never live HEAD) +- **Outputs:** scalar median `totalMs` on stdout. Detailed schemaVersion 1 JSON is always written to `gc-measurement-output.json` (gitignored; override path with `BENCH_GC_OUTPUT`). Failed runs still write `complete:false` reports before exiting nonzero + +```bash +yarn build # webpack + BuildManifest +yarn bench:gc # all GC scenarios, 5 samples each +yarn bench:gc --scenario unique-1000 --samples 3 +yarn bench:gc --scenario entity-unique-100000 +BENCH_GC_OUTPUT=./custom-gc.json yarn bench:gc --scenario mixed-unique-100000 +yarn test:gc-metrics +yarn test:gc-provenance # after build +``` + **Startup (local only)** - **Startup FCP** (`startup-fcp`) — First Contentful Paint time via CDP `Performance.getMetrics`. @@ -171,8 +195,10 @@ CI convergence targets: 2% (small scenarios), 3% (large scenarios). Reported mar |---|---|---| | `--lib ` | `BENCH_LIB` | Comma-separated library names (e.g. `data-client,swr`) | | `--size ` | `BENCH_SIZE` | Run only `small` (cheap, full rigor) or `large` (expensive, reduced runs) scenarios | - | `--action ` | `BENCH_ACTION` | Filter by action group (`mount`, `update`, `mutation`, `memory`) or exact action name. Memory is **not run by default**; use `--action memory` to include. | + | `--action ` | `BENCH_ACTION` | Filter by action group (`mount`, `update`, `mutation`, `memory`, `gc`) or exact action name. Memory and GC are **not run by default**; use `--action memory` or `--action gc` to include. | | `--scenario ` | `BENCH_SCENARIO` | Substring filter on scenario name | + | `--samples ` | `BENCH_GC_SAMPLES` | Samples per GC scenario (default 5; GC phase only) | + | (env only) | `BENCH_GC_OUTPUT` | Override detailed GC report path (default `gc-measurement-output.json`) | CLI flags take precedence over env vars. Examples: @@ -181,6 +207,7 @@ CI convergence targets: 2% (small scenarios), 3% (large scenarios). Reported mar yarn bench --size small # only cheap scenarios (full warmup/measurement) yarn bench --action mount # init, mountSortedView yarn bench --action memory # memory-mount-unmount-cycle (heap delta; opt-in category) + yarn bench --action gc # isolated cache GC scenarios (data-client; opt-in) yarn bench --action update --lib swr # update scenarios for swr only yarn bench --scenario sorted-view # only sorted-view scenarios ``` @@ -191,6 +218,7 @@ CI convergence targets: 2% (small scenarios), 3% (large scenarios). Reported mar yarn bench:small # --size small yarn bench:large # --size large yarn bench:dc # --lib data-client + yarn bench:gc # --lib data-client --action gc ``` 5. **Scenario sizes** @@ -201,6 +229,7 @@ CI convergence targets: 2% (small scenarios), 3% (large scenarios). Reported mar - **Small** (deterministic, single run): `ref-stability-*` - **Large** (convergent: 5 warmup + 10–50 measurement iterations): `getlist-500`, `getlist-500-sorted`, `update-user`, `update-user-10000`, `update-entity-sorted`, `update-entity-multi-view`, `list-detail-switch-10` - **Memory** (opt-in, 1 warmup + 3 measurement rounds): `memory-mount-unmount-cycle` — run with `--action memory` + - **GC** (opt-in, fixed samples default 5): `gc-*-{1000,10000,100000}-{gc,no-gc}` — run with `yarn bench:gc` or `--action gc` Timing scenarios use convergent mode (single page load, inline convergence per scenario). Each group uses its own warmup/measurement config. Use `--size` to run only one group. diff --git a/examples/benchmark-react/bench/build-manifest.ts b/examples/benchmark-react/bench/build-manifest.ts new file mode 100644 index 000000000000..ee554a63ea54 --- /dev/null +++ b/examples/benchmark-react/bench/build-manifest.ts @@ -0,0 +1,277 @@ +/** + * BuildManifest v1 — provenance for browser GC measurements. + * + * Generated after webpack build. Runner verifies local source digest + artifact + * hashes and that the served `/gc-build-manifest.json` matches before measuring. + */ +import { execSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +export const BENCH_ROOT = path.resolve(__dirname, '..'); +export const REPO_ROOT = path.resolve(BENCH_ROOT, '../..'); +export const DIST_DIR = path.join(BENCH_ROOT, 'dist'); +export const MANIFEST_FILENAME = 'gc-build-manifest.json'; +export const MANIFEST_PATH = path.join(DIST_DIR, MANIFEST_FILENAME); + +export interface BuildManifestV1 { + schemaVersion: 1; + buildId: string; + commit: string; + dirty: boolean; + sourceDigest: string; + /** relative path from dist/ → sha256 hex */ + artifacts: Record; +} + +function sha256Buffer(buf: Buffer): string { + return createHash('sha256').update(buf).digest('hex'); +} + +function sha256File(filePath: string): string { + return sha256Buffer(fs.readFileSync(filePath)); +} + +function git(cmd: string): string { + try { + return execSync(`git ${cmd}`, { + cwd: REPO_ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + return ''; + } +} + +/** Walk directory; return absolute file paths sorted. */ +function walkFiles(dir: string): string[] { + if (!fs.existsSync(dir)) return []; + const out: string[] = []; + const stack = [dir]; + while (stack.length) { + const cur = stack.pop()!; + for (const ent of fs.readdirSync(cur, { withFileTypes: true })) { + const p = path.join(cur, ent.name); + if (ent.isDirectory()) { + if ( + ent.name === 'node_modules' || + ent.name === 'dist' || + ent.name === '.git' + ) + continue; + stack.push(p); + } else if (ent.isFile()) { + out.push(p); + } + } + } + return out.sort(); +} + +/** + * Sorted relevant inputs for sourceDigest: + * packages/core/src + benchmark-react src/bench/config/package files. + * Hashes actual file contents (includes dirty/untracked). + */ +export function listSourceInputFiles(): string[] { + const files = new Set(); + for (const f of walkFiles(path.join(REPO_ROOT, 'packages/core/src'))) { + files.add(f); + } + const benchRoots = [ + path.join(BENCH_ROOT, 'src'), + path.join(BENCH_ROOT, 'bench'), + ]; + for (const root of benchRoots) { + for (const f of walkFiles(root)) files.add(f); + } + for (const name of [ + 'package.json', + 'tsconfig.json', + 'webpack.config.cjs', + '.babelrc.js', + 'playwright.config.ts', + ]) { + const p = path.join(BENCH_ROOT, name); + if (fs.existsSync(p)) files.add(p); + } + return [...files].sort(); +} + +export function computeSourceDigest(files = listSourceInputFiles()): string { + const h = createHash('sha256'); + for (const abs of files) { + const rel = path.relative(REPO_ROOT, abs).split(path.sep).join('/'); + h.update(rel); + h.update('\0'); + h.update(fs.readFileSync(abs)); + h.update('\0'); + } + return h.digest('hex'); +} + +export function listDistArtifacts(distDir = DIST_DIR): string[] { + return walkFiles(distDir) + .filter(f => path.basename(f) !== MANIFEST_FILENAME) + .sort(); +} + +export function hashDistArtifacts(distDir = DIST_DIR): Record { + const artifacts: Record = {}; + for (const abs of listDistArtifacts(distDir)) { + const rel = path.relative(distDir, abs).split(path.sep).join('/'); + artifacts[rel] = sha256File(abs); + } + // Stable key order + return Object.fromEntries( + Object.keys(artifacts) + .sort() + .map(k => [k, artifacts[k]]), + ); +} + +export function isGitDirty(): boolean { + const status = git('status --porcelain'); + return status.length > 0; +} + +export function gitCommitFull(): string { + return git('rev-parse HEAD') || 'unknown'; +} + +export function computeBuildId(inputs: { + schemaVersion: 1; + commit: string; + dirty: boolean; + sourceDigest: string; + artifacts: Record; +}): string { + const h = createHash('sha256'); + // Canonical BuildManifest v1 inputs (order fixed; artifacts sorted by path) + h.update(`schemaVersion:${inputs.schemaVersion}\n`); + h.update(`commit:${inputs.commit}\n`); + h.update(`dirty:${inputs.dirty ? '1' : '0'}\n`); + h.update(`sourceDigest:${inputs.sourceDigest}\n`); + for (const k of Object.keys(inputs.artifacts).sort()) { + h.update(k); + h.update(':'); + h.update(inputs.artifacts[k]); + h.update('\n'); + } + return h.digest('hex'); +} + +export function writeBuildManifest(distDir = DIST_DIR): BuildManifestV1 { + fs.mkdirSync(distDir, { recursive: true }); + const sourceDigest = computeSourceDigest(); + const artifacts = hashDistArtifacts(distDir); + const commit = gitCommitFull(); + const dirty = isGitDirty(); + const schemaVersion = 1 as const; + const buildId = computeBuildId({ + schemaVersion, + commit, + dirty, + sourceDigest, + artifacts, + }); + const manifest: BuildManifestV1 = { + schemaVersion, + buildId, + commit, + dirty, + sourceDigest, + artifacts, + }; + fs.writeFileSync( + path.join(distDir, MANIFEST_FILENAME), + `${JSON.stringify(manifest, null, 2)}\n`, + ); + return manifest; +} + +export function readBuildManifest( + manifestPath = MANIFEST_PATH, +): BuildManifestV1 { + const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + if (raw.schemaVersion !== 1) { + throw new Error( + `unsupported build manifest schemaVersion: ${raw.schemaVersion}`, + ); + } + return raw as BuildManifestV1; +} + +export interface ProvenanceVerification { + ok: true; + manifest: BuildManifestV1; +} + +/** + * Verify local source digest + on-disk artifact hashes match the manifest. + * Does not fetch the served URL (caller does that separately). + */ +export function verifyLocalManifest( + manifest: BuildManifestV1, + distDir = DIST_DIR, +): void { + const sourceDigest = computeSourceDigest(); + if (sourceDigest !== manifest.sourceDigest) { + throw new Error( + `stale build: sourceDigest mismatch (manifest ${manifest.sourceDigest.slice(0, 12)}… vs current ${sourceDigest.slice(0, 12)}…). Rebuild with yarn build.`, + ); + } + const artifacts = hashDistArtifacts(distDir); + const manKeys = Object.keys(manifest.artifacts).sort(); + const curKeys = Object.keys(artifacts).sort(); + if (manKeys.join('\n') !== curKeys.join('\n')) { + throw new Error( + 'stale/tampered build: dist artifact set does not match manifest', + ); + } + for (const k of manKeys) { + if (artifacts[k] !== manifest.artifacts[k]) { + throw new Error( + `tampered/stale artifact: ${k} hash mismatch (rebuild required)`, + ); + } + } + const buildId = computeBuildId({ + schemaVersion: 1, + commit: manifest.commit, + dirty: manifest.dirty, + sourceDigest, + artifacts, + }); + if (buildId !== manifest.buildId) { + throw new Error( + 'buildId mismatch after recompute (manifest fields tampered or corrupted?)', + ); + } +} + +/** CLI: write manifest after webpack. */ +async function main() { + if (!fs.existsSync(DIST_DIR)) { + throw new Error(`dist/ missing at ${DIST_DIR}; run webpack build first`); + } + const m = writeBuildManifest(); + process.stderr.write( + `BuildManifest v1 → ${MANIFEST_FILENAME} buildId=${m.buildId.slice(0, 16)}… commit=${m.commit.slice(0, 8)} dirty=${m.dirty}\n`, + ); +} + +const isMain = + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); + +if (isMain) { + main().catch(err => { + console.error(err); + process.exit(1); + }); +} diff --git a/examples/benchmark-react/bench/gc-interaction-metrics.test.ts b/examples/benchmark-react/bench/gc-interaction-metrics.test.ts new file mode 100644 index 000000000000..fe816cb5a9fa --- /dev/null +++ b/examples/benchmark-react/bench/gc-interaction-metrics.test.ts @@ -0,0 +1,291 @@ +/** + * Deterministic unit tests for GC interaction metrics, stable IDs, and report completeness. + * + * yarn test:gc-metrics + */ +import { buildGCReport } from './gc-report.ts'; +import { + browserGCScenarioId, + computeMaxInputDelayMs, + excessMissedFrames, + frameIntervalsFromTimestamps, + longTaskOverlapsWindow, + parseBrowserGCScenarioId, +} from '../src/data-client/gcInteractionMetrics.ts'; + +function assert(cond: boolean, message: string): asserts cond { + if (!cond) throw new Error(message); +} + +function assertEq(actual: unknown, expected: unknown, label: string) { + assert( + Object.is(actual, expected) || actual === expected, + `${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, + ); +} + +function assertClose( + actual: number, + expected: number, + eps: number, + label: string, +) { + assert( + Math.abs(actual - expected) <= eps, + `${label}: expected ≈${expected} (±${eps}), got ${actual}`, + ); +} + +// --- frameIntervalsFromTimestamps --- +assertEq(frameIntervalsFromTimestamps([]).length, 0, 'empty timestamps'); +assertEq(frameIntervalsFromTimestamps([10]).length, 0, 'single timestamp'); +{ + const intervals = frameIntervalsFromTimestamps([0, 16.7, 50.1]); + assertEq(intervals.length, 2, 'two intervals'); + assertClose(intervals[0], 16.7, 1e-9, 'first interval'); + assertClose(intervals[1], 33.4, 1e-9, '2-period gap interval'); +} + +// --- excessMissedFrames (nearest-period) --- +const period = 16.67; +assertEq(excessMissedFrames([period], period), 0, 'nominal 1× interval'); +assertEq( + excessMissedFrames([period * 1.98], period), + 1, + '1.98× must count as 2 periods → 1 missed (floor would wrongly yield 0)', +); +assertEq( + excessMissedFrames([32.5], 16.67), + 1, + '32.5/16.67≈1.95 → round to 2 → 1 missed', +); +assertEq( + excessMissedFrames([period * 0.9, period * 1.05], period), + 0, + 'sub-period jitter around 1× → zero excess', +); +assertEq( + excessMissedFrames([period * 2.9, period], period), + 2, + '2.9× → 3 periods → 2 missed; plus nominal frame', +); +assertEq(excessMissedFrames([period], 0), 0, 'zero displayPeriod safe'); + +// Calibration-scale gap (~45ms busy with ~16.7 period → ~2 missed) +{ + const blockedSpan = frameIntervalsFromTimestamps([ + 0, + 16.7, + 16.7 + 45, + 16.7 + 45 + 16.7, + ]); + assertEq( + excessMissedFrames(blockedSpan, 16.7), + 2, + '45ms blocked span → 2 missed frames over one expected period', + ); + assert( + Math.max(...blockedSpan) >= 16.7 * 1.5, + 'calibration-scale interval exceeds 1.5× display period', + ); +} + +// --- computeMaxInputDelayMs --- +assertEq( + computeMaxInputDelayMs(12, [16.7, 16.7], 16.7), + 12, + 'timer dominates when frames are nominal', +); +assertEq( + computeMaxInputDelayMs(5, [16.7, 40], 16.7), + 40 - 16.7, + 'frame excess dominates when larger than timer', +); +assertEq(computeMaxInputDelayMs(0, [], 16.7), 0, 'empty frames → timer only'); + +// --- longTaskOverlapsWindow --- +assert( + longTaskOverlapsWindow({ startTime: 9, duration: 20 }, 10, 40), + 'entry starting just before windowStart still overlaps', +); +assert( + !longTaskOverlapsWindow({ startTime: 0, duration: 5 }, 10, 40), + 'entry ending before windowStart does not overlap', +); +assert( + !longTaskOverlapsWindow({ startTime: 40, duration: 5 }, 10, 40), + 'entry starting at windowEnd does not overlap', +); +assert( + longTaskOverlapsWindow({ startTime: 15, duration: 10 }, 10, 40), + 'entry fully inside window overlaps', +); + +// --- stable semantic IDs --- +assertEq( + browserGCScenarioId({ + candidateKind: 'endpoint', + pattern: 'unique', + count: 100000, + control: 'gc', + }), + 'browser/endpoint/unique/100000/end-to-end/gc', + 'endpoint unique 100k gc id', +); +assertEq( + browserGCScenarioId({ + candidateKind: 'entity', + pattern: 'duplicate', + count: 1000, + control: 'no-gc', + }), + 'browser/entity/duplicate/1000/end-to-end/no-gc', + 'entity duplicate no-gc id', +); +{ + const id = 'browser/mixed/unique/10000/end-to-end/gc'; + const parsed = parseBrowserGCScenarioId(id); + assertEq(parsed.candidateKind, 'mixed', 'parse kind'); + assertEq(parsed.pattern, 'unique', 'parse pattern'); + assertEq(parsed.count, 10000, 'parse count'); + assertEq(parsed.control, 'gc', 'parse control'); + assertEq(parsed.mode, 'end-to-end', 'parse mode'); + assertEq(browserGCScenarioId(parsed), id, 'round-trip id'); +} +let threw = false; +try { + browserGCScenarioId({ + candidateKind: 'endpoint', + pattern: 'duplicate', + count: 1000, + control: 'gc', + }); +} catch { + threw = true; +} +assert(threw, 'duplicate+endpoint must throw'); + +// --- report complete flag --- +{ + const provenance = { + schemaVersion: 1 as const, + buildId: 'abc', + commit: 'deadbeef', + dirty: false, + sourceDigest: 'digest', + artifacts: {}, + servedManifestBuildId: 'abc', + }; + const completeReport = buildGCReport({ + scenarios: [], + samplesPerScenario: 5, + filter: null, + browserVersion: '1', + headless: true, + requestedScenarios: 0, + requestedSamples: 0, + completedScenarios: 0, + completedSamples: 0, + failures: [], + provenance, + }); + assert(completeReport.complete === true, 'empty requested → complete'); + + const incomplete = buildGCReport({ + scenarios: [], + samplesPerScenario: 5, + filter: null, + browserVersion: '1', + headless: true, + requestedScenarios: 1, + requestedSamples: 5, + completedScenarios: 0, + completedSamples: 2, + failures: [ + { + scenarioId: 'browser/endpoint/unique/100000/end-to-end/gc', + sampleIndex: 2, + error: 'boom', + }, + ], + provenance, + }); + assert(incomplete.complete === false, 'failures → complete:false'); + assertEq(incomplete.failures.length, 1, 'failures recorded'); + assertEq(incomplete.completedSamples, 2, 'partial samples counted'); + assertEq( + incomplete.provenance.buildId, + 'abc', + 'provenance from verified manifest not HEAD', + ); +} + +/** Throwing work must reject promptly and leave no active rAF polyfill chain. */ +async function testThrowingWorkRejectsPromptly() { + const { runChromiumInteractionProbe } = + await import('../src/data-client/gcInteractionProbe.ts'); + + const pendingRaf = new Set>(); + const g = globalThis as typeof globalThis & { + requestAnimationFrame?: (cb: (t: number) => void) => number; + cancelAnimationFrame?: (id: number) => void; + }; + const prevRaf = g.requestAnimationFrame; + const prevCancel = g.cancelAnimationFrame; + + g.requestAnimationFrame = (cb: (t: number) => void) => { + const handle = setTimeout(() => { + pendingRaf.delete(handle); + cb(performance.now()); + }, 0); + pendingRaf.add(handle); + return handle as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + const handle = id as unknown as ReturnType; + clearTimeout(handle); + pendingRaf.delete(handle); + }; + + try { + const t0 = performance.now(); + let rejected: unknown; + try { + await runChromiumInteractionProbe({ + displayPeriodMs: 16.7, + work: () => { + throw new Error('synthetic work boom'); + }, + }); + } catch (err) { + rejected = err; + } + const elapsed = performance.now() - t0; + assert(rejected instanceof Error, 'throwing work must reject'); + assert( + (rejected as Error).message === 'synthetic work boom', + 'rejection carries work error', + ); + assert(elapsed < 500, `must reject promptly (elapsed ${elapsed}ms)`); + + await new Promise(r => setTimeout(r, 30)); + assert( + pendingRaf.size === 0, + `no active rAF chain after reject (pending=${pendingRaf.size})`, + ); + } finally { + if (prevRaf) g.requestAnimationFrame = prevRaf; + else delete g.requestAnimationFrame; + if (prevCancel) g.cancelAnimationFrame = prevCancel; + else delete g.cancelAnimationFrame; + } +} + +testThrowingWorkRejectsPromptly() + .then(() => { + process.stderr.write('gc-interaction-metrics: all assertions passed\n'); + }) + .catch(err => { + console.error(err); + process.exit(1); + }); diff --git a/examples/benchmark-react/bench/gc-provenance.test.ts b/examples/benchmark-react/bench/gc-provenance.test.ts new file mode 100644 index 000000000000..b6e33ac38608 --- /dev/null +++ b/examples/benchmark-react/bench/gc-provenance.test.ts @@ -0,0 +1,125 @@ +/** + * Provenance tamper tests: artifact / commit / dirty / buildId must fail verify. + * + * yarn test:gc-provenance + */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { + DIST_DIR, + MANIFEST_PATH, + computeBuildId, + readBuildManifest, + verifyLocalManifest, + writeBuildManifest, + type BuildManifestV1, +} from './build-manifest.ts'; + +function assert(cond: boolean, msg: string): asserts cond { + if (!cond) throw new Error(msg); +} + +function expectVerifyFail(label: string, mutate: (m: BuildManifestV1) => void) { + const original = fs.readFileSync(MANIFEST_PATH, 'utf8'); + try { + const m = readBuildManifest(); + mutate(m); + fs.writeFileSync(MANIFEST_PATH, `${JSON.stringify(m, null, 2)}\n`); + let failed = false; + try { + verifyLocalManifest(readBuildManifest()); + } catch (err) { + failed = true; + process.stderr.write( + `provenance: ${label} rejected: ${err instanceof Error ? err.message : err}\n`, + ); + } + assert(failed, `${label}: expected verifyLocalManifest to fail`); + } finally { + fs.writeFileSync(MANIFEST_PATH, original); + } +} + +if ( + !fs.existsSync(DIST_DIR) || + !fs.existsSync(path.join(DIST_DIR, 'data-client.js')) +) { + throw new Error('dist/ incomplete — run yarn build first'); +} + +// Ensure a fresh manifest exists for the current tree +writeBuildManifest(); +const manifest = readBuildManifest(); +verifyLocalManifest(manifest); +process.stderr.write('provenance: clean verify OK\n'); + +// Canonical buildId includes schemaVersion, commit, dirty, sourceDigest, artifacts +{ + const expected = computeBuildId({ + schemaVersion: 1, + commit: manifest.commit, + dirty: manifest.dirty, + sourceDigest: manifest.sourceDigest, + artifacts: manifest.artifacts, + }); + assert( + expected === manifest.buildId, + 'stored buildId must match canonical digest of manifest fields', + ); + const flippedDirty = computeBuildId({ + schemaVersion: 1, + commit: manifest.commit, + dirty: !manifest.dirty, + sourceDigest: manifest.sourceDigest, + artifacts: manifest.artifacts, + }); + assert(flippedDirty !== manifest.buildId, 'dirty bit must affect buildId'); + const otherCommit = computeBuildId({ + schemaVersion: 1, + commit: `${manifest.commit}dead`, + dirty: manifest.dirty, + sourceDigest: manifest.sourceDigest, + artifacts: manifest.artifacts, + }); + assert(otherCommit !== manifest.buildId, 'commit must affect buildId'); +} + +// Artifact content tamper +const target = path.join(DIST_DIR, 'data-client.js'); +const originalArtifact = fs.readFileSync(target); +try { + fs.writeFileSync( + target, + Buffer.concat([originalArtifact, Buffer.from('\n/* tamper */\n')]), + ); + let failed = false; + try { + verifyLocalManifest(readBuildManifest()); + } catch (err) { + failed = true; + process.stderr.write( + `provenance: artifact tamper rejected: ${err instanceof Error ? err.message : err}\n`, + ); + } + assert(failed, 'tampered artifact must fail verifyLocalManifest'); +} finally { + fs.writeFileSync(target, originalArtifact); + writeBuildManifest(); + verifyLocalManifest(readBuildManifest()); +} + +// Manifest field tampers (buildId left stale → mismatch) +expectVerifyFail('commit field tamper', m => { + m.commit = `${m.commit}0`; +}); +expectVerifyFail('dirty field tamper', m => { + m.dirty = !m.dirty; +}); +expectVerifyFail('buildId field tamper', m => { + m.buildId = '0'.repeat(64); +}); + +verifyLocalManifest(readBuildManifest()); +assert(fs.existsSync(MANIFEST_PATH), 'manifest exists'); +process.stderr.write('gc-provenance: all assertions passed\n'); diff --git a/examples/benchmark-react/bench/gc-report.ts b/examples/benchmark-react/bench/gc-report.ts new file mode 100644 index 000000000000..679fae09c5e2 --- /dev/null +++ b/examples/benchmark-react/bench/gc-report.ts @@ -0,0 +1,273 @@ +/** + * Aggregate schemaVersion 1 detailed GC measurement report for the browser harness. + * Raw Chromium traces (BENCH_TRACE) remain separate diagnostic artifacts. + */ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import type { BuildManifestV1 } from './build-manifest.js'; +import { browserGCScenarioId } from '../src/data-client/gcInteractionMetrics.ts'; +import type { + GCBrowserMeasurement, + GCScenarioConfig, +} from '../src/shared/types.js'; + +export interface GCSampleResult extends GCBrowserMeasurement { + heapBeforeBytes?: number; + heapAfterBytes?: number; + heapDeltaBytes?: number; +} + +export interface NumberSummary { + median: number; + min: number; + max: number; + p95: number; + p99: number; +} + +export interface GCScenarioReport { + /** Stable id: browser/{kind}/{pattern}/{count}/end-to-end/{control} */ + id: string; + platform: 'browser'; + candidateKind: GCScenarioConfig['candidateKind']; + pattern: GCScenarioConfig['pattern']; + count: number; + mode: 'end-to-end'; + control: GCScenarioConfig['control']; + samples: GCSampleResult[]; + summary: Record; +} + +export interface GCFailureRecord { + scenarioId: string; + sampleIndex?: number; + error: string; +} + +export interface GCMeasurementReport { + schemaVersion: 1; + /** false when any requested sample/scenario failed */ + complete: boolean; + requestedScenarios: number; + requestedSamples: number; + completedScenarios: number; + completedSamples: number; + failures: GCFailureRecord[]; + units: Record; + memorySemantics: { + model: 'keep-store-drop-observer'; + description: string; + }; + /** Verified BuildManifest provenance — never live HEAD */ + provenance: { + buildId: string; + commit: string; + dirty: boolean; + sourceDigest: string; + servedManifestBuildId: string; + }; + build: { + commit: string; + label: string; + }; + environment: { + runtime: 'chromium'; + browserVersion: string; + headless: boolean; + os: string; + arch: string; + cpuModel: string; + cpus: number; + platform: NodeJS.Platform; + nodeVersion: string; + }; + config: { + samplesPerScenario: number; + filter: string | null; + }; + scenarios: GCScenarioReport[]; +} + +/** Caller must pass a non-empty sorted array. */ +function percentile(sorted: number[], p: number): number { + if (sorted.length === 1) return sorted[0]!; + const idx = (p / 100) * (sorted.length - 1); + const lo = Math.floor(idx); + const hi = Math.ceil(idx); + if (lo === hi) return sorted[lo]!; + const w = idx - lo; + return sorted[lo]! * (1 - w) + sorted[hi]! * w; +} + +export function summarizeNumbers(values: number[]): NumberSummary | null { + if (values.length === 0) return null; + const sorted = values.slice().sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + const median = + sorted.length % 2 === 0 ? + (sorted[mid - 1]! + sorted[mid]!) / 2 + : sorted[mid]!; + return { + median, + min: sorted[0]!, + max: sorted[sorted.length - 1]!, + p95: percentile(sorted, 95), + p99: percentile(sorted, 99), + }; +} + +export function summarizeGCSamples( + samples: GCSampleResult[], +): Record { + const summary: Record = { + totalMs: summarizeNumbers(samples.map(s => s.totalMs)), + actionCount: summarizeNumbers(samples.map(s => s.actionCount)), + queueEntries: summarizeNumbers(samples.map(s => s.queueEntries)), + uniqueTargets: summarizeNumbers(samples.map(s => s.uniqueTargets)), + actionTargetCount: summarizeNumbers(samples.map(s => s.actionTargetCount)), + deletionCount: summarizeNumbers(samples.map(s => s.deletionCount)), + timerDelayMs: summarizeNumbers(samples.map(s => s.timerDelayMs)), + displayPeriodMs: summarizeNumbers(samples.map(s => s.displayPeriodMs)), + missedFrames: summarizeNumbers(samples.map(s => s.missedFrames)), + maxInputDelayMs: summarizeNumbers(samples.map(s => s.maxInputDelayMs)), + longTaskCount: summarizeNumbers(samples.map(s => s.longTaskCount)), + longTaskTotalMs: summarizeNumbers(samples.map(s => s.longTaskTotalMs)), + }; + const slices = samples.flatMap(s => s.sliceDurationsMs ?? []); + if (slices.length) { + summary.sliceDurationsMs = summarizeNumbers(slices); + } + const frameIntervals = samples.flatMap(s => s.frameIntervalsMs ?? []); + if (frameIntervals.length) { + summary.frameIntervalsMs = summarizeNumbers(frameIntervals); + } + const heaps = samples.filter(s => s.heapBeforeBytes != null); + if (heaps.length) { + summary.heapBeforeBytes = summarizeNumbers( + heaps.map(s => s.heapBeforeBytes!), + ); + summary.heapAfterBytes = summarizeNumbers( + heaps.map(s => s.heapAfterBytes!), + ); + summary.heapDeltaBytes = summarizeNumbers( + heaps.map(s => s.heapDeltaBytes!), + ); + } + return summary; +} + +export function cpuModel(): string { + return os.cpus()[0]?.model ?? 'unknown'; +} + +export function defaultGCReportPath(): string { + return path.resolve( + process.env.BENCH_GC_OUTPUT ?? 'gc-measurement-output.json', + ); +} + +export function writeGCReport( + report: GCMeasurementReport, + outputPath: string = defaultGCReportPath(), +): void { + fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`); + process.stderr.write( + `GC detailed report → ${outputPath} (complete=${report.complete})\n`, + ); +} + +export function scenarioReportFromConfig( + config: GCScenarioConfig, + samples: GCSampleResult[], +): GCScenarioReport { + const id = browserGCScenarioId(config); + return { + id, + platform: 'browser', + candidateKind: config.candidateKind, + pattern: config.pattern, + count: config.count, + mode: 'end-to-end', + control: config.control, + samples, + summary: summarizeGCSamples(samples), + }; +} + +export function buildGCReport(opts: { + scenarios: GCScenarioReport[]; + samplesPerScenario: number; + filter: string | null; + browserVersion: string; + headless: boolean; + requestedScenarios: number; + requestedSamples: number; + completedScenarios: number; + completedSamples: number; + failures: GCFailureRecord[]; + provenance: BuildManifestV1 & { servedManifestBuildId: string }; +}): GCMeasurementReport { + const complete = + opts.failures.length === 0 && + opts.completedScenarios === opts.requestedScenarios && + opts.completedSamples === opts.requestedSamples; + + return { + schemaVersion: 1, + complete, + requestedScenarios: opts.requestedScenarios, + requestedSamples: opts.requestedSamples, + completedScenarios: opts.completedScenarios, + completedSamples: opts.completedSamples, + failures: opts.failures, + units: { + totalMs: 'milliseconds', + sliceDurationsMs: 'milliseconds', + timerDelayMs: 'milliseconds', + frameIntervalsMs: 'milliseconds', + displayPeriodMs: 'milliseconds', + maxInputDelayMs: 'milliseconds', + longTaskTotalMs: 'milliseconds', + missedFrames: 'frames', + heapBeforeBytes: 'bytes', + heapAfterBytes: 'bytes', + heapDeltaBytes: 'bytes', + }, + memorySemantics: { + model: 'keep-store-drop-observer', + description: + 'heapBefore is taken after prepareGCScenario with forced Chromium engine GC. After runGCScenario (which validates and releases captured GC action arrays), settle + forced engine GC yields heapAfter while the live store remains. Engine GC is never inside interaction timing. Compare gc vs no-gc for retained-cache signal.', + }, + provenance: { + buildId: opts.provenance.buildId, + commit: opts.provenance.commit, + dirty: opts.provenance.dirty, + sourceDigest: opts.provenance.sourceDigest, + servedManifestBuildId: opts.provenance.servedManifestBuildId, + }, + build: { + commit: opts.provenance.commit, + label: 'examples/benchmark-react GC monolithic browser baseline', + }, + environment: { + runtime: 'chromium', + browserVersion: opts.browserVersion, + headless: opts.headless, + os: `${os.type()} ${os.release()}`, + arch: os.arch(), + cpuModel: cpuModel(), + cpus: os.cpus().length, + platform: process.platform, + nodeVersion: process.version, + }, + config: { + samplesPerScenario: opts.samplesPerScenario, + filter: opts.filter, + }, + scenarios: opts.scenarios, + }; +} + +export { browserGCScenarioId }; diff --git a/examples/benchmark-react/bench/runner.ts b/examples/benchmark-react/bench/runner.ts index 56dec71ce177..5588213ec37b 100644 --- a/examples/benchmark-react/bench/runner.ts +++ b/examples/benchmark-react/bench/runner.ts @@ -10,6 +10,21 @@ import type { Page, } from 'playwright'; +import { + MANIFEST_FILENAME, + MANIFEST_PATH, + readBuildManifest, + verifyLocalManifest, + type BuildManifestV1, +} from './build-manifest.js'; +import { + buildGCReport, + scenarioReportFromConfig, + writeGCReport, + type GCFailureRecord, + type GCSampleResult, + type GCScenarioReport, +} from './gc-report.js'; import { collectMeasures, getMeasureDuration } from './measure.js'; import { collectHeapUsed } from './memory.js'; import { formatReport, type BenchmarkResult } from './report.js'; @@ -24,7 +39,13 @@ import { import type { ConvergentProfile } from './scenarios.js'; import { computeStats, isConverged } from './stats.js'; import { parseTraceDuration } from './tracing.js'; -import type { Scenario, ScenarioSize } from '../src/shared/types.js'; +import { browserGCScenarioId } from '../src/data-client/gcInteractionMetrics.ts'; +import type { + GCBrowserMeasurement, + GCScenarioConfig, + Scenario, + ScenarioSize, +} from '../src/shared/types.js'; // --------------------------------------------------------------------------- // CLI + env var parsing @@ -37,6 +58,8 @@ function parseArgs(): { scenario?: string; networkSim: boolean; opsPerRound?: number; + /** Validated samples-per-GC-scenario (≥1 integer). */ + gcSamples: number; } { const argv = process.argv.slice(2); const get = (flag: string, envVar: string): string | undefined => { @@ -51,6 +74,8 @@ function parseArgs(): { const scenarioRaw = get('--scenario', 'BENCH_SCENARIO'); const networkSimRaw = get('--network-sim', 'BENCH_NETWORK_SIM'); const opsRaw = get('--ops-per-round', 'BENCH_OPS_PER_ROUND'); + // Flag / env / default resolved once here (filterScenarios must not re-read env). + const gcSamplesRaw = get('--samples', 'BENCH_GC_SAMPLES') ?? '5'; const libs = libRaw ? libRaw.split(',').map(s => s.trim()) : undefined; const size = sizeRaw === 'small' || sizeRaw === 'large' ? sizeRaw : undefined; @@ -59,6 +84,16 @@ function parseArgs(): { const networkSim = networkSimRaw != null ? networkSimRaw !== 'false' : !process.env.CI; const opsPerRound = opsRaw ? parseInt(opsRaw, 10) : undefined; + const gcSamplesParsed = Number.parseInt(gcSamplesRaw, 10); + if ( + !Number.isFinite(gcSamplesParsed) || + !Number.isInteger(gcSamplesParsed) || + gcSamplesParsed < 1 + ) { + throw new Error( + `invalid --samples / BENCH_GC_SAMPLES=${JSON.stringify(gcSamplesRaw)}; expected integer ≥ 1`, + ); + } return { libs, @@ -67,6 +102,7 @@ function parseArgs(): { scenario: scenarioRaw, networkSim, opsPerRound, + gcSamples: gcSamplesParsed, }; } @@ -75,18 +111,32 @@ function filterScenarios(scenarios: Scenario[]): { libraries: string[]; networkSim: boolean; opsPerRound?: number; + gcSamples: number; + scenarioFilter: string | null; } { const { libs, size, actions, - scenario: scenarioFilter, + scenario: scenarioFilterArg, networkSim, opsPerRound, + gcSamples, } = parseArgs(); + const scenarioFilter = scenarioFilterArg ?? null; const libraries = libs ?? (process.env.CI ? ['data-client'] : [...LIBRARIES]); + // GC scenarios are data-client-only; default lib when selecting --action gc + const effectiveLibraries = + ( + !libs && + actions && + actions.every(a => a === 'gc' || a === 'runGCScenario') + ) ? + ['data-client'] + : libraries; + let filtered = scenarios; // In CI, restrict to data-client hot-path only (existing behavior) @@ -96,14 +146,23 @@ function filterScenarios(scenarios: Scenario[]): { s.name.startsWith('data-client:') && s.category !== 'memory' && s.category !== 'startup' && + s.category !== 'gc' && !s.deterministic, ); } else if ( !actions || - !actions.some(a => a === 'memory' || a === 'mountUnmountCycle') + !actions.some( + a => + a === 'memory' || + a === 'mountUnmountCycle' || + a === 'gc' || + a === 'runGCScenario', + ) ) { - // Locally: exclude memory by default; use --action memory to include - filtered = filtered.filter(s => s.category !== 'memory'); + // Locally: exclude memory/gc by default; use --action memory|gc to include + filtered = filtered.filter( + s => s.category !== 'memory' && s.category !== 'gc', + ); } if (libs) { @@ -133,10 +192,18 @@ function filterScenarios(scenarios: Scenario[]): { // Multi-lib runs: omit scenarios that do not apply to every selected library (e.g. invalidate-and-resolve). filtered = filtered.filter( s => - !s.onlyLibs?.length || libraries.every(lib => s.onlyLibs!.includes(lib)), + !s.onlyLibs?.length || + effectiveLibraries.every(lib => s.onlyLibs!.includes(lib)), ); - return { filtered, libraries, networkSim, opsPerRound }; + return { + filtered, + libraries: effectiveLibraries, + networkSim, + opsPerRound, + gcSamples, + scenarioFilter, + }; } // --------------------------------------------------------------------------- @@ -190,7 +257,9 @@ function isConvergentScenario(scenario: Scenario): boolean { return ( !scenario.deterministic && scenario.category !== 'memory' && + scenario.category !== 'gc' && scenario.resultMetric !== 'heapDelta' && + scenario.resultMetric !== 'totalMs' && !isRefStabilityScenario(scenario) ); } @@ -579,11 +648,15 @@ async function runScenarioConvergent( if (cdp && subIdx > 0 && subIdx % CONVERGENT_GC_INTERVAL === 0) { try { await cdp.send('HeapProfiler.collectGarbage'); - } catch {} + } catch { + // best-effort + } await page.waitForTimeout(30); try { await cdp.send('HeapProfiler.collectGarbage'); - } catch {} + } catch { + // best-effort + } await page.waitForTimeout(50); } @@ -760,6 +833,7 @@ function reportV8Logs(): void { function scenarioUnit(scenario: Scenario): string { if (isRefStabilityScenario(scenario)) return 'count'; if (scenario.resultMetric === 'heapDelta') return 'bytes'; + if (scenario.resultMetric === 'totalMs') return 'ms'; return 'ops/s'; } @@ -781,10 +855,243 @@ function recordResult( function warmupCount(scenario: Scenario): number { if (scenario.deterministic) return 0; if (scenario.category === 'memory') return MEMORY_WARMUP; + if (scenario.category === 'gc') return 0; if (isConvergentScenario(scenario)) return 0; return RUN_CONFIG[scenario.size ?? 'small'].warmup; } +async function settlePage(page: Page): Promise { + await page.evaluate( + () => + new Promise(r => + requestAnimationFrame(() => requestAnimationFrame(() => r())), + ), + ); + await page.waitForTimeout(50); +} + +async function verifyGCBuildProvenance( + baseUrl: string, +): Promise { + if (!fs.existsSync(MANIFEST_PATH)) { + throw new Error( + `Missing ${MANIFEST_FILENAME}. Run yarn build (writes BuildManifest v1 after webpack).`, + ); + } + const local = readBuildManifest(); + verifyLocalManifest(local); + + const servedUrl = `${baseUrl.replace(/\/$/, '')}/${MANIFEST_FILENAME}`; + const res = await fetch(servedUrl); + if (!res.ok) { + throw new Error( + `Failed to fetch served ${MANIFEST_FILENAME} from ${servedUrl}: ${res.status}`, + ); + } + const served = (await res.json()) as BuildManifestV1; + if (served.schemaVersion !== 1) { + throw new Error(`served manifest schemaVersion ${served.schemaVersion}`); + } + if (served.buildId !== local.buildId) { + throw new Error( + `served manifest buildId mismatch (local ${local.buildId.slice(0, 12)}… vs served ${served.buildId.slice(0, 12)}…). Restart preview after rebuild.`, + ); + } + process.stderr.write( + `GC provenance OK buildId=${local.buildId.slice(0, 16)}… commit=${local.commit.slice(0, 8)} dirty=${local.dirty}\n`, + ); + return { ...local, servedManifestBuildId: served.buildId }; +} + +/** + * Dedicated GC phase (not the generic convergent/update path). + * Per sample: prepare → engine GC + heapBefore → run → settle → engine GC + + * heapAfter (store live) → dispose. Never force engine GC inside interaction timing. + * Failures are accumulated; report is always written with complete true/false; + * returns { reports, complete } and caller must exit nonzero when incomplete. + */ +async function runGCScenarioSample( + page: Page, + bench: any, + config: GCScenarioConfig, + cdp: CDPSession, +): Promise { + await (bench as any).evaluate(async (api: any, cfg: GCScenarioConfig) => { + if (!api.prepareGCScenario) { + throw new Error('prepareGCScenario not available'); + } + await api.prepareGCScenario(cfg); + }, config); + + // Forced engine GC + heapBefore (outside interaction timing) + const heapBeforeBytes = await collectHeapUsed(cdp); + + const measurement: GCBrowserMeasurement = await (bench as any).evaluate( + async (api: any) => { + if (!api.runGCScenario) { + throw new Error('runGCScenario not available'); + } + return api.runGCScenario(); + }, + ); + + await settlePage(page); + + // Forced engine GC + heapAfter while store remains live + const heapAfterBytes = await collectHeapUsed(cdp); + + await (bench as any).evaluate((api: any) => { + if (api.disposeGCScenario) api.disposeGCScenario(); + }); + + return { + ...measurement, + heapBeforeBytes, + heapAfterBytes, + heapDeltaBytes: heapAfterBytes - heapBeforeBytes, + }; +} + +async function runGCPhase( + browser: Browser, + scenarios: Scenario[], + sampleCount: number, + scenarioFilter: string | null, + samples: Map, + provenance: BuildManifestV1 & { servedManifestBuildId: string }, +): Promise<{ reports: GCScenarioReport[]; complete: boolean }> { + const reports: GCScenarioReport[] = []; + const failures: GCFailureRecord[] = []; + const requestedScenarios = scenarios.length; + const requestedSamples = scenarios.length * sampleCount; + let completedScenarios = 0; + let completedSamples = 0; + + if (scenarios.length === 0) { + return { reports, complete: true }; + } + + process.stderr.write( + `\n── GC (${scenarios.length} scenarios, ${sampleCount} samples each) ──\n`, + ); + + const context = await browser.newContext(); + const page = await context.newPage(); + const cdp = await context.newCDPSession(page); + try { + await cdp.send('Performance.enable'); + } catch { + // best-effort + } + + for (const scenario of scenarios) { + const config = scenario.args[0] as GCScenarioConfig; + const scenarioId = browserGCScenarioId(config); + const scenarioSamples: GCSampleResult[] = []; + let scenarioFailed = false; + + process.stderr.write(` ${scenario.name} (${scenarioId})...\n`); + + for (let i = 0; i < sampleCount; i++) { + try { + const { bench } = await setupBenchPage( + page, + 'data-client', + scenario, + false, + ); + const sample = await runGCScenarioSample(page, bench, config, cdp); + scenarioSamples.push(sample); + recordResult(samples, scenario, { value: sample.totalMs }); + completedSamples++; + await bench.dispose(); + } catch (err) { + scenarioFailed = true; + const message = err instanceof Error ? err.message : String(err); + failures.push({ + scenarioId, + sampleIndex: i, + error: message, + }); + console.error(` ${scenario.name} sample ${i} FAILED:`, message); + try { + await page.evaluate(() => { + window.__BENCH__?.disposeGCScenario?.(); + }); + } catch { + // ignore cleanup failures + } + break; + } + } + + if (!scenarioFailed && scenarioSamples.length === sampleCount) { + completedScenarios++; + const report = scenarioReportFromConfig(config, scenarioSamples); + const { summary } = report; + const medianTotal = summary.totalMs?.median ?? 0; + const maxFrame = + summary.frameIntervalsMs?.max ?? summary.frameIntervalsMs?.median; + process.stderr.write( + ` median totalMs=${medianTotal.toFixed(3)} ms` + + (summary.timerDelayMs ? + ` timerDelay=${summary.timerDelayMs.median.toFixed(2)} ms` + : '') + + (summary.displayPeriodMs ? + ` displayPeriod=${summary.displayPeriodMs.median.toFixed(2)} ms` + : '') + + (maxFrame != null ? + ` frameIntervalMax=${maxFrame.toFixed(2)} ms` + : '') + + (summary.maxInputDelayMs ? + ` maxInputDelay=${summary.maxInputDelayMs.median.toFixed(2)} ms` + : '') + + (summary.missedFrames != null ? + ` missedFrames=${summary.missedFrames.median}` + : '') + + (summary.longTaskCount ? + ` longTasks=${summary.longTaskCount.median}` + + (summary.longTaskTotalMs ? + `/${summary.longTaskTotalMs.median.toFixed(2)} ms` + : '') + : '') + + (summary.heapDeltaBytes ? + ` heapΔ=${Math.round(summary.heapDeltaBytes.median)} B` + : '') + + `\n`, + ); + reports.push(report); + } + } + + await cdp.detach().catch(() => {}); + await context.close(); + + const version = browser.version(); + const report = buildGCReport({ + scenarios: reports, + samplesPerScenario: sampleCount, + filter: scenarioFilter, + browserVersion: version, + headless: true, + requestedScenarios, + requestedSamples, + completedScenarios, + completedSamples, + failures, + provenance, + }); + writeGCReport(report); + + if (!report.complete) { + process.stderr.write( + `GC phase incomplete: ${failures.length} failure(s), completed ${completedSamples}/${requestedSamples} samples\n`, + ); + } + + return { reports, complete: report.complete }; +} + /** Run each scenario once per matching library (one browser context per lib). */ async function runRound( browser: Browser, @@ -811,11 +1118,15 @@ async function runRound( // Double-GC before each scenario to reduce variance from prior allocations try { await cdp.send('HeapProfiler.collectGarbage'); - } catch {} + } catch { + // best-effort + } await page.waitForTimeout(100); try { await cdp.send('HeapProfiler.collectGarbage'); - } catch {} + } catch { + // best-effort + } await page.waitForTimeout(400); done++; @@ -858,6 +1169,8 @@ async function main() { libraries, networkSim, opsPerRound, + gcSamples, + scenarioFilter, } = filterScenarios(SCENARIOS); if (opsPerRound != null) { @@ -875,7 +1188,10 @@ async function main() { } const memoryScenarios = SCENARIOS_TO_RUN.filter(s => s.category === 'memory'); - const mainScenarios = SCENARIOS_TO_RUN.filter(s => s.category !== 'memory'); + const gcScenarios = SCENARIOS_TO_RUN.filter(s => s.category === 'gc'); + const mainScenarios = SCENARIOS_TO_RUN.filter( + s => s.category !== 'memory' && s.category !== 'gc', + ); const bySize: Record = { small: [], large: [] }; for (const s of mainScenarios) { @@ -937,11 +1253,15 @@ async function main() { for (const scenario of libScenarios) { try { await cdp.send('HeapProfiler.collectGarbage'); - } catch {} + } catch { + // best-effort + } await page.waitForTimeout(100); try { await cdp.send('HeapProfiler.collectGarbage'); - } catch {} + } catch { + // best-effort + } await page.waitForTimeout(400); process.stderr.write(` ${scenario.name}...\n`); @@ -1043,6 +1363,21 @@ async function main() { } } + // GC: dedicated phase (opt-in via --action gc) + let gcComplete = true; + if (gcScenarios.length > 0) { + const provenance = await verifyGCBuildProvenance(BASE_URL); + const { complete } = await runGCPhase( + browser, + gcScenarios, + gcSamples, + scenarioFilter, + samples, + provenance, + ); + gcComplete = complete; + } + await closeBenchBrowser(); reportV8Logs(); @@ -1110,6 +1445,10 @@ async function main() { process.stderr.write('\n'); process.stdout.write(formatReport(report)); + + if (!gcComplete) { + process.exitCode = 1; + } } main().catch(err => { diff --git a/examples/benchmark-react/bench/scenarios.ts b/examples/benchmark-react/bench/scenarios.ts index 9e1298142ddc..93d9be515989 100644 --- a/examples/benchmark-react/bench/scenarios.ts +++ b/examples/benchmark-react/bench/scenarios.ts @@ -65,6 +65,7 @@ export const ACTION_GROUPS: Record = { update: ['updateEntity', 'updateUser'], mutation: ['unshiftItem', 'deleteEntity', 'invalidateAndResolve', 'moveItem'], memory: ['mountUnmountCycle'], + gc: ['runGCScenario'], }; type BaseScenario = Omit & { @@ -220,14 +221,68 @@ export const LIBRARIES = [ 'baseline', ] as const; -export const SCENARIOS: Scenario[] = LIBRARIES.flatMap(lib => - BASE_SCENARIOS.filter( - base => !base.onlyLibs || base.onlyLibs.includes(lib), - ).map( - ({ nameSuffix, onlyLibs, ...rest }): Scenario => ({ +/** Canonical GC counts matching the Node GC harness / shared vocabulary. */ +export const GC_CANONICAL_COUNTS = [1_000, 10_000, 100_000] as const; + +const GC_UNIQUE_KINDS = ['entity', 'endpoint', 'mixed'] as const; +const GC_CONTROLS = ['gc', 'no-gc'] as const; + +/** data-client-only browser GC scenarios (excluded by default / CI; `--action gc`). */ +const GC_SCENARIOS: Scenario[] = (() => { + const out: Scenario[] = []; + for (const count of GC_CANONICAL_COUNTS) { + const size: ScenarioSize = count <= 1_000 ? 'small' : 'large'; + for (const candidateKind of GC_UNIQUE_KINDS) { + for (const control of GC_CONTROLS) { + out.push({ + name: `data-client: gc-${candidateKind}-unique-${count}-${control}`, + action: 'runGCScenario', + args: [ + { + candidateKind, + pattern: 'unique', + count, + control, + }, + ], + category: 'gc', + resultMetric: 'totalMs', + size, + onlyLibs: ['data-client'], + }); + } + } + for (const control of GC_CONTROLS) { + out.push({ + name: `data-client: gc-entity-duplicate-${count}-${control}`, + action: 'runGCScenario', + args: [ + { + candidateKind: 'entity', + pattern: 'duplicate', + count, + control, + }, + ], + category: 'gc', + resultMetric: 'totalMs', + size, + onlyLibs: ['data-client'], + }); + } + } + return out; +})(); + +export const SCENARIOS: Scenario[] = [ + ...LIBRARIES.flatMap(lib => + BASE_SCENARIOS.filter( + base => !base.onlyLibs || base.onlyLibs.includes(lib), + ).map(({ nameSuffix, onlyLibs, ...rest }): Scenario => ({ name: `${lib}: ${nameSuffix}`, ...rest, ...(onlyLibs ? { onlyLibs: [...onlyLibs] } : {}), - }), + })), ), -); + ...GC_SCENARIOS, +]; diff --git a/examples/benchmark-react/bench/validate.ts b/examples/benchmark-react/bench/validate.ts index 3006c11a896c..7d13796b0eae 100644 --- a/examples/benchmark-react/bench/validate.ts +++ b/examples/benchmark-react/bench/validate.ts @@ -534,6 +534,188 @@ test('listDetailSwitch completes with correct DOM transitions', async (page, lib ); }); +test( + 'GC frame probe calibration: synthetic block spans pending frames', + async (page, lib) => { + if ( + !(await page.evaluate( + () => typeof window.__BENCH__?.calibrateGCFrameProbe === 'function', + )) + ) + return; + + const result = await page.evaluate(async () => { + return window.__BENCH__!.calibrateGCFrameProbe!(45); + }); + + assert( + result.blockMs >= 40 && result.blockMs <= 50, + lib, + 'gc calibrate blockMs', + `expected 40–50ms block, got ${result.blockMs}`, + ); + assert( + result.totalMs >= 35, + lib, + 'gc calibrate totalMs', + `synthetic block should be timed (~45ms), got ${result.totalMs}`, + ); + assert( + result.spannedPendingFrame === true, + lib, + 'gc calibrate spannedPendingFrame', + `expected blocking to span pending frame (missedFrames=${result.missedFrames} frameIntervalMax=${result.frameIntervalMax} displayPeriod=${result.displayPeriodMs})`, + ); + assert( + result.frameIntervalMax >= result.displayPeriodMs * 1.5 || + result.missedFrames >= 1, + lib, + 'gc calibrate frame gap', + `expected large frame gap or missedFrames≥1`, + ); + process.stderr.write( + ` calibrate: totalMs=${result.totalMs.toFixed(1)} frameIntervalMax=${result.frameIntervalMax.toFixed(1)} missedFrames=${result.missedFrames} displayPeriod=${result.displayPeriodMs.toFixed(2)}\n`, + ); + }, + { onlyLibs: ['data-client'] }, +); + +test( + 'GC scenario API: prepare/run/dispose cardinality (1k entity unique)', + async (page, lib) => { + if ( + !(await page.evaluate( + () => typeof window.__BENCH__?.prepareGCScenario === 'function', + )) + ) + return; + + const prepared = await page.evaluate(async () => { + return window.__BENCH__!.prepareGCScenario!({ + candidateKind: 'entity', + pattern: 'unique', + count: 1000, + control: 'gc', + }); + }); + assert( + prepared.queueEntries === 1000, + lib, + 'gc prepare queueEntries', + `expected 1000, got ${prepared.queueEntries}`, + ); + assert( + prepared.uniqueTargets === 1000, + lib, + 'gc prepare uniqueTargets', + `expected 1000, got ${prepared.uniqueTargets}`, + ); + + const measurement = await page.evaluate(async () => { + return window.__BENCH__!.runGCScenario!(); + }); + assert( + measurement.schemaVersion === 1, + lib, + 'gc schemaVersion', + `expected 1, got ${measurement.schemaVersion}`, + ); + assert( + measurement.actionCount === 1, + lib, + 'gc actionCount', + `expected 1, got ${measurement.actionCount}`, + ); + assert( + measurement.deletionCount === 1000, + lib, + 'gc deletionCount', + `expected 1000, got ${measurement.deletionCount}`, + ); + assert( + measurement.actionTargetCount === 1000, + lib, + 'gc actionTargetCount', + `expected 1000, got ${measurement.actionTargetCount}`, + ); + assert( + measurement.sliceDurationsMs.length === 1, + lib, + 'gc sliceDurationsMs', + `expected 1 slice, got ${measurement.sliceDurationsMs.length}`, + ); + assert( + measurement.displayPeriodMs > 0, + lib, + 'gc displayPeriodMs', + `expected positive displayPeriodMs, got ${measurement.displayPeriodMs}`, + ); + assert( + typeof measurement.maxInputDelayMs === 'number' && + measurement.maxInputDelayMs >= 0, + lib, + 'gc maxInputDelayMs', + `expected non-negative maxInputDelayMs, got ${measurement.maxInputDelayMs}`, + ); + assert( + typeof measurement.missedFrames === 'number' && + measurement.missedFrames >= 0, + lib, + 'gc missedFrames', + `expected non-negative missedFrames, got ${measurement.missedFrames}`, + ); + + // no-gc control: same prepare/run path, zero actions/deletions + await page.evaluate(() => window.__BENCH__!.disposeGCScenario!()); + await page.evaluate(async () => { + await window.__BENCH__!.prepareGCScenario!({ + candidateKind: 'entity', + pattern: 'unique', + count: 1000, + control: 'no-gc', + }); + }); + const noGc = await page.evaluate(async () => { + return window.__BENCH__!.runGCScenario!(); + }); + assert( + noGc.actionCount === 0 && noGc.deletionCount === 0, + lib, + 'gc no-gc control', + `expected zero action/deletion, got action=${noGc.actionCount} deletion=${noGc.deletionCount}`, + ); + + // duplicate pattern: queueEntries=count, uniqueTargets=1, deletionCount=1 + await page.evaluate(() => window.__BENCH__!.disposeGCScenario!()); + const dupPrepared = await page.evaluate(async () => { + return window.__BENCH__!.prepareGCScenario!({ + candidateKind: 'entity', + pattern: 'duplicate', + count: 1000, + control: 'gc', + }); + }); + assert( + dupPrepared.queueEntries === 1000 && dupPrepared.uniqueTargets === 1, + lib, + 'gc duplicate prepare', + `expected queue=1000 unique=1, got queue=${dupPrepared.queueEntries} unique=${dupPrepared.uniqueTargets}`, + ); + const dup = await page.evaluate(async () => { + return window.__BENCH__!.runGCScenario!(); + }); + assert( + dup.deletionCount === 1 && dup.actionTargetCount === 1000, + lib, + 'gc duplicate run', + `expected deletion=1 actionTarget=1000, got deletion=${dup.deletionCount} actionTarget=${dup.actionTargetCount}`, + ); + + await page.evaluate(() => window.__BENCH__!.disposeGCScenario!()); + }, + { onlyLibs: ['data-client'] }, +); + // ── TIMING VALIDATION ──────────────────────────────────────────────── // Verify that when data-bench-complete fires (measurement ends), the DOM // already reflects the update. A 100ms network delay makes timing bugs diff --git a/examples/benchmark-react/package.json b/examples/benchmark-react/package.json index 68f46b9cd26b..00d45ed28ec4 100644 --- a/examples/benchmark-react/package.json +++ b/examples/benchmark-react/package.json @@ -4,8 +4,8 @@ "private": true, "description": "React rendering benchmark comparing @data-client/react against other data libraries", "scripts": { - "build": "BROWSERSLIST_ENV=2026 webpack --mode=production", - "build:no-compiler": "BROWSERSLIST_ENV=2026 REACT_COMPILER=false webpack --mode=production", + "build": "BROWSERSLIST_ENV=2026 webpack --mode=production && npx tsx bench/build-manifest.ts", + "build:no-compiler": "BROWSERSLIST_ENV=2026 REACT_COMPILER=false webpack --mode=production && npx tsx bench/build-manifest.ts", "preview": "serve dist -l ${BENCH_PORT:-5173} --no-request-logging", "bench": "npx tsx bench/runner.ts", "bench:no-compiler": "BENCH_LABEL=no-compiler npx tsx bench/runner.ts", @@ -14,6 +14,9 @@ "bench:dc": "npx tsx bench/runner.ts --lib data-client", "bench:trace": "BENCH_V8_TRACE=true npx tsx bench/runner.ts --lib data-client --size small", "bench:deopt": "BENCH_V8_DEOPT=true npx tsx bench/runner.ts --lib data-client --size small", + "bench:gc": "npx tsx bench/runner.ts --lib data-client --action gc", + "test:gc-metrics": "npx tsx bench/gc-interaction-metrics.test.ts", + "test:gc-provenance": "npx tsx bench/gc-provenance.test.ts", "bench:run": "yarn build && (yarn preview &) && sleep 5 && yarn bench", "bench:run:no-compiler": "yarn build:no-compiler && (yarn preview &) && sleep 5 && yarn bench:no-compiler", "validate": "npx tsx bench/validate.ts", diff --git a/examples/benchmark-react/src/data-client/gcBrowserHarness.ts b/examples/benchmark-react/src/data-client/gcBrowserHarness.ts new file mode 100644 index 000000000000..62c396995923 --- /dev/null +++ b/examples/benchmark-react/src/data-client/gcBrowserHarness.ts @@ -0,0 +1,548 @@ +/** + * Isolated browser GC harness — separate from DataProvider's benchGC singleton. + * + * Builds a benchmark-only Controller + createReducer + timerless GCPolicy with + * raw deterministic state, queues via createCountRef, mode=end-to-end. Measures + * real core cache GC on the main thread without 100k React components or network. + * + * Timing boundaries: + * prepare — fixtures, queue, quiet displayPeriodMs (untimed) + * run — interaction probes + explicit sweep/no-op (timed); resolves after probes settle + * dispose — drop store/policy (after Playwright heapAfter) + */ +import { + Controller, + GCPolicy, + actionTypes, + createReducer, + initialState, +} from '@data-client/core'; +import type { State } from '@data-client/core'; +import type { + GCBrowserMeasurement, + GCPreparedSummary, + GCScenarioConfig, +} from '@shared/types'; + +import { measureDisplayPeriodMs } from './gcInteractionMetrics'; +import { + CALIBRATION_BLOCK_MS_MAX, + CALIBRATION_BLOCK_MS_MIN, + runChromiumInteractionProbe, + syntheticBlockMs, +} from './gcInteractionProbe'; + +const { GC } = actionTypes; + +type EntityPath = { key: string; pk: string }; + +export const ENTITY_KEY = 'BenchEntity'; +export const ZERO_META = Object.freeze({ + date: 0, + fetchedAt: 0, + expiresAt: 0, +}); + +/** Timerless explicit policy: no intervals; public sweep → protected runSweep. */ +class BrowserBenchmarkGCPolicy extends GCPolicy { + constructor() { + super({ expiresAt: () => 0 }); + } + + init(controller: Controller) { + this.controller = controller; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + cleanup() {} + + sweep() { + this.runSweep(); + } + + get entityQueueLength() { + return this.entitiesQ.length; + } + + get endpointQueueSize() { + return this.endpointsQ.size; + } + + get queueEntries() { + return this.entityQueueLength + this.endpointQueueSize; + } +} + +interface ExpectedScalars { + queueEntries: number; + uniqueTargets: number; + expectedEntitiesInAction: number; + expectedEndpointsInAction: number; + expectedUniqueEntityDeletions: number; + expectedEndpointDeletions: number; +} + +interface Harness { + policy: BrowserBenchmarkGCPolicy; + expected: ExpectedScalars; + getState: () => State; + getCapturedAction: () => { + type: string; + entities: EntityPath[]; + endpoints: string[]; + } | null; + clearCapturedAction: () => void; + dispose: () => void; +} + +function splitMixedCount(total: number) { + const endpoints = Math.floor(total / 2); + return { entities: total - endpoints, endpoints }; +} + +function entityPath(pk: number): EntityPath { + return { key: ENTITY_KEY, pk: String(pk) }; +} + +function endpointKey(i: number) { + return `bench-endpoint-${i}`; +} + +function buildEntityState(count: number): State { + const entities = { [ENTITY_KEY]: {} as Record }; + const entitiesMeta = { + [ENTITY_KEY]: {} as Record, + }; + for (let i = 0; i < count; i++) { + const pk = String(i); + entities[ENTITY_KEY][pk] = { id: pk }; + entitiesMeta[ENTITY_KEY][pk] = { ...ZERO_META }; + } + return { ...initialState, entities, entitiesMeta }; +} + +function buildEndpointState(count: number): State { + const endpoints: Record = {}; + const meta: Record = {}; + for (let i = 0; i < count; i++) { + const key = endpointKey(i); + endpoints[key] = String(i); + meta[key] = { ...ZERO_META }; + } + return { ...initialState, endpoints, meta }; +} + +function buildMixedState( + entityCount: number, + endpointCount: number, +): State { + const entities = buildEntityState(entityCount); + const endpoints = buildEndpointState(endpointCount); + return { + ...entities, + endpoints: endpoints.endpoints, + meta: endpoints.meta, + }; +} + +function buildStateForSpec(config: GCScenarioConfig): State { + const { candidateKind, pattern, count } = config; + if (pattern === 'duplicate') return buildEntityState(1); + if (candidateKind === 'entity') return buildEntityState(count); + if (candidateKind === 'endpoint') return buildEndpointState(count); + if (candidateKind === 'mixed') { + const { entities, endpoints } = splitMixedCount(count); + return buildMixedState(entities, endpoints); + } + throw new Error(`unknown candidateKind: ${candidateKind}`); +} + +function queueCandidates( + policy: BrowserBenchmarkGCPolicy, + config: GCScenarioConfig, +): ExpectedScalars { + const { candidateKind, pattern, count } = config; + + if (pattern === 'duplicate') { + if (candidateKind !== 'entity') { + throw new Error( + `duplicate pattern only supports candidateKind=entity (got ${candidateKind})`, + ); + } + const path = entityPath(0); + const countRef = policy.createCountRef({ paths: [path] }); + for (let i = 0; i < count; i++) { + const release = countRef(); + release(); + } + return { + queueEntries: count, + uniqueTargets: 1, + expectedEntitiesInAction: count, + expectedEndpointsInAction: 0, + expectedUniqueEntityDeletions: 1, + expectedEndpointDeletions: 0, + }; + } + + if (candidateKind === 'entity') { + for (let i = 0; i < count; i++) { + const release = policy.createCountRef({ paths: [entityPath(i)] })(); + release(); + } + return { + queueEntries: count, + uniqueTargets: count, + expectedEntitiesInAction: count, + expectedEndpointsInAction: 0, + expectedUniqueEntityDeletions: count, + expectedEndpointDeletions: 0, + }; + } + + if (candidateKind === 'endpoint') { + for (let i = 0; i < count; i++) { + const release = policy.createCountRef({ key: endpointKey(i) })(); + release(); + } + return { + queueEntries: count, + uniqueTargets: count, + expectedEntitiesInAction: 0, + expectedEndpointsInAction: count, + expectedUniqueEntityDeletions: 0, + expectedEndpointDeletions: count, + }; + } + + if (candidateKind === 'mixed') { + const { entities, endpoints } = splitMixedCount(count); + for (let i = 0; i < entities; i++) { + const release = policy.createCountRef({ paths: [entityPath(i)] })(); + release(); + } + for (let i = 0; i < endpoints; i++) { + const release = policy.createCountRef({ key: endpointKey(i) })(); + release(); + } + return { + queueEntries: count, + uniqueTargets: count, + expectedEntitiesInAction: entities, + expectedEndpointsInAction: endpoints, + expectedUniqueEntityDeletions: entities, + expectedEndpointDeletions: endpoints, + }; + } + + throw new Error(`unknown candidateKind: ${candidateKind}`); +} + +/** End-to-end harness: sweep dispatches and reduces synchronously. */ +function createHarness(config: GCScenarioConfig): Harness { + const state = buildStateForSpec(config); + const policy = new BrowserBenchmarkGCPolicy(); + const controller = new Controller({ gcPolicy: policy }); + const reducer = createReducer(controller); + + let capturedAction: { + type: string; + entities: EntityPath[]; + endpoints: string[]; + } | null = null; + let workingState: State = state; + + controller.getState = () => workingState; + controller.dispatch = ((action: any) => { + capturedAction = action; + workingState = reducer(workingState, action); + }) as typeof controller.dispatch; + + policy.init(controller); + const expected = queueCandidates(policy, config); + + if (policy.queueEntries !== expected.queueEntries) { + throw new Error( + `fixture queue cardinality ${policy.queueEntries} !== expected ${expected.queueEntries}`, + ); + } + + return { + policy, + expected, + getState: () => workingState, + getCapturedAction: () => capturedAction, + clearCapturedAction() { + capturedAction = null; + }, + dispose() { + capturedAction = null; + controller.getState = () => initialState; + controller.dispatch = (() => + Promise.resolve()) as typeof controller.dispatch; + policy.cleanup(); + }, + }; +} + +function countRemaining( + state: State, + config: GCScenarioConfig, + expected: ExpectedScalars, +) { + const bucket = state.entities?.[ENTITY_KEY]; + const entityRemaining = bucket ? Object.keys(bucket).length : 0; + const endpointRemaining = + state.endpoints ? Object.keys(state.endpoints).length : 0; + + const startedEntities = + config.pattern === 'duplicate' ? 1 : expected.expectedUniqueEntityDeletions; + const startedEndpoints = expected.expectedEndpointDeletions; + + return { + entityDeleted: startedEntities - entityRemaining, + endpointDeleted: startedEndpoints - endpointRemaining, + entityRemaining, + endpointRemaining, + }; +} + +function validateMeasurement( + config: GCScenarioConfig, + harness: Harness, + sample: Pick< + GCBrowserMeasurement, + | 'actionCount' + | 'deletionCount' + | 'queueEntries' + | 'uniqueTargets' + | 'actionTargetCount' + >, +) { + const { expected, policy, getCapturedAction, getState } = harness; + + if (config.control === 'no-gc') { + if (sample.actionCount !== 0) { + throw new Error( + `no-gc control expected actionCount 0, got ${sample.actionCount}`, + ); + } + if (sample.deletionCount !== 0) { + throw new Error( + `no-gc control expected deletionCount 0, got ${sample.deletionCount}`, + ); + } + return; + } + + if (sample.queueEntries !== expected.queueEntries) { + throw new Error( + `queueEntries ${sample.queueEntries} !== expected ${expected.queueEntries}`, + ); + } + if (sample.uniqueTargets !== expected.uniqueTargets) { + throw new Error( + `uniqueTargets ${sample.uniqueTargets} !== expected ${expected.uniqueTargets}`, + ); + } + + const actionTargetCount = + expected.expectedEntitiesInAction + expected.expectedEndpointsInAction; + if (sample.actionTargetCount !== actionTargetCount) { + throw new Error( + `actionTargetCount ${sample.actionTargetCount} !== expected ${actionTargetCount}`, + ); + } + + const action = getCapturedAction(); + if (!action || action.type !== GC) { + throw new Error('expected a GC action to be dispatched'); + } + if (action.entities.length !== expected.expectedEntitiesInAction) { + throw new Error( + `action.entities.length ${action.entities.length} !== ${expected.expectedEntitiesInAction}`, + ); + } + if (action.endpoints.length !== expected.expectedEndpointsInAction) { + throw new Error( + `action.endpoints.length ${action.endpoints.length} !== ${expected.expectedEndpointsInAction}`, + ); + } + if (config.pattern === 'duplicate') { + const unique = new Set(action.entities.map(p => `${p.key}:${p.pk}`)); + if (unique.size !== 1) { + throw new Error( + `duplicate pattern expected 1 unique entity path in action, got ${unique.size}`, + ); + } + } + + const expectedDeletions = + expected.expectedUniqueEntityDeletions + expected.expectedEndpointDeletions; + if (sample.deletionCount !== expectedDeletions) { + throw new Error( + `deletionCount ${sample.deletionCount} !== expected ${expectedDeletions}`, + ); + } + const remaining = countRemaining(getState(), config, expected); + if (remaining.entityRemaining !== 0 || remaining.endpointRemaining !== 0) { + throw new Error( + `expected empty GC targets after deletion; remaining entities=${remaining.entityRemaining} endpoints=${remaining.endpointRemaining}`, + ); + } + if (policy.queueEntries !== 0) { + throw new Error( + `expected empty queues after sweep, got ${policy.queueEntries}`, + ); + } +} + +interface Session { + config: GCScenarioConfig; + harness: Harness; + displayPeriodMs: number; +} + +let session: Session | null = null; + +export async function prepareGCScenario( + config: GCScenarioConfig, +): Promise { + if (session) { + session.harness.dispose(); + session = null; + } + + const harness = createHarness(config); + const displayPeriodMs = await measureDisplayPeriodMs(); + + session = { config, harness, displayPeriodMs }; + + return { + queueEntries: harness.expected.queueEntries, + uniqueTargets: harness.expected.uniqueTargets, + }; +} + +/** + * Interaction measurement via Chromium-calibrated probe: + * rAF registers the next rAF, then setTimeout(0) runs collection post-paint + * while a future frame is pending. See gcInteractionProbe.ts. + * `totalMs` is sweep/no-op only (scheduling excluded). + */ +export async function runGCScenario(): Promise { + if (!session) { + throw new Error( + 'prepareGCScenario() must be called before runGCScenario()', + ); + } + const { config, harness, displayPeriodMs } = session; + const { policy, expected } = harness; + + harness.clearCapturedAction(); + + let actionCount = 0; + let deletionCount = 0; + + const probe = await runChromiumInteractionProbe({ + displayPeriodMs, + work: () => { + if (config.control === 'gc') { + policy.sweep(); + const action = harness.getCapturedAction(); + actionCount = action && action.type === GC ? 1 : 0; + } + }, + }); + + // Deletion accounting outside timed work + if (config.control === 'gc') { + const remaining = countRemaining(harness.getState(), config, expected); + deletionCount = remaining.entityDeleted + remaining.endpointDeleted; + } + + const actionTargetCount = + config.control === 'gc' ? + expected.expectedEntitiesInAction + expected.expectedEndpointsInAction + : 0; + + const measurement: GCBrowserMeasurement = { + schemaVersion: 1, + totalMs: probe.totalMs, + sliceDurationsMs: config.control === 'gc' ? [probe.totalMs] : [], + actionCount, + queueEntries: expected.queueEntries, + uniqueTargets: expected.uniqueTargets, + actionTargetCount, + deletionCount, + timerDelayMs: probe.timerDelayMs, + frameIntervalsMs: probe.frameIntervalsMs, + displayPeriodMs: probe.displayPeriodMs, + missedFrames: probe.missedFrames, + maxInputDelayMs: probe.maxInputDelayMs, + longTaskCount: probe.longTaskCount, + longTaskTotalMs: probe.longTaskTotalMs, + }; + + validateMeasurement(config, harness, measurement); + + // Drop captured GC action arrays before Playwright heap snapshot; keep live store + harness.clearCapturedAction(); + + return measurement; +} + +/** + * Validation/calibration only: synthetic 40–50ms block through the same probe. + * Confirms blocking spans pending frame boundaries (missedFrames / large interval). + * Not used by GC timing scenarios. + */ +export async function calibrateGCFrameProbe(blockMs?: number): Promise<{ + blockMs: number; + totalMs: number; + timerDelayMs: number; + displayPeriodMs: number; + frameIntervalsMs: number[]; + frameIntervalMax: number; + missedFrames: number; + maxInputDelayMs: number; + spannedPendingFrame: boolean; +}> { + const displayPeriodMs = + session?.displayPeriodMs ?? (await measureDisplayPeriodMs()); + const ms = Math.min( + CALIBRATION_BLOCK_MS_MAX, + Math.max( + CALIBRATION_BLOCK_MS_MIN, + blockMs ?? (CALIBRATION_BLOCK_MS_MIN + CALIBRATION_BLOCK_MS_MAX) / 2, + ), + ); + + const probe = await runChromiumInteractionProbe({ + displayPeriodMs, + work: () => syntheticBlockMs(ms), + }); + + const frameIntervalMax = + probe.frameIntervalsMs.length > 0 ? Math.max(...probe.frameIntervalsMs) : 0; + const spannedPendingFrame = + probe.missedFrames >= 1 || frameIntervalMax >= displayPeriodMs * 1.5; + + return { + blockMs: ms, + totalMs: probe.totalMs, + timerDelayMs: probe.timerDelayMs, + displayPeriodMs: probe.displayPeriodMs, + frameIntervalsMs: probe.frameIntervalsMs, + frameIntervalMax, + missedFrames: probe.missedFrames, + maxInputDelayMs: probe.maxInputDelayMs, + spannedPendingFrame, + }; +} + +export function disposeGCScenario(): void { + if (!session) return; + session.harness.dispose(); + session = null; +} diff --git a/examples/benchmark-react/src/data-client/gcInteractionMetrics.ts b/examples/benchmark-react/src/data-client/gcInteractionMetrics.ts new file mode 100644 index 000000000000..aa88019e9436 --- /dev/null +++ b/examples/benchmark-react/src/data-client/gcInteractionMetrics.ts @@ -0,0 +1,169 @@ +/** + * Pure GC interaction metric helpers + stable scenario IDs. + * Deterministic — safe to unit-test without Playwright timing. + */ + +import type { GCScenarioConfig } from '@shared/types'; + +/** + * Count excess whole display periods beyond the single expected vsync gap. + * + * Uses nearest-period (Math.round) rather than floor: rAF timestamps jitter, so + * a true 2-period stall often measures slightly under 2×displayPeriodMs + * (e.g. 1.98×). floor() would report 0 missed frames; round() recovers the + * intended whole-period count. Sub-period noise around 1× still rounds to 1 + * (zero excess). + */ +export function excessMissedFrames( + frameIntervalsMs: number[], + displayPeriodMs: number, +): number { + if (!(displayPeriodMs > 0)) return 0; + let missed = 0; + for (const interval of frameIntervalsMs) { + const periods = Math.round(interval / displayPeriodMs); + missed += Math.max(0, periods - 1); + } + return missed; +} + +/** + * Responsiveness proxy from timer + frame probes (not pointer input). + * max(timerDelayMs, largest frame-interval excess over one display period). + */ +export function computeMaxInputDelayMs( + timerDelayMs: number, + frameIntervalsMs: number[], + displayPeriodMs: number, +): number { + let maxExcessFrame = 0; + if (displayPeriodMs > 0) { + for (const interval of frameIntervalsMs) { + maxExcessFrame = Math.max( + maxExcessFrame, + Math.max(0, interval - displayPeriodMs), + ); + } + } + return Math.max(timerDelayMs, maxExcessFrame); +} + +/** LongTask overlaps measurement window (entry may start just before windowStart). */ +export function longTaskOverlapsWindow( + entry: { startTime: number; duration: number }, + windowStart: number, + windowEnd: number, +): boolean { + return ( + entry.startTime < windowEnd && + entry.startTime + entry.duration > windowStart + ); +} + +/** Convert consecutive rAF timestamps into raw intervals. */ +export function frameIntervalsFromTimestamps(timestamps: number[]): number[] { + const intervals: number[] = []; + for (let i = 1; i < timestamps.length; i++) { + intervals.push(timestamps[i] - timestamps[i - 1]); + } + return intervals; +} + +export function median(values: number[]): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 ? + (sorted[mid - 1]! + sorted[mid]!) / 2 + : sorted[mid]!; +} + +/** Quiet rAF intervals → display period (never hardcode 16.67). */ +export async function measureDisplayPeriodMs(samples = 8): Promise { + const intervals: number[] = []; + await new Promise(resolve => { + let last = 0; + let n = 0; + const frame = (now: number) => { + if (n > 0) intervals.push(now - last); + last = now; + n++; + if (n <= samples) requestAnimationFrame(frame); + else resolve(); + }; + requestAnimationFrame(frame); + }); + return median(intervals); +} + +/** Canonical GC counts (must match `GC_CANONICAL_COUNTS` in bench/scenarios.ts). */ +const CANONICAL_COUNTS: readonly number[] = [1_000, 10_000, 100_000]; + +/** + * Stable detailed-report scenario ID from validated axes (not display names). + * Format: browser/{kind}/{pattern}/{count}/end-to-end/{control} + */ +export function browserGCScenarioId( + axes: Pick< + GCScenarioConfig, + 'candidateKind' | 'pattern' | 'count' | 'control' + >, +): string { + const { candidateKind, pattern, count, control } = axes; + if ( + candidateKind !== 'entity' && + candidateKind !== 'endpoint' && + candidateKind !== 'mixed' + ) { + throw new Error(`invalid candidateKind: ${candidateKind}`); + } + if (pattern !== 'unique' && pattern !== 'duplicate') { + throw new Error(`invalid pattern: ${pattern}`); + } + if (pattern === 'duplicate' && candidateKind !== 'entity') { + throw new Error('duplicate pattern requires candidateKind=entity'); + } + if (!Number.isInteger(count) || !CANONICAL_COUNTS.includes(count)) { + throw new Error( + `invalid count: ${count}; expected one of ${CANONICAL_COUNTS.join('|')}`, + ); + } + if (control !== 'gc' && control !== 'no-gc') { + throw new Error(`invalid control: ${control}`); + } + return `browser/${candidateKind}/${pattern}/${count}/end-to-end/${control}`; +} + +/** Parse a stable ID back to axes (throws on malformed). */ +export function parseBrowserGCScenarioId(id: string): { + platform: 'browser'; + candidateKind: GCScenarioConfig['candidateKind']; + pattern: GCScenarioConfig['pattern']; + count: number; + mode: 'end-to-end'; + control: GCScenarioConfig['control']; +} { + const parts = id.split('/'); + if ( + parts.length !== 6 || + parts[0] !== 'browser' || + parts[4] !== 'end-to-end' + ) { + throw new Error(`malformed browser GC scenario id: ${id}`); + } + const axes = { + candidateKind: parts[1] as GCScenarioConfig['candidateKind'], + pattern: parts[2] as GCScenarioConfig['pattern'], + count: Number(parts[3]), + control: parts[5] as GCScenarioConfig['control'], + }; + // Re-validate via builder (rejects invalid axis combos) + if (browserGCScenarioId(axes) !== id) { + throw new Error(`browser GC scenario id failed revalidation: ${id}`); + } + return { + platform: 'browser', + ...axes, + mode: 'end-to-end', + }; +} diff --git a/examples/benchmark-react/src/data-client/gcInteractionProbe.ts b/examples/benchmark-react/src/data-client/gcInteractionProbe.ts new file mode 100644 index 000000000000..c572a7ec4268 --- /dev/null +++ b/examples/benchmark-react/src/data-client/gcInteractionProbe.ts @@ -0,0 +1,235 @@ +/** + * Chromium-calibrated interaction probe for browser GC measurement. + * + * Ordering (Chromium-specific calibration — NOT a web platform guarantee): + * 1. In an rAF callback, register the *next* rAF first (future frame pending). + * 2. Then post setTimeout(0) so the collection/work task runs after paint. + * 3. Synchronous work in that timer therefore blocks while a frame is pending, + * so wall-clock frame intervals can observe the stalled vsync gap. + * + * `totalMs` times only `work()` — scheduling overhead is excluded. + * If `work()` throws, the probe rejects promptly, stops the rAF/timer chain, + * and disconnects the PerformanceObserver (no further mutations). + */ +import { + computeMaxInputDelayMs, + excessMissedFrames, + frameIntervalsFromTimestamps, + longTaskOverlapsWindow, +} from './gcInteractionMetrics'; + +export interface InteractionProbeResult { + totalMs: number; + timerDelayMs: number; + frameIntervalsMs: number[]; + displayPeriodMs: number; + missedFrames: number; + maxInputDelayMs: number; + longTaskCount: number; + longTaskTotalMs: number; +} + +const POST_FRAMES = 6; + +function drainAndDisconnectObserver( + observer: PerformanceObserver | null, + longTasks: { startTime: number; duration: number }[], +): void { + if (!observer) return; + try { + for (const entry of observer.takeRecords()) { + longTasks.push({ + startTime: entry.startTime, + duration: entry.duration, + }); + } + } catch { + // takeRecords unavailable + } + try { + observer.disconnect(); + } catch { + // ignore + } +} + +/** + * Run one Chromium-calibrated probe. `work` is the only timed body. + * Rejects if `work()` throws; settles exactly once. + */ +export async function runChromiumInteractionProbe(opts: { + displayPeriodMs: number; + work: () => void; +}): Promise { + const { displayPeriodMs, work } = opts; + + type LongTaskEntry = { startTime: number; duration: number }; + const longTasks: LongTaskEntry[] = []; + let observer: PerformanceObserver | null = null; + if (typeof PerformanceObserver !== 'undefined') { + try { + observer = new PerformanceObserver(list => { + for (const entry of list.getEntries()) { + longTasks.push({ + startTime: entry.startTime, + duration: entry.duration, + }); + } + }); + observer.observe({ + type: 'longtask', + buffered: false, + } as PerformanceObserverInit); + } catch { + observer = null; + } + } + + const frameTimestamps: number[] = []; + let timerDelayMs = 0; + let totalMs = 0; + let windowStart = 0; + let windowEnd = 0; + + try { + await new Promise((resolve, reject) => { + let armed = false; + let collectionDone = false; + let postRemaining = POST_FRAMES; + /** Once true, no callback may mutate probe state or schedule further work. */ + let closed = false; + + let rafHandle: number | null = null; + let collectionTimer: ReturnType | null = null; + let delayProbeTimer: ReturnType | null = null; + let settleTimer: ReturnType | null = null; + + const clearPendingTimers = () => { + if (rafHandle != null && typeof cancelAnimationFrame === 'function') { + cancelAnimationFrame(rafHandle); + rafHandle = null; + } + if (collectionTimer != null) { + clearTimeout(collectionTimer); + collectionTimer = null; + } + if (delayProbeTimer != null) { + clearTimeout(delayProbeTimer); + delayProbeTimer = null; + } + if (settleTimer != null) { + clearTimeout(settleTimer); + settleTimer = null; + } + }; + + const settleOnce = (err?: unknown) => { + if (closed) return; + closed = true; + clearPendingTimers(); + if (err !== undefined) + reject(err instanceof Error ? err : new Error(String(err))); + else resolve(); + }; + + const scheduleFrame = () => { + if (closed) return; + rafHandle = requestAnimationFrame(onFrame); + }; + + const onFrame = (rafNow: number) => { + rafHandle = null; + if (closed) return; + + frameTimestamps.push(rafNow); + + if (!armed) { + armed = true; + // 1) Future frame pending BEFORE collection is posted. + scheduleFrame(); + // 2) Collection as macrotask — Chromium typically runs this post-paint. + collectionTimer = setTimeout(() => { + collectionTimer = null; + if (closed) return; + + windowStart = performance.now(); + const beforeProbe = performance.now(); + // Responsiveness probe: delay from immediately before timed work. + delayProbeTimer = setTimeout(() => { + delayProbeTimer = null; + if (closed) return; + timerDelayMs = performance.now() - beforeProbe; + }, 0); + + try { + const t0 = performance.now(); + work(); + totalMs = performance.now() - t0; + collectionDone = true; + } catch (err) { + settleOnce(err); + } + }, 0); + return; + } + + if (!collectionDone) { + // Keep the chain alive until collection finishes (blocked frames). + scheduleFrame(); + return; + } + + postRemaining--; + if (postRemaining > 0) { + scheduleFrame(); + return; + } + + windowEnd = performance.now(); + // Settle the responsiveness setTimeout(0) probe. + settleTimer = setTimeout(() => { + settleTimer = null; + if (closed) return; + settleOnce(); + }, 0); + }; + + scheduleFrame(); + }); + } finally { + // Resolve and reject both land here — drain + disconnect observer. + drainAndDisconnectObserver(observer, longTasks); + observer = null; + } + + const frameIntervalsMs = frameIntervalsFromTimestamps(frameTimestamps); + const longTasksInWindow = longTasks.filter(t => + longTaskOverlapsWindow(t, windowStart, windowEnd), + ); + + return { + totalMs, + timerDelayMs, + frameIntervalsMs, + displayPeriodMs, + missedFrames: excessMissedFrames(frameIntervalsMs, displayPeriodMs), + maxInputDelayMs: computeMaxInputDelayMs( + timerDelayMs, + frameIntervalsMs, + displayPeriodMs, + ), + longTaskCount: longTasksInWindow.length, + longTaskTotalMs: longTasksInWindow.reduce((s, t) => s + t.duration, 0), + }; +} + +/** Busy-wait for calibration only (not used in GC timing scenarios). */ +export function syntheticBlockMs(ms: number): void { + const end = performance.now() + ms; + while (performance.now() < end) { + // spin + } +} + +export const CALIBRATION_BLOCK_MS_MIN = 40; +export const CALIBRATION_BLOCK_MS_MAX = 50; diff --git a/examples/benchmark-react/src/data-client/index.tsx b/examples/benchmark-react/src/data-client/index.tsx index cf1af16ad3af..8688185cce62 100644 --- a/examples/benchmark-react/src/data-client/index.tsx +++ b/examples/benchmark-react/src/data-client/index.tsx @@ -30,11 +30,24 @@ import { sortedIssuesEndpoint, } from '@shared/resources'; import { patchIssue } from '@shared/server'; -import type { Issue } from '@shared/types'; +import type { GCScenarioConfig, Issue } from '@shared/types'; import React, { useCallback, useRef } from 'react'; let mutationCounter = 0; +/** Lazily loaded GC harness — never statically imported (bundle fairness). */ +type GCHarnessModule = typeof import('./gcBrowserHarness'); +let gcHarnessModule: GCHarnessModule | null = null; + +async function loadGCHarness(): Promise { + if (!gcHarnessModule) { + gcHarnessModule = await import( + /* webpackChunkName: "gc-browser-harness" */ './gcBrowserHarness' + ); + } + return gcHarnessModule; +} + /** GCPolicy with no interval (won't fire during timing scenarios) and instant * expiry so an explicit sweep() collects all unreferenced data immediately. */ class BenchGCPolicy extends GCPolicy { @@ -300,6 +313,25 @@ function BenchmarkHarness() { moveItem, triggerGC: () => benchGC.sweep(), resetStore, + prepareGCScenario: async (config: GCScenarioConfig) => { + const mod = await loadGCHarness(); + return mod.prepareGCScenario(config); + }, + runGCScenario: () => { + if (!gcHarnessModule) { + throw new Error( + 'prepareGCScenario() must load the GC harness before runGCScenario()', + ); + } + return gcHarnessModule.runGCScenario(); + }, + disposeGCScenario: () => { + gcHarnessModule?.disposeGCScenario(); + }, + calibrateGCFrameProbe: async (blockMs?: number) => { + const mod = await loadGCHarness(); + return mod.calibrateGCFrameProbe(blockMs); + }, }); return ( diff --git a/examples/benchmark-react/src/shared/types.ts b/examples/benchmark-react/src/shared/types.ts index bdde003695d8..da646eecf5eb 100644 --- a/examples/benchmark-react/src/shared/types.ts +++ b/examples/benchmark-react/src/shared/types.ts @@ -9,6 +9,56 @@ export interface RefStabilityReport { userRefChanged: number; } +/** Axes for browser GC scenarios (local mirror of the shared GC vocabulary). */ +export interface GCScenarioConfig { + candidateKind: 'entity' | 'endpoint' | 'mixed'; + /** `duplicate` is entity-only (one path released `count` times). */ + pattern: 'unique' | 'duplicate'; + /** Canonical counts: 1000 | 10000 | 100000 */ + count: number; + control: 'gc' | 'no-gc'; +} + +/** Queue cardinality after prepare (fixtures + createCountRef outside timing). */ +export interface GCPreparedSummary { + queueEntries: number; + uniqueTargets: number; +} + +/** + * Page-side GC interaction measurement (schemaVersion 1). + * Heap fields are attached by the Playwright runner, not page code. + */ +export interface GCBrowserMeasurement { + schemaVersion: 1; + totalMs: number; + /** Monolithic baseline: exactly `[totalMs]` when control is `gc`; empty for `no-gc`. */ + sliceDurationsMs: number[]; + actionCount: number; + queueEntries: number; + uniqueTargets: number; + actionTargetCount: number; + deletionCount: number; + /** Actual setTimeout(0) callback delay from immediately before collection. */ + timerDelayMs: number; + /** Raw rAF intervals around collection. */ + frameIntervalsMs: number[]; + /** Quiet-frame period measured during prepare (never hardcode 16.67). */ + displayPeriodMs: number; + /** + * Excess whole frame periods vs displayPeriodMs (nearest-period; see harness). + */ + missedFrames: number; + /** + * Responsiveness proxy: max(timerDelayMs, max excess of any frame interval + * over one displayPeriodMs). Not pointer/input-event latency — no synthetic + * pointer events are generated. + */ + maxInputDelayMs: number; + longTaskCount: number; + longTaskTotalMs: number; +} + /** * Benchmark API interface exposed by each library app on window.__BENCH__ */ @@ -57,6 +107,34 @@ export interface BenchAPI { setRenderLimit?(n: number | undefined): void; /** Clear client-side cache/store so the next mount triggers a fresh fetch. Called between sub-iterations for mount scenarios. */ resetStore?(): void; + /** + * Prepare isolated browser GC fixtures + queues (untimed). data-client only. + * Also measures quiet displayPeriodMs. Does not start interaction timing. + * Dynamically loads the GC harness chunk during prepare (before timing). + */ + prepareGCScenario?(config: GCScenarioConfig): Promise; + /** + * Run explicit monolithic cache GC (or no-gc control) with interaction probes. + * Resolves only after timer + rAF probes settle. data-client only. + */ + runGCScenario?(): Promise; + /** Tear down the isolated GC harness (store + policy). data-client only. */ + disposeGCScenario?(): void; + /** + * Validation/calibration only: synthetic ~40–50ms block through the Chromium + * frame probe. Confirms blocking spans pending frame boundaries. + */ + calibrateGCFrameProbe?(blockMs?: number): Promise<{ + blockMs: number; + totalMs: number; + timerDelayMs: number; + displayPeriodMs: number; + frameIntervalsMs: number[]; + frameIntervalMax: number; + missedFrames: number; + maxInputDelayMs: number; + spannedPendingFrame: boolean; + }>; } declare global { @@ -126,13 +204,10 @@ export type ScenarioAction = | { action: 'moveItem'; args: [number] }; export type ResultMetric = - | 'duration' - | 'issueRefChanged' - | 'userRefChanged' - | 'heapDelta'; + 'duration' | 'issueRefChanged' | 'userRefChanged' | 'heapDelta' | 'totalMs'; -/** hotPath = JS only, included in CI. memory = heap delta, not CI. startup = page load metrics, not CI. */ -export type ScenarioCategory = 'hotPath' | 'memory' | 'startup'; +/** hotPath = JS only, included in CI. memory = heap delta, not CI. startup = page load metrics, not CI. gc = cache GC interaction, not CI. */ +export type ScenarioCategory = 'hotPath' | 'memory' | 'startup' | 'gc'; /** small = cheap scenarios (full warmup + measurement). large = expensive scenarios (reduced runs). */ export type ScenarioSize = 'small' | 'large'; @@ -141,9 +216,9 @@ export interface Scenario { name: string; action: keyof BenchAPI; args: unknown[]; - /** Which value to report; default 'duration'. Ref-stability use issueRefChanged/userRefChanged; memory use heapDelta. */ + /** Which value to report; default 'duration'. Ref-stability use issueRefChanged/userRefChanged; memory use heapDelta; gc use totalMs. */ resultMetric?: ResultMetric; - /** hotPath (default) = run in CI. memory = heap delta. startup = page load metrics. */ + /** hotPath (default) = run in CI. memory = heap delta. startup = page load metrics. gc = cache GC (opt-in). */ category?: ScenarioCategory; /** small (default) = full runs. large = reduced warmup/measurement for expensive scenarios. */ size?: ScenarioSize; diff --git a/examples/benchmark/README.md b/examples/benchmark/README.md index 760c3ed276c2..a7d612a69505 100644 --- a/examples/benchmark/README.md +++ b/examples/benchmark/README.md @@ -21,13 +21,14 @@ yarn workspace example-benchmark start [suite-name] [filter] ``` Both arguments are optional: + - **No arguments**: runs `normalizr` + `core` suites with all benchmarks - **Suite only**: `yarn start normalizr` runs all benchmarks in that suite - **Suite + filter**: `yarn start normalizr denormalize` runs only benchmarks containing "denormalize" #### Filter syntax -- `text` → substring match (contains "text") +- `text` → substring match (contains) - `^text` → starts with "text" #### Suites @@ -78,6 +79,107 @@ counts (minor/major/other) and total GC pause during the loop, and retained heap after a full GC (should be ~0; nonzero indicates actual retention). Local-only — not tracked in CI. +### GC policy (isolated Node baseline) + +Measures the current monolithic cache GC (`GCPolicy.runSweep` + GC reducer) +under a timerless `BenchmarkGCPolicy` subclass: `expiresAt` always zero, `init` +only stores the controller (no intervals), `cleanup` is a no-op, and a public +`sweep()` calls protected `runSweep` once. This is **data-client cache GC**, not +V8/engine GC — do not conflate the two. + +```bash +yarn build:benchmark +yarn workspace example-benchmark start:gc [filter] [--samples=N] [--memory] [--table|--no-table] +yarn workspace example-benchmark start:gc --verify-manifest +# or: yarn workspace example-benchmark start:gc:verify +``` + +Every `yarn build:benchmark` / workspace `build` runs webpack then writes +`dist/gc-build-manifest.json` (BuildManifest v1: `schemaVersion`, `buildId`, +`commit`, `dirty`, `sourceDigest`, `artifacts`). `sourceDigest` hashes sorted +relevant inputs on disk (`packages/core/src/**`, GC harness/runner/config/ +package files), so dirty or untracked relevant files change it. `artifacts` +maps each emitted `dist/*` file (except the manifest) to sha256. `buildId` is +the digest of those canonical fields excluding itself. + +`start:gc` **verifies** the manifest before scenarios: recomputes the current +source digest and every dist artifact hash, and rejects stale/missing/tampered +builds with a rebuild instruction. Report `build` provenance comes from the +**verified manifest**, never from live `git rev-parse` at run time. Manifest +generation and verification are outside timed cache-GC work. + +`--verify-manifest` / `start:gc:verify` self-tests that source and artifact +tampering are rejected and restores all files afterward. + +Examples: + +```bash +# All 1k scenarios (substring filter); JSON on stdout +yarn workspace example-benchmark start:gc /1000/ --samples=5 + +# Single 100k entity scan without building unrelated fixtures +yarn workspace example-benchmark start:gc node/entity/unique/100000/scan/gc --samples=3 + +# Duplicate entity queue amplification baseline +yarn workspace example-benchmark start:gc entity/duplicate/1000 + +# Capture JSON only (no stderr table) +yarn workspace example-benchmark start:gc /1000/scan/gc --no-table > gc-report.json +``` + +Filter syntax matches the other suites (`text` substring, `^text` prefix) against +stable scenario IDs: + +`node/{entity|endpoint|mixed}/{unique|duplicate}/{1000|10000|100000}/{scan|reducer|end-to-end}/{gc|no-gc}` + +| Axis | Values | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `candidateKind` | `entity`, `endpoint`, `mixed` (mixed splits total count across entity+endpoint) | +| `pattern` | `unique` (distinct keys/paths); `duplicate` (one entity path released `count` times; entity only) | +| `count` | Canonical `1000`, `10000`, `100000` | +| `mode` | `scan` (dispatch captures action, no reduce), `reducer` (prebuilt GC action through reducer), `end-to-end` (sweep dispatch reduces synchronously) | +| `control` | `gc` or `no-gc` (harness/no-op overhead) | + +**Setup / timing boundaries** + +1. **Prepare** (untimed) — build deterministic fixture state (`entities`/`entitiesMeta` or `endpoints`/`meta`), queue candidates via `createCountRef` mount/release. `reducer` mode prebuilds a GC action and clones state (seed dropped before timing/heap baseline). `scan` / `end-to-end` do **not** retain parallel 100k target arrays. +2. **Timed cache-GC** — `sweep()`, `reducer(state, action)`, or empty `no-op` body only. No per-candidate marks/logging inside this window. +3. **Optional retained-heap (`--memory`)** — after timed work + validation, drop observer refs (captured GC action, prebuilt action) while **keeping the live store**; then settle + forced V8 GC. Engine GC pauses are never folded into `totalMs`. + +Each destructive sample (`reducer` / `end-to-end`) gets a fresh fixture. Default +`--samples=11` (CLI-overridable). Fixtures for unmatched counts are never built, +so a filtered `100000` run does not construct 1k/10k stores. + +**Metrics** (JSON `schemaVersion: 1` on stdout) + +- `totalMs` — timed cache-GC wall time +- `sliceDurationsMs` — current monolith is **one slice** (honest representation; not fabricated per-candidate samples) +- `actionCount`, `queueEntries`, `uniqueTargets`, `actionTargetCount`, `deletionCount` +- For `duplicate`: queue/action may list the same entity path repeatedly; `uniqueTargets` / final `deletionCount` stay `1` +- Summary: median / min / max / p95 / p99 where meaningful; units are explicit in the report + +Every accepted sample is self-validated (queue cardinality, action target shape, +final deletions for reducer/end-to-end). Invalid fixtures/results throw. + +**`--memory` semantics (keep-store / drop-observer)** + +`heapDeltaBytes` is **not** “drop the whole harness and see what V8 frees.” That +would free the fixture for both `gc` and `no-gc` and hide cache deletion. +Instead: + +1. `heapBefore` — store + queues retained (reducer seed already dropped); forced V8 GC +2. timed cache-GC (or `no-gc` no-op) +3. validate, then release observer-only refs (captured action target arrays, prebuilt action) +4. `heapAfter` — **live store still retained**; settle + forced V8 GC + +Interpretation: compare `gc` vs `no-gc` on `reducer` / `end-to-end` — `gc` should +show a more negative (or smaller) retained heap after deleting cache entries. +`scan` does not mutate the store, so its heap delta is not a retained-cache signal. +Report includes `memorySemantics` documenting this model. Still separate from +engine GC timing. + +Local calibration harness only — not claimed as CI-integrated. + ### Profiling For opt/deopt investigation: diff --git a/examples/benchmark/gc-build-manifest.js b/examples/benchmark/gc-build-manifest.js new file mode 100644 index 000000000000..d1a9c5877d90 --- /dev/null +++ b/examples/benchmark/gc-build-manifest.js @@ -0,0 +1,502 @@ +/** + * BuildManifest v1 for the Node GC harness. + * + * Generated after webpack (`yarn build` / `yarn build:benchmark`) as + * `dist/gc-build-manifest.json`. `start:gc` verifies source + artifact digests + * before any scenario work and reports provenance from the verified manifest + * (never current HEAD alone). + * + * Usage: + * node ./gc-build-manifest.js write + * node ./gc-build-manifest.js verify + * node ./gc-build-manifest.js self-test + */ +import { Buffer } from 'node:buffer'; +import { execFileSync, execSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { + existsSync, + readdirSync, + readFileSync, + writeFileSync, + unlinkSync, + statSync, +} from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const BENCH_ROOT = __dirname; +const REPO_ROOT = path.resolve(BENCH_ROOT, '../..'); + +export const MANIFEST_SCHEMA_VERSION = 1; +export const MANIFEST_RELATIVE = 'dist/gc-build-manifest.json'; +export const MANIFEST_PATH = path.join(BENCH_ROOT, MANIFEST_RELATIVE); + +/** Benchmark harness / runner / config inputs (relative to repo root). */ +const BENCH_SOURCE_FILES = [ + 'examples/benchmark/gc-build-manifest.js', + 'examples/benchmark/gc-policy.js', + 'examples/benchmark/gc-policy-scenarios.js', + 'examples/benchmark/filter.js', + 'examples/benchmark/package.json', + 'examples/benchmark/webpack.config.cjs', + 'examples/benchmark/src/index.ts', + 'examples/benchmark/tsconfig.json', +]; + +const CORE_SRC_DIR = 'packages/core/src'; + +function sha256Buffer(buf) { + return createHash('sha256').update(buf).digest('hex'); +} + +function sha256Text(text) { + return sha256Buffer(Buffer.from(text, 'utf8')); +} + +function toPosix(p) { + return p.split(path.sep).join('/'); +} + +/** Walk a directory; return repo-relative posix paths (files only), sorted. */ +function walkFiles(absDir, repoRelDir) { + const out = []; + if (!existsSync(absDir)) return out; + const entries = readdirSync(absDir, { withFileTypes: true }); + for (const ent of entries) { + const abs = path.join(absDir, ent.name); + const rel = toPosix(path.join(repoRelDir, ent.name)); + if (ent.isDirectory()) { + out.push(...walkFiles(abs, rel)); + } else if (ent.isFile()) { + out.push(rel); + } + } + return out; +} + +/** + * Sorted list of relevant source paths (repo-relative posix). + * Includes untracked files under packages/core/src when present on disk. + */ +export function listRelevantSourcePaths() { + const coreFiles = walkFiles(path.join(REPO_ROOT, CORE_SRC_DIR), CORE_SRC_DIR); + const benchFiles = BENCH_SOURCE_FILES.filter(rel => + existsSync(path.join(REPO_ROOT, rel)), + ); + return [...new Set([...coreFiles, ...benchFiles])].sort((a, b) => + a.localeCompare(b), + ); +} + +/** + * Canonical source digest over sorted path→content (filesystem, not git blobs). + * Dirty/untracked relevant files change this digest. + */ +export function computeSourceDigest(paths = listRelevantSourcePaths()) { + const hash = createHash('sha256'); + for (const rel of paths) { + const abs = path.join(REPO_ROOT, rel); + if (!existsSync(abs) || !statSync(abs).isFile()) { + throw new Error(`relevant source missing: ${rel}`); + } + hash.update(rel); + hash.update('\0'); + hash.update(readFileSync(abs)); + hash.update('\0'); + } + return hash.digest('hex'); +} + +/** Dist artifact paths relative to examples/benchmark, excluding the manifest. */ +export function listDistArtifactPaths() { + const distDir = path.join(BENCH_ROOT, 'dist'); + if (!existsSync(distDir)) return []; + const out = []; + for (const name of readdirSync(distDir)) { + if (name === 'gc-build-manifest.json') continue; + const abs = path.join(distDir, name); + if (statSync(abs).isFile()) { + out.push(toPosix(path.join('dist', name))); + } + } + return out.sort((a, b) => a.localeCompare(b)); +} + +export function hashDistArtifacts(paths = listDistArtifactPaths()) { + /** @type {Record} */ + const artifacts = {}; + for (const rel of paths) { + const abs = path.join(BENCH_ROOT, rel); + if (!existsSync(abs)) { + throw new Error(`dist artifact missing: ${rel}`); + } + artifacts[rel] = sha256Buffer(readFileSync(abs)); + } + return artifacts; +} + +export function gitCommitFull() { + try { + return execSync('git rev-parse HEAD', { + cwd: REPO_ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + return 'unknown'; + } +} + +/** + * dirty=true when any relevant path is modified, added, or untracked vs git. + */ +export function computeDirty(paths = listRelevantSourcePaths()) { + try { + const pathspecs = [CORE_SRC_DIR, ...BENCH_SOURCE_FILES]; + const out = execFileSync( + 'git', + ['status', '--porcelain', '-u', '--', ...pathspecs], + { + cwd: REPO_ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }, + ).trim(); + if (!out) return false; + const relevant = new Set(paths); + for (const line of out.split('\n')) { + if (!line) continue; + const rest = line.slice(3); + const filePath = rest.includes(' -> ') ? rest.split(' -> ').pop() : rest; + const norm = toPosix(filePath.replace(/^"|"$/g, '')); + if ( + relevant.has(norm) || + norm.startsWith(`${CORE_SRC_DIR}/`) || + BENCH_SOURCE_FILES.includes(norm) + ) { + return true; + } + } + return false; + } catch { + return true; + } +} + +/** + * buildId = digest of canonical manifest fields excluding buildId itself. + */ +export function computeBuildId({ + schemaVersion, + commit, + dirty, + sourceDigest, + artifacts, +}) { + const sortedArtifacts = Object.fromEntries( + Object.entries(artifacts).sort(([a], [b]) => a.localeCompare(b)), + ); + const canonical = JSON.stringify({ + schemaVersion, + commit, + dirty, + sourceDigest, + artifacts: sortedArtifacts, + }); + return sha256Text(canonical); +} + +export function buildManifest() { + const paths = listRelevantSourcePaths(); + const sourceDigest = computeSourceDigest(paths); + const artifacts = hashDistArtifacts(); + if (Object.keys(artifacts).length === 0) { + throw new Error( + 'no dist artifacts found; run webpack before writing the GC build manifest', + ); + } + const commit = gitCommitFull(); + const dirty = computeDirty(paths); + const body = { + schemaVersion: MANIFEST_SCHEMA_VERSION, + commit, + dirty, + sourceDigest, + artifacts, + }; + const buildId = computeBuildId(body); + return { + schemaVersion: MANIFEST_SCHEMA_VERSION, + buildId, + commit, + dirty, + sourceDigest, + artifacts, + }; +} + +export function writeManifest(manifest = buildManifest()) { + writeFileSync( + MANIFEST_PATH, + `${JSON.stringify(manifest, null, 2)}\n`, + 'utf8', + ); + return manifest; +} + +export function readManifest(manifestPath = MANIFEST_PATH) { + if (!existsSync(manifestPath)) { + return null; + } + return JSON.parse(readFileSync(manifestPath, 'utf8')); +} + +const REBUILD_HINT = + 'Rebuild with: yarn build:benchmark (or: yarn workspace example-benchmark run build)'; + +/** + * Recompute current source digest + dist artifact hashes and compare to manifest. + * @returns {{ ok: true, manifest: object } | { ok: false, reason: string, details?: string[] }} + */ +export function verifyManifest(manifestPath = MANIFEST_PATH) { + const details = []; + if (!existsSync(manifestPath)) { + return { + ok: false, + reason: `missing build manifest at ${MANIFEST_RELATIVE}`, + details: [REBUILD_HINT], + }; + } + + let manifest; + try { + manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + } catch (err) { + return { + ok: false, + reason: `unreadable build manifest: ${err.message}`, + details: [REBUILD_HINT], + }; + } + + if (manifest.schemaVersion !== MANIFEST_SCHEMA_VERSION) { + return { + ok: false, + reason: `unsupported manifest schemaVersion ${manifest.schemaVersion} (expected ${MANIFEST_SCHEMA_VERSION})`, + details: [REBUILD_HINT], + }; + } + + const expectedBuildId = computeBuildId({ + schemaVersion: manifest.schemaVersion, + commit: manifest.commit, + dirty: manifest.dirty, + sourceDigest: manifest.sourceDigest, + artifacts: manifest.artifacts ?? {}, + }); + if (manifest.buildId !== expectedBuildId) { + details.push( + `buildId mismatch (manifest tampered or corrupt): got ${manifest.buildId}, expected ${expectedBuildId}`, + ); + } + + let currentSourceDigest; + try { + currentSourceDigest = computeSourceDigest(); + } catch (err) { + return { + ok: false, + reason: err.message, + details: [REBUILD_HINT], + }; + } + if (currentSourceDigest !== manifest.sourceDigest) { + details.push( + `sourceDigest stale: manifest ${manifest.sourceDigest.slice(0, 12)}… vs current ${currentSourceDigest.slice(0, 12)}… (relevant sources changed since build)`, + ); + } + + const currentArtifacts = hashDistArtifacts(); + const manifestArts = manifest.artifacts ?? {}; + const allKeys = new Set([ + ...Object.keys(manifestArts), + ...Object.keys(currentArtifacts), + ]); + for (const key of [...allKeys].sort()) { + if (!(key in manifestArts)) { + details.push(`unexpected dist artifact not in manifest: ${key}`); + } else if (!(key in currentArtifacts)) { + details.push(`missing dist artifact listed in manifest: ${key}`); + } else if (manifestArts[key] !== currentArtifacts[key]) { + details.push( + `dist artifact hash mismatch: ${key} (tampered or stale webpack output)`, + ); + } + } + + if (details.length) { + return { + ok: false, + reason: 'GC build provenance verification failed', + details: [...details, REBUILD_HINT], + }; + } + + return { ok: true, manifest }; +} + +export function verifyManifestOrThrow(manifestPath = MANIFEST_PATH) { + const result = verifyManifest(manifestPath); + if (!result.ok) { + const msg = [result.reason, ...(result.details ?? [])].join('\n '); + throw new Error(msg); + } + return result.manifest; +} + +/** + * Prove source + artifact tampering rejects, restoring all files afterward. + */ +export function runSelfTest() { + const steps = []; + const baseline = verifyManifest(); + if (!baseline.ok) { + throw new Error( + `self-test requires a valid build first:\n ${baseline.reason}\n ${REBUILD_HINT}`, + ); + } + steps.push('baseline verify: ok'); + + // --- artifact tamper --- + const artifactRel = Object.keys(baseline.manifest.artifacts)[0]; + if (!artifactRel) throw new Error('self-test: no artifacts in manifest'); + const artifactAbs = path.join(BENCH_ROOT, artifactRel); + const artifactOrig = readFileSync(artifactAbs); + try { + writeFileSync( + artifactAbs, + Buffer.concat([artifactOrig, Buffer.from('\n')]), + ); + const afterArt = verifyManifest(); + if (afterArt.ok) { + throw new Error('self-test: expected artifact tampering to be rejected'); + } + steps.push( + `artifact tamper rejected: ${afterArt.details?.[0] ?? afterArt.reason}`, + ); + } finally { + writeFileSync(artifactAbs, artifactOrig); + } + const restoredArt = verifyManifest(); + if (!restoredArt.ok) { + throw new Error( + `self-test: artifact restore failed verify: ${restoredArt.reason}`, + ); + } + steps.push('artifact restore: ok'); + + // --- source tamper via temporary untracked file under packages/core/src --- + const tmpRel = `${CORE_SRC_DIR}/.__gc_manifest_selftest__.tmp`; + const tmpAbs = path.join(REPO_ROOT, tmpRel); + try { + writeFileSync(tmpAbs, '// gc-build-manifest self-test — safe to delete\n'); + const afterSrc = verifyManifest(); + if (afterSrc.ok) { + throw new Error('self-test: expected source tampering to be rejected'); + } + steps.push( + `source tamper rejected: ${afterSrc.details?.[0] ?? afterSrc.reason}`, + ); + } finally { + if (existsSync(tmpAbs)) unlinkSync(tmpAbs); + } + const restoredSrc = verifyManifest(); + if (!restoredSrc.ok) { + throw new Error( + `self-test: source restore failed verify: ${restoredSrc.reason}`, + ); + } + steps.push('source restore: ok'); + + // --- manifest buildId tamper --- + const manifestOrig = readFileSync(MANIFEST_PATH); + try { + const mangled = JSON.parse(manifestOrig.toString('utf8')); + mangled.buildId = '0'.repeat(64); + writeFileSync(MANIFEST_PATH, `${JSON.stringify(mangled, null, 2)}\n`); + const afterMan = verifyManifest(); + if (afterMan.ok) { + throw new Error( + 'self-test: expected manifest buildId tamper to be rejected', + ); + } + steps.push( + `manifest tamper rejected: ${afterMan.details?.[0] ?? afterMan.reason}`, + ); + } finally { + writeFileSync(MANIFEST_PATH, manifestOrig); + } + const final = verifyManifest(); + if (!final.ok) { + throw new Error(`self-test: final verify failed: ${final.reason}`); + } + steps.push('final verify: ok'); + + return { ok: true, steps, manifest: final.manifest }; +} + +function printVerifyFailure(result) { + console.error(result.reason); + for (const d of result.details ?? []) { + console.error(` ${d}`); + } +} + +function main(argv) { + const cmd = argv[0] ?? 'write'; + if (cmd === 'write') { + const manifest = writeManifest(); + console.error( + `wrote ${MANIFEST_RELATIVE} buildId=${manifest.buildId.slice(0, 12)}… commit=${manifest.commit.slice(0, 12)}… dirty=${manifest.dirty} artifacts=${Object.keys(manifest.artifacts).length}`, + ); + return; + } + if (cmd === 'verify') { + const result = verifyManifest(); + if (!result.ok) { + printVerifyFailure(result); + process.exitCode = 1; + return; + } + console.error( + `GC build manifest OK buildId=${result.manifest.buildId.slice(0, 12)}… commit=${result.manifest.commit.slice(0, 12)}…`, + ); + return; + } + if (cmd === 'self-test') { + try { + const result = runSelfTest(); + for (const step of result.steps) { + console.error(` ${step}`); + } + console.error('gc-build-manifest self-test passed'); + } catch (err) { + console.error(err.message ?? err); + process.exitCode = 1; + } + return; + } + console.error(`unknown command: ${cmd} (expected write|verify|self-test)`); + process.exitCode = 1; +} + +const isMain = + process.argv[1] != null && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); + +if (isMain) { + main(process.argv.slice(2)); +} diff --git a/examples/benchmark/gc-policy-scenarios.js b/examples/benchmark/gc-policy-scenarios.js new file mode 100644 index 000000000000..9acaca8a3af7 --- /dev/null +++ b/examples/benchmark/gc-policy-scenarios.js @@ -0,0 +1,548 @@ +/** + * Deterministic GC fixtures and scenario descriptors for the Node GC harness. + * + * Builds state + candidate queues outside timed work. Fixtures match + * entities/entitiesMeta and endpoints/meta so sweeps are eligible immediately + * under BenchmarkGCPolicy (expiresAt → 0). + */ +import { + GCPolicy, + Controller, + createReducer, + initialState, + actionTypes, +} from './dist/index.js'; +import { createMatcher } from './filter.js'; + +const { GC } = actionTypes; + +export const CANONICAL_COUNTS = [1_000, 10_000, 100_000]; +export const ENTITY_KEY = 'BenchEntity'; +export const ZERO_META = Object.freeze({ + date: 0, + fetchedAt: 0, + expiresAt: 0, +}); + +/** Timerless explicit policy: no intervals/idleness; one-shot public sweep. */ +export class BenchmarkGCPolicy extends GCPolicy { + constructor() { + super({ expiresAt: () => 0 }); + } + + init(controller) { + this.controller = controller; + } + + cleanup() {} + + /** Public one-shot entry that calls the protected monolithic runSweep. */ + sweep() { + this.runSweep(); + } + + get entityQueueLength() { + return this.entitiesQ.length; + } + + get endpointQueueSize() { + return this.endpointsQ.size; + } + + get queueEntries() { + return this.entityQueueLength + this.endpointQueueSize; + } +} + +/** + * Split mixed total into entity + endpoint counts (entities get the remainder + * when odd so entityCount + endpointCount === total). + */ +export function splitMixedCount(total) { + const endpoints = Math.floor(total / 2); + return { entities: total - endpoints, endpoints }; +} + +function entityPath(pk) { + return { key: ENTITY_KEY, pk: String(pk) }; +} + +function endpointKey(i) { + return `bench-endpoint-${i}`; +} + +/** Build store state with matching entities + entitiesMeta. */ +export function buildEntityState(count) { + const entities = { [ENTITY_KEY]: {} }; + const entitiesMeta = { [ENTITY_KEY]: {} }; + for (let i = 0; i < count; i++) { + const pk = String(i); + entities[ENTITY_KEY][pk] = { id: pk }; + entitiesMeta[ENTITY_KEY][pk] = { ...ZERO_META }; + } + return { + ...initialState, + entities, + entitiesMeta, + }; +} + +/** Build store state with matching endpoints + meta. */ +export function buildEndpointState(count) { + const endpoints = {}; + const meta = {}; + for (let i = 0; i < count; i++) { + const key = endpointKey(i); + endpoints[key] = String(i); + meta[key] = { ...ZERO_META }; + } + return { + ...initialState, + endpoints, + meta, + }; +} + +/** Mixed state: entityCount distinct entities + endpointCount distinct endpoints. */ +export function buildMixedState(entityCount, endpointCount) { + const state = buildEntityState(entityCount); + const endpoints = {}; + const meta = {}; + for (let i = 0; i < endpointCount; i++) { + const key = endpointKey(i); + endpoints[key] = String(i); + meta[key] = { ...ZERO_META }; + } + return { ...state, endpoints, meta }; +} + +/** Scalar expectations only — no retained path/key arrays (avoids observer heap). */ +function expectedScalars(overrides) { + return { + queueEntries: overrides.queueEntries, + uniqueTargets: overrides.uniqueTargets, + expectedEntitiesInAction: overrides.expectedEntitiesInAction, + expectedEndpointsInAction: overrides.expectedEndpointsInAction, + expectedUniqueEntityDeletions: overrides.expectedUniqueEntityDeletions, + expectedEndpointDeletions: overrides.expectedEndpointDeletions, + }; +} + +/** + * Queue candidates via createCountRef mount/release (outside timed work). + * Duplicate pattern repeatedly releases one entity path. + * Does not retain parallel target arrays — callers that need a prebuilt GC + * action use buildPrebuiltAction(). + */ +export function queueCandidates(policy, spec) { + const { candidateKind, pattern, count } = spec; + + if (pattern === 'duplicate') { + if (candidateKind !== 'entity') { + throw new Error( + `duplicate pattern only supports candidateKind=entity (got ${candidateKind})`, + ); + } + const path = entityPath(0); + const countRef = policy.createCountRef({ paths: [path] }); + for (let i = 0; i < count; i++) { + const release = countRef(); + release(); + } + return expectedScalars({ + queueEntries: count, + uniqueTargets: 1, + expectedEntitiesInAction: count, + expectedEndpointsInAction: 0, + expectedUniqueEntityDeletions: 1, + expectedEndpointDeletions: 0, + }); + } + + if (candidateKind === 'entity') { + for (let i = 0; i < count; i++) { + const release = policy.createCountRef({ paths: [entityPath(i)] })(); + release(); + } + return expectedScalars({ + queueEntries: count, + uniqueTargets: count, + expectedEntitiesInAction: count, + expectedEndpointsInAction: 0, + expectedUniqueEntityDeletions: count, + expectedEndpointDeletions: 0, + }); + } + + if (candidateKind === 'endpoint') { + for (let i = 0; i < count; i++) { + const release = policy.createCountRef({ key: endpointKey(i) })(); + release(); + } + return expectedScalars({ + queueEntries: count, + uniqueTargets: count, + expectedEntitiesInAction: 0, + expectedEndpointsInAction: count, + expectedUniqueEntityDeletions: 0, + expectedEndpointDeletions: count, + }); + } + + if (candidateKind === 'mixed') { + const { entities, endpoints } = splitMixedCount(count); + for (let i = 0; i < entities; i++) { + const release = policy.createCountRef({ paths: [entityPath(i)] })(); + release(); + } + for (let i = 0; i < endpoints; i++) { + const release = policy.createCountRef({ key: endpointKey(i) })(); + release(); + } + return expectedScalars({ + queueEntries: count, + uniqueTargets: count, + expectedEntitiesInAction: entities, + expectedEndpointsInAction: endpoints, + expectedUniqueEntityDeletions: entities, + expectedEndpointDeletions: endpoints, + }); + } + + throw new Error(`unknown candidateKind: ${candidateKind}`); +} + +/** Build a GC action for reducer mode only (scan/e2e get targets from sweep). */ +export function buildPrebuiltAction(spec) { + const { candidateKind, pattern, count } = spec; + + if (pattern === 'duplicate') { + const path = entityPath(0); + return { + type: GC, + entities: Array.from({ length: count }, () => ({ ...path })), + endpoints: [], + }; + } + + if (candidateKind === 'entity') { + return { + type: GC, + entities: Array.from({ length: count }, (_, i) => entityPath(i)), + endpoints: [], + }; + } + + if (candidateKind === 'endpoint') { + return { + type: GC, + entities: [], + endpoints: Array.from({ length: count }, (_, i) => endpointKey(i)), + }; + } + + if (candidateKind === 'mixed') { + const { entities, endpoints } = splitMixedCount(count); + return { + type: GC, + entities: Array.from({ length: entities }, (_, i) => entityPath(i)), + endpoints: Array.from({ length: endpoints }, (_, i) => endpointKey(i)), + }; + } + + throw new Error(`unknown candidateKind: ${candidateKind}`); +} + +function buildStateForSpec(spec) { + const { candidateKind, pattern, count } = spec; + if (pattern === 'duplicate') { + return buildEntityState(1); + } + if (candidateKind === 'entity') return buildEntityState(count); + if (candidateKind === 'endpoint') return buildEndpointState(count); + if (candidateKind === 'mixed') { + const { entities, endpoints } = splitMixedCount(count); + return buildMixedState(entities, endpoints); + } + throw new Error(`unknown candidateKind: ${candidateKind}`); +} + +export function scenarioId(axes) { + return [ + axes.platform, + axes.candidateKind, + axes.pattern, + String(axes.count), + axes.mode, + axes.control, + ].join('/'); +} + +/** + * Wire a Controller + BenchmarkGCPolicy for the given mode. + * - scan: dispatch captures action only; no prebuilt action arrays + * - end-to-end: dispatch reduces; no prebuilt action arrays + * - reducer: prebuilt GC action only (needed for timed reducer path) + */ +export function createHarness(spec, mode) { + const state = buildStateForSpec(spec); + const policy = new BenchmarkGCPolicy(); + const controller = new Controller({ gcPolicy: policy }); + const reducer = createReducer(controller); + + let capturedAction = null; + let workingState = state; + /** Seed retained only until reducer takes its clone (then nulled). */ + let seedState = mode === 'reducer' ? state : null; + + controller.getState = () => workingState; + + if (mode === 'scan') { + controller.dispatch = action => { + capturedAction = action; + }; + } else if (mode === 'end-to-end') { + controller.dispatch = action => { + capturedAction = action; + workingState = reducer(workingState, action); + }; + } else if (mode === 'reducer') { + // Reducer mode does not sweep; dispatch unused during timed work + controller.dispatch = action => { + capturedAction = action; + }; + } else { + throw new Error(`unknown harness mode: ${mode}`); + } + + policy.init(controller); + const expected = queueCandidates(policy, spec); + + if (policy.queueEntries !== expected.queueEntries) { + throw new Error( + `fixture queue cardinality ${policy.queueEntries} !== expected ${expected.queueEntries}`, + ); + } + + // Only reducer needs a prebuilt action; scan/e2e would duplicate 100k target arrays + let prebuiltAction = mode === 'reducer' ? buildPrebuiltAction(spec) : null; + + const harness = { + policy, + reducer, + expected, + get prebuiltAction() { + return prebuiltAction; + }, + getState: () => workingState, + getCapturedAction: () => capturedAction, + clearCapturedAction() { + capturedAction = null; + }, + /** + * Drop observer-only refs (captured action, prebuilt action) while keeping + * the live store + policy for retained-cache heap measurement. + */ + releaseObserverRefs() { + capturedAction = null; + prebuiltAction = null; + seedState = null; + }, + /** + * Reducer: clone seed into working state and drop the full-size seed + * so heapBefore does not retain two copies of the fixture. + */ + takeReducerState() { + if (!seedState) { + throw new Error('takeReducerState() requires reducer mode with a seed'); + } + const clone = structuredClone(seedState); + seedState = null; + workingState = clone; + return clone; + }, + /** Drop store/policy/controller so a later V8 GC can reclaim everything. */ + dispose() { + capturedAction = null; + prebuiltAction = null; + seedState = null; + workingState = null; + controller.getState = () => initialState; + controller.dispatch = () => {}; + policy.cleanup(); + }, + }; + + return harness; +} + +/** Count deletions vs. fixture after a destructive GC apply. */ +export function countRemaining(state, spec, expected) { + const bucket = state.entities?.[ENTITY_KEY]; + const entityRemaining = bucket ? Object.keys(bucket).length : 0; + const endpointRemaining = + state.endpoints ? Object.keys(state.endpoints).length : 0; + + // unique/mixed start with expectedUniqueEntityDeletions entities; duplicate starts with 1 + const startedEntities = + spec.pattern === 'duplicate' ? 1 : expected.expectedUniqueEntityDeletions; + const startedEndpoints = expected.expectedEndpointDeletions; + + return { + entityDeleted: startedEntities - entityRemaining, + endpointDeleted: startedEndpoints - endpointRemaining, + entityRemaining, + endpointRemaining, + }; +} + +/** + * Validate an accepted sample against fixture expectations. + * Throws on invalid fixture/result. + */ +export function validateSample(spec, harness, sampleMetrics, mode, control) { + const { expected, policy, getCapturedAction, getState } = harness; + + if (control === 'no-gc') { + if (sampleMetrics.actionCount !== 0) { + throw new Error( + `no-gc control expected actionCount 0, got ${sampleMetrics.actionCount}`, + ); + } + if (sampleMetrics.deletionCount !== 0) { + throw new Error( + `no-gc control expected deletionCount 0, got ${sampleMetrics.deletionCount}`, + ); + } + return; + } + + if (sampleMetrics.queueEntries !== expected.queueEntries) { + throw new Error( + `queueEntries ${sampleMetrics.queueEntries} !== expected ${expected.queueEntries}`, + ); + } + if (sampleMetrics.uniqueTargets !== expected.uniqueTargets) { + throw new Error( + `uniqueTargets ${sampleMetrics.uniqueTargets} !== expected ${expected.uniqueTargets}`, + ); + } + + const actionTargetCount = + expected.expectedEntitiesInAction + expected.expectedEndpointsInAction; + if (sampleMetrics.actionTargetCount !== actionTargetCount) { + throw new Error( + `actionTargetCount ${sampleMetrics.actionTargetCount} !== expected ${actionTargetCount}`, + ); + } + + if (mode === 'scan' || mode === 'end-to-end') { + const action = getCapturedAction(); + if (!action || action.type !== GC) { + throw new Error('expected a GC action to be dispatched'); + } + if (action.entities.length !== expected.expectedEntitiesInAction) { + throw new Error( + `action.entities.length ${action.entities.length} !== ${expected.expectedEntitiesInAction}`, + ); + } + if (action.endpoints.length !== expected.expectedEndpointsInAction) { + throw new Error( + `action.endpoints.length ${action.endpoints.length} !== ${expected.expectedEndpointsInAction}`, + ); + } + if (spec.pattern === 'duplicate') { + const unique = new Set(action.entities.map(p => `${p.key}:${p.pk}`)); + if (unique.size !== 1) { + throw new Error( + `duplicate pattern expected 1 unique entity path in action, got ${unique.size}`, + ); + } + } + } + + if (mode === 'reducer' || mode === 'end-to-end') { + const expectedDeletions = + expected.expectedUniqueEntityDeletions + + expected.expectedEndpointDeletions; + if (sampleMetrics.deletionCount !== expectedDeletions) { + throw new Error( + `deletionCount ${sampleMetrics.deletionCount} !== expected ${expectedDeletions}`, + ); + } + const remaining = countRemaining(getState(), spec, expected); + if (remaining.entityRemaining !== 0 || remaining.endpointRemaining !== 0) { + throw new Error( + `expected empty GC targets after deletion; remaining entities=${remaining.entityRemaining} endpoints=${remaining.endpointRemaining}`, + ); + } + } + + if (mode === 'scan' && sampleMetrics.deletionCount !== 0) { + throw new Error( + `scan mode must not delete; got ${sampleMetrics.deletionCount}`, + ); + } + + // After a successful sweep, queues should be drained (gc path). + if ((mode === 'scan' || mode === 'end-to-end') && policy.queueEntries !== 0) { + throw new Error( + `expected empty queues after sweep, got ${policy.queueEntries}`, + ); + } +} + +const MODES = ['scan', 'reducer', 'end-to-end']; +const CONTROLS = ['gc', 'no-gc']; +const UNIQUE_KINDS = ['entity', 'endpoint', 'mixed']; + +/** + * Scenario axis list. Fixtures are not built here — only descriptors — + * so filtering a single 100k case never constructs unrelated stores. + * + * @param {string} [filter] substring or ^prefix against scenario id + * @returns {Array} + */ +export function listScenarios(filter) { + const match = createMatcher(filter); + const scenarios = []; + + for (const count of CANONICAL_COUNTS) { + for (const candidateKind of UNIQUE_KINDS) { + for (const mode of MODES) { + for (const control of CONTROLS) { + const axes = { + platform: 'node', + candidateKind, + pattern: 'unique', + count, + mode, + control, + }; + const id = scenarioId(axes); + if (match(id)) scenarios.push({ id, ...axes }); + } + } + } + + // Duplicate baseline: one entity path released `count` times + for (const mode of MODES) { + for (const control of CONTROLS) { + const axes = { + platform: 'node', + candidateKind: 'entity', + pattern: 'duplicate', + count, + mode, + control, + }; + const id = scenarioId(axes); + if (match(id)) scenarios.push({ id, ...axes }); + } + } + } + + return scenarios; +} + +export { GC }; diff --git a/examples/benchmark/gc-policy.js b/examples/benchmark/gc-policy.js new file mode 100644 index 000000000000..b09169af0772 --- /dev/null +++ b/examples/benchmark/gc-policy.js @@ -0,0 +1,401 @@ +/** + * Isolated Node GC measurement harness (monolithic baseline). + * + * Timing windows (see plans/garbage-collection.md): + * 1. Prepare — fixture + queue outside the timed window + * 2. Timed cache-GC work — sweep / reducer / no-op control + * 3. Optional retained-heap path — validate, drop observer refs (captured + * action / prebuilt action / seed clone), keep live store, then settle + + * forced V8 GC. Engine GC is never inside the timed cache-GC section. + * + * JSON report (schemaVersion 1) → stdout + * Optional human table → stderr (--table / default when stderr is a TTY) + * + * Usage: + * yarn build:benchmark + * yarn workspace example-benchmark start:gc [filter] [--samples=N] [--memory] [--table|--no-table] + * yarn workspace example-benchmark start:gc --verify-manifest + */ +import os from 'node:os'; +import process from 'node:process'; +import { setImmediate } from 'node:timers'; +import v8 from 'node:v8'; +import vm from 'node:vm'; + +import { verifyManifestOrThrow, runSelfTest } from './gc-build-manifest.js'; +import { + listScenarios, + createHarness, + validateSample, + countRemaining, + GC, +} from './gc-policy-scenarios.js'; + +const DEFAULT_SAMPLES = 11; + +function parseArgs(argv) { + let filter; + let samples = DEFAULT_SAMPLES; + let memory = false; + let table; + let verifyManifestOnly = false; + for (const arg of argv) { + if (arg === '--memory') { + memory = true; + } else if (arg === '--table') { + table = true; + } else if (arg === '--no-table') { + table = false; + } else if (arg === '--verify-manifest') { + verifyManifestOnly = true; + } else if (arg.startsWith('--samples=')) { + samples = Number(arg.slice('--samples='.length)); + if (!Number.isFinite(samples) || samples < 1) { + throw new Error(`invalid --samples value: ${arg}`); + } + samples = Math.floor(samples); + } else if (arg.startsWith('-')) { + throw new Error(`unknown flag: ${arg}`); + } else if (filter === undefined) { + filter = arg; + } else { + throw new Error(`unexpected argument: ${arg}`); + } + } + if (table === undefined) table = Boolean(process.stderr.isTTY); + return { filter, samples, memory, table, verifyManifestOnly }; +} + +function cpuModel() { + return os.cpus()[0]?.model ?? 'unknown'; +} + +function percentile(sorted, p) { + if (sorted.length === 1) return sorted[0]; + const idx = (p / 100) * (sorted.length - 1); + const lo = Math.floor(idx); + const hi = Math.ceil(idx); + if (lo === hi) return sorted[lo]; + const w = idx - lo; + return sorted[lo] * (1 - w) + sorted[hi] * w; +} + +function summarizeNumbers(values) { + if (values.length === 0) return null; + const sorted = values.slice().sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + const median = + sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]; + return { + median, + min: sorted[0], + max: sorted[sorted.length - 1], + p95: percentile(sorted, 95), + p99: percentile(sorted, 99), + }; +} + +function summarizeScenario(samples) { + const summary = { + totalMs: summarizeNumbers(samples.map(s => s.totalMs)), + actionCount: summarizeNumbers(samples.map(s => s.actionCount)), + queueEntries: summarizeNumbers(samples.map(s => s.queueEntries)), + uniqueTargets: summarizeNumbers(samples.map(s => s.uniqueTargets)), + actionTargetCount: summarizeNumbers(samples.map(s => s.actionTargetCount)), + deletionCount: summarizeNumbers(samples.map(s => s.deletionCount)), + }; + const slices = samples.flatMap(s => s.sliceDurationsMs ?? []); + if (slices.length) { + summary.sliceDurationsMs = summarizeNumbers(slices); + } + const heaps = samples.filter(s => s.heapBeforeBytes != null); + if (heaps.length) { + summary.heapBeforeBytes = summarizeNumbers( + heaps.map(s => s.heapBeforeBytes), + ); + summary.heapAfterBytes = summarizeNumbers(heaps.map(s => s.heapAfterBytes)); + summary.heapDeltaBytes = summarizeNumbers(heaps.map(s => s.heapDeltaBytes)); + } + return summary; +} + +function maybeExposeGc() { + try { + v8.setFlagsFromString('--expose_gc'); + return vm.runInNewContext('gc'); + } catch { + return undefined; + } +} + +function fullGC(gc) { + if (!gc) return; + gc(); + gc(); +} + +function heapUsed() { + return v8.getHeapStatistics().used_heap_size; +} + +async function settleEventLoop() { + await new Promise(resolve => setImmediate(resolve)); + await new Promise(resolve => setImmediate(resolve)); +} + +/** + * Run one sample. Setup/queue are outside the timed section. + * Destructive modes rebuild a fresh harness each sample. + * + * Memory lifecycle (--memory), keep-store / drop-observer: + * prepare → (reducer: take clone, drop seed) → V8 GC → heapBefore + * → timed cache-GC → validate → releaseObserverRefs (action/seed only) + * → settle + V8 GC → heapAfter (live store still retained) + * → dispose harness for the next sample + * + * heapDelta therefore reflects cache contents still held by the store, not + * harness/action observer arrays. Compare gc vs no-gc end-to-end/reducer. + */ +async function runSample(scenario, { memory, gc }) { + const { mode, control } = scenario; + const harness = createHarness(scenario, mode); + const { policy, reducer, expected } = harness; + + const queueEntries = expected.queueEntries; + const uniqueTargets = expected.uniqueTargets; + const actionTargetCount = + expected.expectedEntitiesInAction + expected.expectedEndpointsInAction; + + // Reducer: clone into working state and drop the full-size seed before baseline + let reducerState; + if (mode === 'reducer') { + reducerState = harness.takeReducerState(); + } + + let heapBeforeBytes; + if (memory) { + fullGC(gc); + await settleEventLoop(); + heapBeforeBytes = heapUsed(); + } + + harness.clearCapturedAction(); + + const t0 = performance.now(); + let actionCount = 0; + let deletionCount = 0; + + if (control === 'no-gc') { + // Harness / no-op overhead only — intentionally empty + } else if (mode === 'scan') { + policy.sweep(); + actionCount = harness.getCapturedAction() ? 1 : 0; + } else if (mode === 'reducer') { + reducer(reducerState, harness.prebuiltAction); + actionCount = 1; + } else if (mode === 'end-to-end') { + policy.sweep(); + const action = harness.getCapturedAction(); + actionCount = action && action.type === GC ? 1 : 0; + } else { + throw new Error(`unknown mode: ${mode}`); + } + + const totalMs = performance.now() - t0; + + // Deletion accounting is outside the timed window + if (control === 'gc' && (mode === 'reducer' || mode === 'end-to-end')) { + const remaining = countRemaining(harness.getState(), scenario, expected); + deletionCount = remaining.entityDeleted + remaining.endpointDeleted; + } + + // Monolithic GC: report one slice only (honest representation of current behavior) + const sliceDurationsMs = control === 'gc' ? [totalMs] : []; + + const sample = { + totalMs, + sliceDurationsMs, + actionCount, + queueEntries, + uniqueTargets, + actionTargetCount: control === 'gc' ? actionTargetCount : 0, + deletionCount, + }; + + // Validate while harness/action refs still exist + validateSample(scenario, harness, sample, mode, control); + + if (memory) { + // Drop captured GC action (holds target arrays) and prebuilt action; keep store + harness.releaseObserverRefs(); + reducerState = undefined; + await settleEventLoop(); + fullGC(gc); + const heapAfterBytes = heapUsed(); + sample.heapBeforeBytes = heapBeforeBytes; + sample.heapAfterBytes = heapAfterBytes; + sample.heapDeltaBytes = heapAfterBytes - heapBeforeBytes; + } + + harness.dispose(); + + return sample; +} + +function formatMs(n) { + if (n == null) return '—'; + if (n < 1) return `${(n * 1000).toFixed(1)} µs`; + if (n < 1000) return `${n.toFixed(2)} ms`; + return `${(n / 1000).toFixed(2)} s`; +} + +function formatBytes(n) { + if (n == null) return '—'; + const abs = Math.abs(n); + const sign = n < 0 ? '-' : ''; + if (abs >= 1024 * 1024) + return `${sign}${(abs / (1024 * 1024)).toFixed(2)} MB`; + if (abs >= 1024) return `${sign}${(abs / 1024).toFixed(1)} KB`; + return `${n} B`; +} + +function printTable(scenarioResults) { + const rows = scenarioResults.map(({ id, summary }) => { + const row = { + scenario: id, + 'median total': formatMs(summary.totalMs?.median), + min: formatMs(summary.totalMs?.min), + max: formatMs(summary.totalMs?.max), + p95: formatMs(summary.totalMs?.p95), + actions: summary.actionCount?.median ?? '—', + queue: summary.queueEntries?.median ?? '—', + targets: summary.actionTargetCount?.median ?? '—', + deleted: summary.deletionCount?.median ?? '—', + }; + if (summary.heapDeltaBytes) { + row['heap Δ'] = formatBytes(summary.heapDeltaBytes.median); + } + return row; + }); + const err = new console.Console(process.stderr, process.stderr); + err.log('\nGC policy benchmark (cache GC — not V8 engine GC)\n'); + err.table(rows); +} + +async function main() { + const { + filter, + samples: sampleCount, + memory, + table, + verifyManifestOnly, + } = parseArgs(process.argv.slice(2)); + + // Provenance check is outside timing and runs before any scenario work + if (verifyManifestOnly) { + const result = runSelfTest(); + for (const step of result.steps) { + console.error(` ${step}`); + } + console.error('gc-build-manifest self-test passed'); + process.stdout.write( + `${JSON.stringify( + { + ok: true, + buildId: result.manifest.buildId, + commit: result.manifest.commit, + dirty: result.manifest.dirty, + sourceDigest: result.manifest.sourceDigest, + }, + null, + 2, + )}\n`, + ); + return; + } + + const buildManifest = verifyManifestOrThrow(); + + const scenarios = listScenarios(filter); + if (scenarios.length === 0) { + throw new Error(`no scenarios matched filter: ${filter ?? '(none)'}`); + } + + const gc = memory ? maybeExposeGc() : undefined; + if (memory && !gc) { + console.error( + 'warning: --memory requested but V8 gc() is unavailable; heap metrics omitted', + ); + } + + const scenarioResults = []; + for (const scenario of scenarios) { + const samples = []; + for (let i = 0; i < sampleCount; i++) { + samples.push(await runSample(scenario, { memory: Boolean(gc), gc })); + } + scenarioResults.push({ + id: scenario.id, + platform: scenario.platform, + candidateKind: scenario.candidateKind, + pattern: scenario.pattern, + count: scenario.count, + mode: scenario.mode, + control: scenario.control, + samples, + summary: summarizeScenario(samples), + }); + } + + const report = { + schemaVersion: 1, + units: { + totalMs: 'milliseconds', + sliceDurationsMs: 'milliseconds', + heapBeforeBytes: 'bytes', + heapAfterBytes: 'bytes', + heapDeltaBytes: 'bytes', + }, + memorySemantics: + memory ? + { + model: 'keep-store-drop-observer', + description: + 'heapBefore is taken after fixture/queue prepare (and reducer seed drop) with a forced V8 GC. After timed cache-GC and validation, observer refs (captured GC action target arrays, prebuilt reducer action, reducer seed) are released; the live store remains. heapAfter follows settle + forced V8 GC. heapDeltaBytes ≈ retained store cache change, not harness retention. Compare gc vs no-gc for reducer/end-to-end; scan does not delete store entries so its delta is not a retained-cache signal.', + } + : undefined, + build: { + schemaVersion: buildManifest.schemaVersion, + buildId: buildManifest.buildId, + commit: buildManifest.commit, + dirty: buildManifest.dirty, + sourceDigest: buildManifest.sourceDigest, + artifacts: buildManifest.artifacts, + label: 'examples/benchmark GC monolithic baseline', + manifest: 'dist/gc-build-manifest.json', + }, + environment: { + runtime: 'node', + nodeVersion: process.version, + os: `${os.type()} ${os.release()}`, + arch: os.arch(), + cpuModel: cpuModel(), + cpus: os.cpus().length, + platform: process.platform, + }, + config: { + samplesPerScenario: sampleCount, + filter: filter ?? null, + memoryMetrics: Boolean(gc), + }, + scenarios: scenarioResults, + }; + + if (table) printTable(scenarioResults); + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +} + +main().catch(err => { + console.error(err); + process.exitCode = 1; +}); diff --git a/examples/benchmark/package.json b/examples/benchmark/package.json index 097714f6ec53..3affce30e044 100644 --- a/examples/benchmark/package.json +++ b/examples/benchmark/package.json @@ -11,9 +11,11 @@ "node": ">=22" }, "scripts": { - "build": "webpack --mode=production --target=node --env readable", + "build": "webpack --mode=production --target=node --env readable && node ./gc-build-manifest.js write", "start": "NODE_ENV=production node --allow-natives-syntax ./index.js", "start:memory": "NODE_ENV=production node ./memory.js", + "start:gc": "NODE_ENV=production node ./gc-policy.js", + "start:gc:verify": "node ./gc-build-manifest.js self-test", "start:trace": "yarn run start --trace_opt --trace_deopt", "start:deopt": "NODE_ENV=production npx dexnode --out v8.log --redirect-code-traces-to=/tmp/codetrace --allow-natives-syntax ./index.js" }, diff --git a/package.json b/package.json index 32c5ed89ec89..4da863ca1a57 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "packages/*", "examples/benchmark", "examples/benchmark-react", + "examples/benchmark-native", "examples/test-bundlesize", "examples/normalizr-*", "examples/coin-app", diff --git a/plans/garbage-collection.md b/plans/garbage-collection.md index 42b4375ed7ba..d6b8fce01625 100644 --- a/plans/garbage-collection.md +++ b/plans/garbage-collection.md @@ -444,18 +444,122 @@ Also required before aggressive default changes: | Expensive / device | Heap snapshots; release-Hermes Android frame and memory | Start as manual; promote only with cost justification | | Unit tests | Jest correctness for GC policy and reducer behavior | Required for logic; **not** a substitute for idle or frame measurement | -### Planned commands and artifacts - -Commands and artifact paths are planned, not claimed as already present: +### Implemented harnesses and artifacts + +v1 measurement harnesses exist in the monorepo (shared JSON vocabulary; no shared +runtime package): + +- **Node** — `examples/benchmark` (`start:gc`): scan / reducer / end-to-end modes + over the canonical scenario axes, with `gc` / `no-gc` controls. +- **Browser** — `examples/benchmark-react` (`bench:gc`): Chromium interaction, + long-task, frame-period, and heap-delta collection for the same axes. +- **Android** — `examples/benchmark-native`: release-Hermes interaction / frame / + observational memory runs on device; host matrix/collect scripts pull reports + over adb. Build and unit validation can pass without a device; device runs are + still required for calibration. + +Aggregated schemaVersion-1 JSON reports are generated artifacts (stdout redirect, +`BENCH_GC_OUTPUT`, or the native artifacts path). Optional traces/heap snapshots +stay local. Raw machine reports and calibration notes are not committed as +universal baselines — see Baseline capture below for current host status and +rerun commands. + +### Baseline capture and gate status + +Captured **2026-07-18** on Linux WSL2 x64, Node v24.5.0, headless Chromium 149 +(~16.7 ms measured display period). Numbers below are from **full provenanced +reruns** tied to BuildManifest IDs. Raw reports remain local `/tmp` (and native +sidecar) ignored artifacts — not committed universal baselines. +`dirty=true` is expected: the measurement implementation is still uncommitted; +manifest `buildId` / source / artifact hashes still make those local reports +attributable. This measurement-phase work does **not** change GC production +defaults. + +**What ran successfully** + +- ReactDOM GC policy / `countRef` / web integration: 3 suites, 10 tests passed; + ReactNative GC integration: 1 suite/test passed. +- Provenanced Node and browser 100k matrices (manifests below). +- Core `^get` / `^set` and React browser small hot-path controls (output retained + in local artifacts; passed). +- `yarn ci:build:bundlesize` completed (`rdcClient.js` 33.8 KiB minified absolute + size — a build baseline, not a branch delta). +- Native release assemble + sidecar verification (manifest below). No adb device + attached → **no** Android frame/memory baseline. + +**BuildManifest provenance** + +| Layer | Status | Identity | +| --- | --- | --- | +| Node | complete (100k matrix) | `buildId` `5da8f199371b9c84fe516700e977386f39267009ca6eae05ed5c0c16427d7a1c`; commit `4a2faa52558eb593e6e1121e2301a6bb8410690c`; `dirty=true` | +| Browser | `complete=true` | `buildId` `1d71ad58789bb9ac78d6fd0aa59259e59ebe8915bd9102a55bfd7086c54c0bce`; same commit; `dirty=true` | +| Native | build only (no device) | `buildId` `4a744c2e08acd490161a61b6d2c69a7d718af694e5b7b9775ec27c2ef56a006d`; `sourceDigest` `fab95578c64693c89f767e7fc7378fc8c8c81168174161641f251c79685672b7`; APK sha `fbc7355b4eb168f4dbce8dc837ae697a6e0d2fc908a5479fe2897e46d5f23949`; size 52,247,736 bytes; sidecar verified | -- Node GC scenarios under the existing benchmark workspace (canonical counts, - `gc` / `no-gc` controls). -- Browser Chromium harness for interaction, long-task, and optional heap modes. -- Android release-Hermes runs on mid-range hardware for frame and retained-memory - modes. -- Aggregated JSON reports plus optional traces/heap snapshots as CI or local - artifacts. -- Calibration notes that record host fingerprint, medians, and variance before - any threshold is frozen. +**100k Node median / p95 `totalMs` (samples=5)** -Exact script names and directory layout land with the harness implementation. +| Scenario | scan | end-to-end | +| --- | --- | --- | +| entity unique | 5.247 / 6.571 | 12.991 / 14.467 | +| endpoint unique | 14.745 / 16.633 | 33.691 / 39.094 | +| mixed unique | 8.961 / 9.859 | 21.113 / 21.527 | +| entity duplicate | 2.242 / 2.280 | 5.911 / 5.992 | + +**Browser 100k `gc` medians (3 samples, `complete=true`)** + +| Scenario | `totalMs` | `maxInputDelayMs` | notes | +| --- | --- | --- | --- | +| entity unique | 9.0 | 9.1 | `heapDelta` −10,704,036 B | +| endpoint unique | 16.7 | 16.8 | `frameIntervalMax` 17.7 ms; `heapDelta` −15,031,404 B | +| mixed unique | 13.5 | 13.5 | `heapDelta` −12,755,312 B | +| entity duplicate | 6.8 | 6.8 | `heapDelta` −433,492 B | + +Matching `no-gc` `maxInputDelayMs` controls were 0–0.1 ms. All 100k runs reported +0 Long Tasks and 0 nearest-period `missedFrames`. Long Tasks only fire at ≥50 ms, +so they miss a sweep that still consumes roughly one 60 Hz frame budget — visible +in endpoint unique `maxInputDelayMs` ≈ 16.8 ms versus the ~16.7 ms measured +period. A synthetic 45 ms calibration reported ~45.1 ms max frame interval and +2 missed frames, confirming the pending-frame probe. + +**Frozen gate definitions (not unsupported universal numbers)** + +1. **Correctness** — fixture self-validation of queue cardinality and final + deletion count; Jest coverage for reacquisition, cancellation, duplicates, + index reuse, and starvation. +2. **Primary interaction comparisons** — GC **excess** over the matching `no-gc` + control for `maxInputDelayMs`, missed frames, and long tasks. Frame budget + uses measured `displayPeriodMs`, not a hardcoded 16.67 ms. +3. **Cooperative ceilings** — max-slice and total-overhead numeric limits remain + calibration outputs. One local host with 3–5 samples is insufficient to freeze + universal thresholds. +4. **CI promotion** — Node/browser may become gates only after repeated controlled + CI variance runs; not claimed yet. +5. **Bundle** — repo rule unchanged: ~1 KiB growth must justify a 5–10% relevant + measured win. +6. **Android** — uncalibrated. Any React Native **default** change stays blocked + until release-Hermes runs on a named mid-range physical device under this + protocol. Harness/APK completion is not device validation. + +**Recommended reruns** + +```bash +# Node 100k gc + no-gc +yarn build:benchmark +yarn workspace example-benchmark start:gc /100000/ --samples=5 --no-table + +# Browser 100k (preview must be serving dist/) +yarn build:benchmark-react +BENCH_GC_OUTPUT=/tmp/gc-browser.json yarn workspace example-benchmark-react \ + bench:gc --samples 3 --scenario 100000 + +# Native 100k gc + no-gc (requires adb device + release APK) +yarn workspace example-benchmark-native build:android:release +SAMPLES=5 yarn workspace example-benchmark-native matrix entity/unique/100000 + +# Foreground controls +yarn workspace example-benchmark start core '^get' +yarn workspace example-benchmark start core '^set' +yarn workspace example-benchmark-react bench:small --lib data-client + +# Bundle +yarn ci:build:bundlesize +``` diff --git a/yarn.lock b/yarn.lock index fdbfa7a0360a..076b4d43dd8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -670,7 +670,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.26.2, @babel/code-frame@npm:^7.27.1, @babel/code-frame@npm:^7.29.0, @babel/code-frame@npm:^7.29.7, @babel/code-frame@npm:^7.8.3": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.26.2, @babel/code-frame@npm:^7.27.1, @babel/code-frame@npm:^7.29.0, @babel/code-frame@npm:^7.29.7, @babel/code-frame@npm:^7.8.3": version: 7.29.7 resolution: "@babel/code-frame@npm:7.29.7" dependencies: @@ -688,7 +688,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:7.29.7, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.15, @babel/core@npm:^7.23.5, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.7, @babel/core@npm:^7.25.2, @babel/core@npm:^7.25.9, @babel/core@npm:^7.27.4": +"@babel/core@npm:7.29.7, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.15, @babel/core@npm:^7.23.5, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.4, @babel/core@npm:^7.24.7, @babel/core@npm:^7.25.2, @babel/core@npm:^7.25.9, @babel/core@npm:^7.27.4": version: 7.29.7 resolution: "@babel/core@npm:7.29.7" dependencies: @@ -711,7 +711,7 @@ __metadata: languageName: node linkType: hard -"@babel/eslint-parser@npm:^7.29.7": +"@babel/eslint-parser@npm:^7.25.1, @babel/eslint-parser@npm:^7.29.7": version: 7.29.7 resolution: "@babel/eslint-parser@npm:7.29.7" dependencies: @@ -737,7 +737,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.22.15, @babel/generator@npm:^7.23.5, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.27.5, @babel/generator@npm:^7.29.1, @babel/generator@npm:^7.29.7": +"@babel/generator@npm:^7.22.15, @babel/generator@npm:^7.23.5, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.27.5, @babel/generator@npm:^7.29.1, @babel/generator@npm:^7.29.7, @babel/generator@npm:^7.7.2": version: 7.29.7 resolution: "@babel/generator@npm:7.29.7" dependencies: @@ -979,7 +979,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.29.0, @babel/parser@npm:^7.29.7": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.29.0, @babel/parser@npm:^7.29.7": version: 7.29.7 resolution: "@babel/parser@npm:7.29.7" dependencies: @@ -1074,7 +1074,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-export-default-from@npm:^7.29.7": +"@babel/plugin-proposal-export-default-from@npm:^7.24.7, @babel/plugin-proposal-export-default-from@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-proposal-export-default-from@npm:7.29.7" dependencies: @@ -1174,7 +1174,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-flow@npm:^7.29.7": +"@babel/plugin-syntax-export-default-from@npm:^7.24.7": + version: 7.29.7 + resolution: "@babel/plugin-syntax-export-default-from@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.29.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/932c0046ec3035851baa59b7b02493279f2810e8d26a4fb0ee8000334fdab6a4b035567184a7bb31db22f93a2775a01ecde31eaa4bd1b3e5cde85e2562d24b00 + languageName: node + linkType: hard + +"@babel/plugin-syntax-flow@npm:^7.12.1, @babel/plugin-syntax-flow@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-syntax-flow@npm:7.29.7" dependencies: @@ -1240,7 +1251,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.27.1, @babel/plugin-syntax-jsx@npm:^7.29.7": +"@babel/plugin-syntax-jsx@npm:^7.27.1, @babel/plugin-syntax-jsx@npm:^7.29.7, @babel/plugin-syntax-jsx@npm:^7.7.2": version: 7.29.7 resolution: "@babel/plugin-syntax-jsx@npm:7.29.7" dependencies: @@ -1350,7 +1361,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.27.1, @babel/plugin-syntax-typescript@npm:^7.29.7": +"@babel/plugin-syntax-typescript@npm:^7.27.1, @babel/plugin-syntax-typescript@npm:^7.29.7, @babel/plugin-syntax-typescript@npm:^7.7.2": version: 7.29.7 resolution: "@babel/plugin-syntax-typescript@npm:7.29.7" dependencies: @@ -1384,7 +1395,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.29.7": +"@babel/plugin-transform-async-generator-functions@npm:^7.25.4, @babel/plugin-transform-async-generator-functions@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-async-generator-functions@npm:7.29.7" dependencies: @@ -1397,7 +1408,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-to-generator@npm:^7.29.7": +"@babel/plugin-transform-async-to-generator@npm:^7.24.7, @babel/plugin-transform-async-to-generator@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-async-to-generator@npm:7.29.7" dependencies: @@ -1421,7 +1432,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.29.7": +"@babel/plugin-transform-block-scoping@npm:^7.25.0, @babel/plugin-transform-block-scoping@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-block-scoping@npm:7.29.7" dependencies: @@ -1432,7 +1443,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.24.7, @babel/plugin-transform-class-properties@npm:^7.29.7": +"@babel/plugin-transform-class-properties@npm:^7.24.7, @babel/plugin-transform-class-properties@npm:^7.25.4, @babel/plugin-transform-class-properties@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-class-properties@npm:7.29.7" dependencies: @@ -1456,7 +1467,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.29.7": +"@babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-classes@npm:7.29.7" dependencies: @@ -1484,7 +1495,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.29.7": +"@babel/plugin-transform-destructuring@npm:^7.24.8, @babel/plugin-transform-destructuring@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-destructuring@npm:7.29.7" dependencies: @@ -1576,7 +1587,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-flow-strip-types@npm:^7.29.7": +"@babel/plugin-transform-flow-strip-types@npm:^7.25.2, @babel/plugin-transform-flow-strip-types@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-flow-strip-types@npm:7.29.7" dependencies: @@ -1588,7 +1599,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-for-of@npm:^7.29.7": +"@babel/plugin-transform-for-of@npm:^7.24.7, @babel/plugin-transform-for-of@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-for-of@npm:7.29.7" dependencies: @@ -1669,7 +1680,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.22.15, @babel/plugin-transform-modules-commonjs@npm:^7.23.3, @babel/plugin-transform-modules-commonjs@npm:^7.24.7, @babel/plugin-transform-modules-commonjs@npm:^7.27.1, @babel/plugin-transform-modules-commonjs@npm:^7.29.7": +"@babel/plugin-transform-modules-commonjs@npm:^7.22.15, @babel/plugin-transform-modules-commonjs@npm:^7.23.3, @babel/plugin-transform-modules-commonjs@npm:^7.24.7, @babel/plugin-transform-modules-commonjs@npm:^7.24.8, @babel/plugin-transform-modules-commonjs@npm:^7.27.1, @babel/plugin-transform-modules-commonjs@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.29.7" dependencies: @@ -1707,7 +1718,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.29.7": +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.24.7, @babel/plugin-transform-named-capturing-groups-regex@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.29.7" dependencies: @@ -1790,7 +1801,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.29.7": +"@babel/plugin-transform-optional-catch-binding@npm:^7.24.7, @babel/plugin-transform-optional-catch-binding@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.29.7" dependencies: @@ -1801,7 +1812,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.24.7, @babel/plugin-transform-optional-chaining@npm:^7.29.7": +"@babel/plugin-transform-optional-chaining@npm:^7.24.7, @babel/plugin-transform-optional-chaining@npm:^7.24.8, @babel/plugin-transform-optional-chaining@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-optional-chaining@npm:7.29.7" dependencies: @@ -1836,7 +1847,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.29.7": +"@babel/plugin-transform-private-property-in-object@npm:^7.24.7, @babel/plugin-transform-private-property-in-object@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-private-property-in-object@npm:7.29.7" dependencies: @@ -1871,7 +1882,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-display-name@npm:^7.29.7": +"@babel/plugin-transform-react-display-name@npm:^7.24.7, @babel/plugin-transform-react-display-name@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-react-display-name@npm:7.29.7" dependencies: @@ -1905,7 +1916,29 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx@npm:^7.29.7": +"@babel/plugin-transform-react-jsx-self@npm:^7.24.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-react-jsx-self@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.29.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/288995f0fd0d61ab740a315fb56c8255eb87dd4a4ac2ac7d0fdd4ce173c3878200141e80da2db0e598c7b2a71e74e604afdbb4c8e14ae6e0527ce0b6294c03da + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx-source@npm:^7.24.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-react-jsx-source@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.29.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/a121899631e6d99b9e1b276acf736dbb77948a31f8eeeae67b89c8a4ab0f05e51ba64544baa06c286a2b9944f227244e15aac464e2313d286d0511fe51e27975 + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx@npm:^7.25.2, @babel/plugin-transform-react-jsx@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-react-jsx@npm:7.29.7" dependencies: @@ -1932,7 +1965,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.29.7": +"@babel/plugin-transform-regenerator@npm:^7.24.7, @babel/plugin-transform-regenerator@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-regenerator@npm:7.29.7" dependencies: @@ -1966,7 +1999,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-runtime@npm:^7.22.15, @babel/plugin-transform-runtime@npm:^7.25.9, @babel/plugin-transform-runtime@npm:^7.29.7": +"@babel/plugin-transform-runtime@npm:^7.22.15, @babel/plugin-transform-runtime@npm:^7.24.7, @babel/plugin-transform-runtime@npm:^7.25.9, @babel/plugin-transform-runtime@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-runtime@npm:7.29.7" dependencies: @@ -2038,7 +2071,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.28.5, @babel/plugin-transform-typescript@npm:^7.29.7": +"@babel/plugin-transform-typescript@npm:^7.25.2, @babel/plugin-transform-typescript@npm:^7.28.5, @babel/plugin-transform-typescript@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-typescript@npm:7.29.7" dependencies: @@ -2076,7 +2109,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:^7.29.7": +"@babel/plugin-transform-unicode-regex@npm:^7.24.7, @babel/plugin-transform-unicode-regex@npm:^7.29.7": version: 7.29.7 resolution: "@babel/plugin-transform-unicode-regex@npm:7.29.7" dependencies: @@ -2100,7 +2133,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.20.2, @babel/preset-env@npm:^7.22.15, @babel/preset-env@npm:^7.25.9, @babel/preset-env@npm:^7.29.7": +"@babel/preset-env@npm:^7.20.2, @babel/preset-env@npm:^7.22.15, @babel/preset-env@npm:^7.25.3, @babel/preset-env@npm:^7.25.9, @babel/preset-env@npm:^7.29.7": version: 7.29.7 resolution: "@babel/preset-env@npm:7.29.7" dependencies: @@ -4372,7 +4405,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": +"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": version: 4.9.1 resolution: "@eslint-community/eslint-utils@npm:4.9.1" dependencies: @@ -4383,7 +4416,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2": +"@eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2, @eslint-community/regexpp@npm:^4.6.1": version: 4.12.2 resolution: "@eslint-community/regexpp@npm:4.12.2" checksum: 10c0/fddcbc66851b308478d04e302a4d771d6917a0b3740dc351513c0da9ca2eab8a1adf99f5e0aa7ab8b13fa0df005c81adeee7e63a92f3effd7d367a163b721c2d @@ -4419,6 +4452,23 @@ __metadata: languageName: node linkType: hard +"@eslint/eslintrc@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/eslintrc@npm:2.1.4" + dependencies: + ajv: "npm:^6.12.4" + debug: "npm:^4.3.2" + espree: "npm:^9.6.0" + globals: "npm:^13.19.0" + ignore: "npm:^5.2.0" + import-fresh: "npm:^3.2.1" + js-yaml: "npm:^4.1.0" + minimatch: "npm:^3.1.2" + strip-json-comments: "npm:^3.1.1" + checksum: 10c0/32f67052b81768ae876c84569ffd562491ec5a5091b0c1e1ca1e0f3c24fb42f804952fdd0a137873bc64303ba368a71ba079a6f691cee25beee9722d94cc8573 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^3.3.6": version: 3.3.6 resolution: "@eslint/eslintrc@npm:3.3.6" @@ -4436,6 +4486,13 @@ __metadata: languageName: node linkType: hard +"@eslint/js@npm:8.57.1": + version: 8.57.1 + resolution: "@eslint/js@npm:8.57.1" + checksum: 10c0/b489c474a3b5b54381c62e82b3f7f65f4b8a5eaaed126546520bf2fede5532a8ed53212919fed1e9048dcf7f37167c8561d58d0ba4492a4244004e7793805223 + languageName: node + linkType: hard + "@eslint/js@npm:9.39.5, @eslint/js@npm:^9.39.4": version: 9.39.5 resolution: "@eslint/js@npm:9.39.5" @@ -4502,6 +4559,17 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/config-array@npm:^0.13.0": + version: 0.13.0 + resolution: "@humanwhocodes/config-array@npm:0.13.0" + dependencies: + "@humanwhocodes/object-schema": "npm:^2.0.3" + debug: "npm:^4.3.1" + minimatch: "npm:^3.0.5" + checksum: 10c0/205c99e756b759f92e1f44a3dc6292b37db199beacba8f26c2165d4051fe73a4ae52fdcfd08ffa93e7e5cb63da7c88648f0e84e197d154bbbbe137b2e0dd332e + languageName: node + linkType: hard + "@humanwhocodes/module-importer@npm:^1.0.1": version: 1.0.1 resolution: "@humanwhocodes/module-importer@npm:1.0.1" @@ -4509,6 +4577,13 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/object-schema@npm:^2.0.3": + version: 2.0.3 + resolution: "@humanwhocodes/object-schema@npm:2.0.3" + checksum: 10c0/80520eabbfc2d32fe195a93557cef50dfe8c8905de447f022675aaf66abc33ae54098f5ea78548d925aa671cd4ab7c7daa5ad704fe42358c9b5e7db60f80696c + languageName: node + linkType: hard + "@humanwhocodes/retry@npm:^0.3.0": version: 0.3.1 resolution: "@humanwhocodes/retry@npm:0.3.1" @@ -4858,6 +4933,20 @@ __metadata: languageName: node linkType: hard +"@jest/console@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/console@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10c0/7be408781d0a6f657e969cbec13b540c329671819c2f57acfad0dae9dbfe2c9be859f38fe99b35dba9ff1536937dc6ddc69fdcd2794812fa3c647a1619797f6c + languageName: node + linkType: hard + "@jest/core@npm:30.4.2": version: 30.4.2 resolution: "@jest/core@npm:30.4.2" @@ -4899,6 +4988,47 @@ __metadata: languageName: node linkType: hard +"@jest/core@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/core@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/reporters": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-changed-files: "npm:^29.7.0" + jest-config: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-resolve-dependencies: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-ansi: "npm:^6.0.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10c0/934f7bf73190f029ac0f96662c85cd276ec460d407baf6b0dbaec2872e157db4d55a7ee0b1c43b18874602f662b37cb973dda469a4e6d88b4e4845b521adeeb2 + languageName: node + linkType: hard + "@jest/create-cache-key-function@npm:^29.7.0": version: 29.7.0 resolution: "@jest/create-cache-key-function@npm:29.7.0" @@ -4948,6 +5078,18 @@ __metadata: languageName: node linkType: hard +"@jest/environment@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/environment@npm:29.7.0" + dependencies: + "@jest/fake-timers": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-mock: "npm:^29.7.0" + checksum: 10c0/c7b1b40c618f8baf4d00609022d2afa086d9c6acc706f303a70bb4b67275868f620ad2e1a9efc5edd418906157337cce50589a627a6400bbdf117d351b91ef86 + languageName: node + linkType: hard + "@jest/expect-utils@npm:30.4.1": version: 30.4.1 resolution: "@jest/expect-utils@npm:30.4.1" @@ -4957,6 +5099,15 @@ __metadata: languageName: node linkType: hard +"@jest/expect-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect-utils@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + checksum: 10c0/60b79d23a5358dc50d9510d726443316253ecda3a7fb8072e1526b3e0d3b14f066ee112db95699b7a43ad3f0b61b750c72e28a5a1cac361d7a2bb34747fa938a + languageName: node + linkType: hard + "@jest/expect@npm:30.4.1": version: 30.4.1 resolution: "@jest/expect@npm:30.4.1" @@ -4967,6 +5118,16 @@ __metadata: languageName: node linkType: hard +"@jest/expect@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect@npm:29.7.0" + dependencies: + expect: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + checksum: 10c0/b41f193fb697d3ced134349250aed6ccea075e48c4f803159db102b826a4e473397c68c31118259868fd69a5cba70e97e1c26d2c2ff716ca39dc73a2ccec037e + languageName: node + linkType: hard + "@jest/fake-timers@npm:30.4.1": version: 30.4.1 resolution: "@jest/fake-timers@npm:30.4.1" @@ -4981,6 +5142,20 @@ __metadata: languageName: node linkType: hard +"@jest/fake-timers@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/fake-timers@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@sinonjs/fake-timers": "npm:^10.0.2" + "@types/node": "npm:*" + jest-message-util: "npm:^29.7.0" + jest-mock: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10c0/cf0a8bcda801b28dc2e2b2ba36302200ee8104a45ad7a21e6c234148932f826cb3bc57c8df3b7b815aeea0861d7b6ca6f0d4778f93b9219398ef28749e03595c + languageName: node + linkType: hard + "@jest/get-type@npm:30.1.0": version: 30.1.0 resolution: "@jest/get-type@npm:30.1.0" @@ -5000,6 +5175,18 @@ __metadata: languageName: node linkType: hard +"@jest/globals@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/globals@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + jest-mock: "npm:^29.7.0" + checksum: 10c0/a385c99396878fe6e4460c43bd7bb0a5cc52befb462cc6e7f2a3810f9e7bcce7cdeb51908fd530391ee452dc856c98baa2c5f5fa8a5b30b071d31ef7f6955cea + languageName: node + linkType: hard + "@jest/pattern@npm:30.4.0": version: 30.4.0 resolution: "@jest/pattern@npm:30.4.0" @@ -5046,6 +5233,43 @@ __metadata: languageName: node linkType: hard +"@jest/reporters@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/reporters@npm:29.7.0" + dependencies: + "@bcoe/v8-coverage": "npm:^0.2.3" + "@jest/console": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + collect-v8-coverage: "npm:^1.0.0" + exit: "npm:^0.1.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + istanbul-lib-coverage: "npm:^3.0.0" + istanbul-lib-instrument: "npm:^6.0.0" + istanbul-lib-report: "npm:^3.0.0" + istanbul-lib-source-maps: "npm:^4.0.0" + istanbul-reports: "npm:^3.1.3" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + slash: "npm:^3.0.0" + string-length: "npm:^4.0.1" + strip-ansi: "npm:^6.0.0" + v8-to-istanbul: "npm:^9.0.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10c0/a754402a799541c6e5aff2c8160562525e2a47e7d568f01ebfc4da66522de39cbb809bbb0a841c7052e4270d79214e70aec3c169e4eae42a03bc1a8a20cb9fa2 + languageName: node + linkType: hard + "@jest/schemas@npm:30.4.1": version: 30.4.1 resolution: "@jest/schemas@npm:30.4.1" @@ -5087,6 +5311,17 @@ __metadata: languageName: node linkType: hard +"@jest/source-map@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/source-map@npm:29.6.3" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.18" + callsites: "npm:^3.0.0" + graceful-fs: "npm:^4.2.9" + checksum: 10c0/a2f177081830a2e8ad3f2e29e20b63bd40bade294880b595acf2fc09ec74b6a9dd98f126a2baa2bf4941acd89b13a4ade5351b3885c224107083a0059b60a219 + languageName: node + linkType: hard + "@jest/test-result@npm:30.4.1": version: 30.4.1 resolution: "@jest/test-result@npm:30.4.1" @@ -5099,6 +5334,18 @@ __metadata: languageName: node linkType: hard +"@jest/test-result@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-result@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + collect-v8-coverage: "npm:^1.0.0" + checksum: 10c0/7de54090e54a674ca173470b55dc1afdee994f2d70d185c80236003efd3fa2b753fff51ffcdda8e2890244c411fd2267529d42c4a50a8303755041ee493e6a04 + languageName: node + linkType: hard + "@jest/test-sequencer@npm:30.4.1": version: 30.4.1 resolution: "@jest/test-sequencer@npm:30.4.1" @@ -5111,6 +5358,18 @@ __metadata: languageName: node linkType: hard +"@jest/test-sequencer@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-sequencer@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10c0/593a8c4272797bb5628984486080cbf57aed09c7cfdc0a634e8c06c38c6bef329c46c0016e84555ee55d1cd1f381518cf1890990ff845524c1123720c8c1481b + languageName: node + linkType: hard + "@jest/transform@npm:30.4.1": version: 30.4.1 resolution: "@jest/transform@npm:30.4.1" @@ -5486,7 +5745,7 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3": +"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" dependencies: @@ -6111,6 +6370,184 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-clean@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli-clean@npm:20.1.0" + dependencies: + "@react-native-community/cli-tools": "npm:20.1.0" + execa: "npm:^5.0.0" + fast-glob: "npm:^3.3.2" + picocolors: "npm:^1.1.1" + checksum: 10c0/5d7fce73545154035e55801e9596b634c85bbe7bb04861f3d0787f2a9588b7b6f927709bfce4e45a8d62da03e5a92789281e07dafd29e2db86ca52ca3e5ad49c + languageName: node + linkType: hard + +"@react-native-community/cli-config-android@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli-config-android@npm:20.1.0" + dependencies: + "@react-native-community/cli-tools": "npm:20.1.0" + fast-glob: "npm:^3.3.2" + fast-xml-parser: "npm:^4.4.1" + picocolors: "npm:^1.1.1" + checksum: 10c0/66e95adf2fb9750a12592a146b034ab4b8ea15aa41efb5d497d6be2f08029a59a0be2450e97de0bf59aa6b66778b3af0bbd693a42b7e7f9f60c9d9b5f966aadc + languageName: node + linkType: hard + +"@react-native-community/cli-config-apple@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli-config-apple@npm:20.1.0" + dependencies: + "@react-native-community/cli-tools": "npm:20.1.0" + execa: "npm:^5.0.0" + fast-glob: "npm:^3.3.2" + picocolors: "npm:^1.1.1" + checksum: 10c0/2c7b7116594c7b0035ef7e41e0219fcff664c3883471460ae48f19dd88ec9a26dd5b4bd60e33c054603b3a10efd78570490bc84599ad987e4ffb274b05512351 + languageName: node + linkType: hard + +"@react-native-community/cli-config@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli-config@npm:20.1.0" + dependencies: + "@react-native-community/cli-tools": "npm:20.1.0" + cosmiconfig: "npm:^9.0.0" + deepmerge: "npm:^4.3.0" + fast-glob: "npm:^3.3.2" + joi: "npm:^17.2.1" + picocolors: "npm:^1.1.1" + checksum: 10c0/e3d863ffc5d18aa7b7406908c1360de7ea7a1bab32897e34d0afe184f4682bb5d110d4db0c6ba2c35c4f1330ca4a281b8fc3548e53a2978b8b07ca3beca5765c + languageName: node + linkType: hard + +"@react-native-community/cli-doctor@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli-doctor@npm:20.1.0" + dependencies: + "@react-native-community/cli-config": "npm:20.1.0" + "@react-native-community/cli-platform-android": "npm:20.1.0" + "@react-native-community/cli-platform-apple": "npm:20.1.0" + "@react-native-community/cli-platform-ios": "npm:20.1.0" + "@react-native-community/cli-tools": "npm:20.1.0" + command-exists: "npm:^1.2.8" + deepmerge: "npm:^4.3.0" + envinfo: "npm:^7.13.0" + execa: "npm:^5.0.0" + node-stream-zip: "npm:^1.9.1" + ora: "npm:^5.4.1" + picocolors: "npm:^1.1.1" + semver: "npm:^7.5.2" + wcwidth: "npm:^1.0.1" + yaml: "npm:^2.2.1" + checksum: 10c0/79762f3e789fe98f21c6f500f6c65df399508979f05174272652a9422295f4dace099e87575776bf209b533a6667070fc355c5b510e14a8a61b31a9a8af7b3be + languageName: node + linkType: hard + +"@react-native-community/cli-platform-android@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli-platform-android@npm:20.1.0" + dependencies: + "@react-native-community/cli-config-android": "npm:20.1.0" + "@react-native-community/cli-tools": "npm:20.1.0" + execa: "npm:^5.0.0" + logkitty: "npm:^0.7.1" + picocolors: "npm:^1.1.1" + checksum: 10c0/8b44178a78245085824c3fda9d6753d03cd817772fdde82b1d4f1a89f177f10339822401c0b8ef17e8353f3168c3971ae4cfb0a158cef03c22e55f5dff66179b + languageName: node + linkType: hard + +"@react-native-community/cli-platform-apple@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli-platform-apple@npm:20.1.0" + dependencies: + "@react-native-community/cli-config-apple": "npm:20.1.0" + "@react-native-community/cli-tools": "npm:20.1.0" + execa: "npm:^5.0.0" + fast-xml-parser: "npm:^4.4.1" + picocolors: "npm:^1.1.1" + checksum: 10c0/2218e172bf13c0bb027f7f2a036a0515f8fe023d98fc65d22d0dab601036c5b461b1af7a5d826df2dd3e86a3894f9531d43e8861746a071ee22c0b33b8e47c29 + languageName: node + linkType: hard + +"@react-native-community/cli-platform-ios@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli-platform-ios@npm:20.1.0" + dependencies: + "@react-native-community/cli-platform-apple": "npm:20.1.0" + checksum: 10c0/f5eef9dfbe3dad7dc69f4b3a37ebee9d124f3a649071e0275624f88468a7f506b622646dcbd61ba0435cc35e705badfc287690d6b7c3867c0a86c9a45d4f463e + languageName: node + linkType: hard + +"@react-native-community/cli-server-api@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli-server-api@npm:20.1.0" + dependencies: + "@react-native-community/cli-tools": "npm:20.1.0" + body-parser: "npm:^1.20.3" + compression: "npm:^1.7.1" + connect: "npm:^3.6.5" + errorhandler: "npm:^1.5.1" + nocache: "npm:^3.0.1" + open: "npm:^6.2.0" + pretty-format: "npm:^29.7.0" + serve-static: "npm:^1.13.1" + ws: "npm:^6.2.3" + checksum: 10c0/d83c9bbff36fb84201478ed8efc17c80f1474cb0346a2577eee7299407e9e34892c213e2316062a2c59d47ebfe9f0c8215b82cd1abb05c85fe7702a94a52cdf5 + languageName: node + linkType: hard + +"@react-native-community/cli-tools@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli-tools@npm:20.1.0" + dependencies: + "@vscode/sudo-prompt": "npm:^9.0.0" + appdirsjs: "npm:^1.2.4" + execa: "npm:^5.0.0" + find-up: "npm:^5.0.0" + launch-editor: "npm:^2.9.1" + mime: "npm:^2.4.1" + ora: "npm:^5.4.1" + picocolors: "npm:^1.1.1" + prompts: "npm:^2.4.2" + semver: "npm:^7.5.2" + checksum: 10c0/993f5dfbc1ae6301e616269896b2e5ecc29888cc8a12325881148b546b07d45572bd065e392ceaad32f86067e31b0eabedbe71fac3b1b820cd489ab86e56094d + languageName: node + linkType: hard + +"@react-native-community/cli-types@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli-types@npm:20.1.0" + dependencies: + joi: "npm:^17.2.1" + checksum: 10c0/247deebf26b435c0cbabfb7dd7de09a4e79c544ee72a2ef2817268108f7c085dab4d54c7fbc8111ad89360e13227f2041c5eac208971c251da332e4d33311a7b + languageName: node + linkType: hard + +"@react-native-community/cli@npm:20.1.0": + version: 20.1.0 + resolution: "@react-native-community/cli@npm:20.1.0" + dependencies: + "@react-native-community/cli-clean": "npm:20.1.0" + "@react-native-community/cli-config": "npm:20.1.0" + "@react-native-community/cli-doctor": "npm:20.1.0" + "@react-native-community/cli-server-api": "npm:20.1.0" + "@react-native-community/cli-tools": "npm:20.1.0" + "@react-native-community/cli-types": "npm:20.1.0" + commander: "npm:^9.4.1" + deepmerge: "npm:^4.3.0" + execa: "npm:^5.0.0" + find-up: "npm:^5.0.0" + fs-extra: "npm:^8.1.0" + graceful-fs: "npm:^4.1.3" + picocolors: "npm:^1.1.1" + prompts: "npm:^2.4.2" + semver: "npm:^7.5.2" + bin: + rnc-cli: build/bin.js + checksum: 10c0/f3fa43fc8c170ba291ae81e796f4ac35956e143bf1094f15f91061b93835ed0665749ca11da91df7fbfcdd96ee32fc16da6cab60adef1a21022195aa1947bd5d + languageName: node + linkType: hard + "@react-native/assets-registry@npm:0.86.0": version: 0.86.0 resolution: "@react-native/assets-registry@npm:0.86.0" @@ -6118,6 +6555,59 @@ __metadata: languageName: node linkType: hard +"@react-native/babel-plugin-codegen@npm:0.86.0": + version: 0.86.0 + resolution: "@react-native/babel-plugin-codegen@npm:0.86.0" + dependencies: + "@babel/traverse": "npm:^7.29.0" + "@react-native/codegen": "npm:0.86.0" + checksum: 10c0/b9263b7fd6a40411639e2343e17454fa9e988e651f44ec9149bd0c3a7945166863713b01d21249b81e62bd9ec67e356fbbee860e7b7f8479df26310b90d2bd4e + languageName: node + linkType: hard + +"@react-native/babel-preset@npm:0.86.0": + version: 0.86.0 + resolution: "@react-native/babel-preset@npm:0.86.0" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/plugin-proposal-export-default-from": "npm:^7.24.7" + "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" + "@babel/plugin-syntax-export-default-from": "npm:^7.24.7" + "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" + "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" + "@babel/plugin-transform-async-generator-functions": "npm:^7.25.4" + "@babel/plugin-transform-async-to-generator": "npm:^7.24.7" + "@babel/plugin-transform-block-scoping": "npm:^7.25.0" + "@babel/plugin-transform-class-properties": "npm:^7.25.4" + "@babel/plugin-transform-classes": "npm:^7.25.4" + "@babel/plugin-transform-destructuring": "npm:^7.24.8" + "@babel/plugin-transform-flow-strip-types": "npm:^7.25.2" + "@babel/plugin-transform-for-of": "npm:^7.24.7" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.8" + "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.24.7" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.24.7" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.24.7" + "@babel/plugin-transform-optional-chaining": "npm:^7.24.8" + "@babel/plugin-transform-private-methods": "npm:^7.24.7" + "@babel/plugin-transform-private-property-in-object": "npm:^7.24.7" + "@babel/plugin-transform-react-display-name": "npm:^7.24.7" + "@babel/plugin-transform-react-jsx": "npm:^7.25.2" + "@babel/plugin-transform-react-jsx-self": "npm:^7.24.7" + "@babel/plugin-transform-react-jsx-source": "npm:^7.24.7" + "@babel/plugin-transform-regenerator": "npm:^7.24.7" + "@babel/plugin-transform-runtime": "npm:^7.24.7" + "@babel/plugin-transform-typescript": "npm:^7.25.2" + "@babel/plugin-transform-unicode-regex": "npm:^7.24.7" + "@react-native/babel-plugin-codegen": "npm:0.86.0" + babel-plugin-syntax-hermes-parser: "npm:0.36.0" + babel-plugin-transform-flow-enums: "npm:^0.0.2" + react-refresh: "npm:^0.14.0" + peerDependencies: + "@babel/core": "*" + checksum: 10c0/cf86875fa72d9bdd0b6ac3819b7a683764f154ff8df5144cce9cc60dd734d9d100bde563acbb48104e0033650dcc28d968eb985ff59e47349dc6861169df02f6 + languageName: node + linkType: hard + "@react-native/codegen@npm:0.86.0": version: 0.86.0 resolution: "@react-native/codegen@npm:0.86.0" @@ -6196,9 +6686,39 @@ __metadata: languageName: node linkType: hard -"@react-native/gradle-plugin@npm:0.86.0": +"@react-native/eslint-config@npm:0.86.0": version: 0.86.0 - resolution: "@react-native/gradle-plugin@npm:0.86.0" + resolution: "@react-native/eslint-config@npm:0.86.0" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/eslint-parser": "npm:^7.25.1" + "@react-native/eslint-plugin": "npm:0.86.0" + "@typescript-eslint/eslint-plugin": "npm:^8.36.0" + "@typescript-eslint/parser": "npm:^8.36.0" + eslint-config-prettier: "npm:^8.5.0" + eslint-plugin-eslint-comments: "npm:^3.2.0" + eslint-plugin-ft-flow: "npm:^2.0.1" + eslint-plugin-jest: "npm:^29.0.1" + eslint-plugin-react: "npm:^7.37.5" + eslint-plugin-react-hooks: "npm:^7.0.1" + eslint-plugin-react-native: "npm:^5.0.0" + peerDependencies: + eslint: ^8.0.0 || ^9.0.0 + prettier: ">=2" + checksum: 10c0/51b3ab96620c8b4ebe14887b9455074de5dbecf87c8860fa065be13a1070a2f69270ecd7eb3a44e215cb49c1eeaa89204e9511606e7c6b452dbb87a1d2bedf3d + languageName: node + linkType: hard + +"@react-native/eslint-plugin@npm:0.86.0": + version: 0.86.0 + resolution: "@react-native/eslint-plugin@npm:0.86.0" + checksum: 10c0/4d8bd7451d75dc572369ab2769647b5ec624bbb13e4a924a35cd526ee57507e3667b2eeedfd79568073dc2fa9db598788fc81b39e0b071397fc854ffd36a3ee5 + languageName: node + linkType: hard + +"@react-native/gradle-plugin@npm:0.86.0": + version: 0.86.0 + resolution: "@react-native/gradle-plugin@npm:0.86.0" checksum: 10c0/02d8f7ae14c3163a6acf7098b1227a7ffd66cbfde96b6b151a02739bfbc27f123e023bd3411383e9a7a33f18e4d4d82100448615b8955a0ff95e8dee5d1ef4e8 languageName: node linkType: hard @@ -6225,6 +6745,32 @@ __metadata: languageName: node linkType: hard +"@react-native/metro-babel-transformer@npm:0.86.0": + version: 0.86.0 + resolution: "@react-native/metro-babel-transformer@npm:0.86.0" + dependencies: + "@babel/core": "npm:^7.25.2" + "@react-native/babel-preset": "npm:0.86.0" + hermes-parser: "npm:0.36.0" + nullthrows: "npm:^1.1.1" + peerDependencies: + "@babel/core": "*" + checksum: 10c0/8b4ab63d32f28f3663204110d37c50a8dd82eef622d57fb236f3d05b69bdfc1fe277f7572e4cf9135790921922f35fe4aea5f7c183fe17cd2ead19ed8e1db5d1 + languageName: node + linkType: hard + +"@react-native/metro-config@npm:0.86.0": + version: 0.86.0 + resolution: "@react-native/metro-config@npm:0.86.0" + dependencies: + "@react-native/js-polyfills": "npm:0.86.0" + "@react-native/metro-babel-transformer": "npm:0.86.0" + metro-config: "npm:^0.84.3" + metro-runtime: "npm:^0.84.3" + checksum: 10c0/92c043b8e4d959a3d8c0aa56940a9ff455c9de21bb6fe2a799fb5a847e45587d9783c3b55e3254f2b17fa71dfc66fff30ce0a70505e0d699dbbf2c76e0ff539c + languageName: node + linkType: hard + "@react-native/normalize-colors@npm:0.86.0": version: 0.86.0 resolution: "@react-native/normalize-colors@npm:0.86.0" @@ -6232,6 +6778,13 @@ __metadata: languageName: node linkType: hard +"@react-native/typescript-config@npm:0.86.0": + version: 0.86.0 + resolution: "@react-native/typescript-config@npm:0.86.0" + checksum: 10c0/66020e6d945e863847639b4932ef22082dad8ba4e345fe785b85987ae18a6cb56b31539c3502731238580ad96a9b1de32fac55e2e245debf693f039ec36fcffc + languageName: node + linkType: hard + "@react-native/virtualized-lists@npm:0.86.0": version: 0.86.0 resolution: "@react-native/virtualized-lists@npm:0.86.0" @@ -6807,7 +7360,7 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^3.0.1": +"@sinonjs/commons@npm:^3.0.0, @sinonjs/commons@npm:^3.0.1": version: 3.0.1 resolution: "@sinonjs/commons@npm:3.0.1" dependencies: @@ -6816,6 +7369,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:^10.0.2": + version: 10.3.0 + resolution: "@sinonjs/fake-timers@npm:10.3.0" + dependencies: + "@sinonjs/commons": "npm:^3.0.0" + checksum: 10c0/2e2fb6cc57f227912814085b7b01fede050cd4746ea8d49a1e44d5a0e56a804663b0340ae2f11af7559ea9bf4d087a11f2f646197a660ea3cb04e19efc04aa63 + languageName: node + linkType: hard + "@sinonjs/fake-timers@npm:^15.4.0": version: 15.4.0 resolution: "@sinonjs/fake-timers@npm:15.4.0" @@ -7729,6 +8291,16 @@ __metadata: languageName: node linkType: hard +"@types/jest@npm:^29.5.13": + version: 29.5.14 + resolution: "@types/jest@npm:29.5.14" + dependencies: + expect: "npm:^29.0.0" + pretty-format: "npm:^29.0.0" + checksum: 10c0/18e0712d818890db8a8dab3d91e9ea9f7f19e3f83c2e50b312f557017dc81466207a71f3ed79cf4428e813ba939954fa26ffa0a9a7f153181ba174581b1c2aed + languageName: node + linkType: hard + "@types/jsdom@npm:^21.1.7": version: 21.1.7 resolution: "@types/jsdom@npm:21.1.7" @@ -7940,7 +8512,7 @@ __metadata: languageName: node linkType: hard -"@types/react-test-renderer@npm:19.1.0": +"@types/react-test-renderer@npm:19.1.0, @types/react-test-renderer@npm:^19.1.0": version: 19.1.0 resolution: "@types/react-test-renderer@npm:19.1.0" dependencies: @@ -7949,7 +8521,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:19.2.17": +"@types/react@npm:*, @types/react@npm:19.2.17, @types/react@npm:^19.2.0": version: 19.2.17 resolution: "@types/react@npm:19.2.17" dependencies: @@ -8039,7 +8611,7 @@ __metadata: languageName: node linkType: hard -"@types/stack-utils@npm:^2.0.3": +"@types/stack-utils@npm:^2.0.0, @types/stack-utils@npm:^2.0.3": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" checksum: 10c0/1f4658385ae936330581bcb8aa3a066df03867d90281cdf89cc356d404bd6579be0f11902304e1f775d92df22c6dd761d4451c804b0a4fba973e06211e9bd77c @@ -8206,6 +8778,26 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/eslint-plugin@npm:^8.36.0": + version: 8.64.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.64.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.12.2" + "@typescript-eslint/scope-manager": "npm:8.64.0" + "@typescript-eslint/type-utils": "npm:8.64.0" + "@typescript-eslint/utils": "npm:8.64.0" + "@typescript-eslint/visitor-keys": "npm:8.64.0" + ignore: "npm:^7.0.5" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + "@typescript-eslint/parser": ^8.64.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/c4b77c05eb2284842583dbc6a0096c50b8f6a73696c431dbf78da555ca1a84f7504d089058e7a7daa2a5f9ebab89523ddfcfd561a2111aeb166a8e862d1801b3 + languageName: node + linkType: hard + "@typescript-eslint/parser@npm:8.63.0, @typescript-eslint/parser@npm:^8.60.1": version: 8.63.0 resolution: "@typescript-eslint/parser@npm:8.63.0" @@ -8222,6 +8814,22 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^8.36.0": + version: 8.64.0 + resolution: "@typescript-eslint/parser@npm:8.64.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:8.64.0" + "@typescript-eslint/types": "npm:8.64.0" + "@typescript-eslint/typescript-estree": "npm:8.64.0" + "@typescript-eslint/visitor-keys": "npm:8.64.0" + debug: "npm:^4.4.3" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/a5a89fd21775ebb9d31a6a5e191a16f03515e34629271c33b96c4587b25e4c2c59988d50bf3dbed68fad9151fc5acdfac3eaf42b004dfcea6e3cc84257e85775 + languageName: node + linkType: hard + "@typescript-eslint/project-service@npm:8.63.0": version: 8.63.0 resolution: "@typescript-eslint/project-service@npm:8.63.0" @@ -8235,6 +8843,19 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/project-service@npm:8.64.0": + version: 8.64.0 + resolution: "@typescript-eslint/project-service@npm:8.64.0" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.64.0" + "@typescript-eslint/types": "npm:^8.64.0" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/855138c17134b7eaa200bb0e6d5acbf7e2190a1bf20e32fe3613461b90bfac5f3716cd261309c3d139d726c72a77ce5e876aa89ac320ad481c9e6f6ebaf2e66c + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.63.0": version: 8.63.0 resolution: "@typescript-eslint/scope-manager@npm:8.63.0" @@ -8245,6 +8866,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.64.0": + version: 8.64.0 + resolution: "@typescript-eslint/scope-manager@npm:8.64.0" + dependencies: + "@typescript-eslint/types": "npm:8.64.0" + "@typescript-eslint/visitor-keys": "npm:8.64.0" + checksum: 10c0/1f1bcad7fcaf3d12af9d398046be3718333546c0a8967d823e21d2a008896b4afad9362450548f93dc2187e4c7444c8736a98448e6de3a26147404abc90abf0d + languageName: node + linkType: hard + "@typescript-eslint/tsconfig-utils@npm:8.63.0, @typescript-eslint/tsconfig-utils@npm:^8.63.0": version: 8.63.0 resolution: "@typescript-eslint/tsconfig-utils@npm:8.63.0" @@ -8254,6 +8885,15 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/tsconfig-utils@npm:8.64.0, @typescript-eslint/tsconfig-utils@npm:^8.64.0": + version: 8.64.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.64.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/701e39ea88a0cbcad5f9657aed973b97db09fe8d5a03df58e4a4ddf307975a9f8270b11f4dad4195c27c37cd335fc44b1437a782f15de3c81b2ec2b6ea0f548d + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.63.0": version: 8.63.0 resolution: "@typescript-eslint/type-utils@npm:8.63.0" @@ -8270,6 +8910,22 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/type-utils@npm:8.64.0": + version: 8.64.0 + resolution: "@typescript-eslint/type-utils@npm:8.64.0" + dependencies: + "@typescript-eslint/types": "npm:8.64.0" + "@typescript-eslint/typescript-estree": "npm:8.64.0" + "@typescript-eslint/utils": "npm:8.64.0" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/2012ee888bb57bd2b49f8b20ec7bed71a53a7ea6e60adaa4c67e282861058e9e952e426e3d872504ed7c4b2ad6ef3df6a81f60afe207f916f525968836d29ea8 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:8.63.0, @typescript-eslint/types@npm:^8.63.0": version: 8.63.0 resolution: "@typescript-eslint/types@npm:8.63.0" @@ -8277,6 +8933,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.64.0, @typescript-eslint/types@npm:^8.64.0": + version: 8.64.0 + resolution: "@typescript-eslint/types@npm:8.64.0" + checksum: 10c0/15ab2b38febfe9d01de801ddca277187e0525ee18c47c94c0d7babe27b69d5680a8e15c7dab5fdf97a050b15c2ab51385f11bc6d6db8264f5a834fa80a83bac1 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.63.0": version: 8.63.0 resolution: "@typescript-eslint/typescript-estree@npm:8.63.0" @@ -8296,6 +8959,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.64.0": + version: 8.64.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.64.0" + dependencies: + "@typescript-eslint/project-service": "npm:8.64.0" + "@typescript-eslint/tsconfig-utils": "npm:8.64.0" + "@typescript-eslint/types": "npm:8.64.0" + "@typescript-eslint/visitor-keys": "npm:8.64.0" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/d6df6990f0381845c3d27b39290a4cae771de937ea8b7bec95a438d89ad5697208b4a1eb96bcc04498c9950882d33a66216a38295ac04b2de24d76cc74a79a0a + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:8.63.0": version: 8.63.0 resolution: "@typescript-eslint/utils@npm:8.63.0" @@ -8311,6 +8993,21 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:8.64.0, @typescript-eslint/utils@npm:^8.0.0": + version: 8.64.0 + resolution: "@typescript-eslint/utils@npm:8.64.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.64.0" + "@typescript-eslint/types": "npm:8.64.0" + "@typescript-eslint/typescript-estree": "npm:8.64.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/3d734a9fd20db6895e5501abd667a7db6c39d5f4a0430ddb6b415be8271886742467a5a18eefd33d1fb2217740ef61f0cf282b28c653964b0a7d568ba70bebd2 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.63.0": version: 8.63.0 resolution: "@typescript-eslint/visitor-keys@npm:8.63.0" @@ -8321,6 +9018,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.64.0": + version: 8.64.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.64.0" + dependencies: + "@typescript-eslint/types": "npm:8.64.0" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10c0/23a5695c5d4ae876ea9a18cdce912ad7a9793f6fe5892da35f0adf3eefd6e39c5742095606329639eb64a4e44ca2810e23fd3a24c067eacec1bd4feebe293374 + languageName: node + linkType: hard + "@typescript/native@npm:typescript@7.0.2": version: 7.0.2 resolution: "typescript@npm:7.0.2" @@ -8560,6 +9267,13 @@ __metadata: languageName: node linkType: hard +"@ungap/structured-clone@npm:^1.2.0": + version: 1.3.3 + resolution: "@ungap/structured-clone@npm:1.3.3" + checksum: 10c0/b199e280ee06e9c447e0ccd38df60a65c2b2c13d3c77af50b1e6d77230aee1ea7da309d7b80c5a4850c8e22e4eee9e4b2813c6a33f8c360560d7f2e99a9f6e8f + languageName: node + linkType: hard + "@unrs/resolver-binding-android-arm-eabi@npm:1.9.0": version: 1.9.0 resolution: "@unrs/resolver-binding-android-arm-eabi@npm:1.9.0" @@ -8710,6 +9424,13 @@ __metadata: languageName: node linkType: hard +"@vscode/sudo-prompt@npm:^9.0.0": + version: 9.3.2 + resolution: "@vscode/sudo-prompt@npm:9.3.2" + checksum: 10c0/9cf63f7001f31ada248aefe0d289e8769d82d9eeb12845aef863faf44620cbe620897625af4e160ab1c2a684d88247a0dbaead0d9a9447a5807feb4a4fd47016 + languageName: node + linkType: hard + "@vue/compiler-core@npm:3.5.39": version: 3.5.39 resolution: "@vue/compiler-core@npm:3.5.39" @@ -9246,6 +9967,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.9.0": + version: 8.17.0 + resolution: "acorn@npm:8.17.0" + bin: + acorn: bin/acorn + checksum: 10c0/5dcefea5f8f023b6cc24cbe71fb5a8112b601d36c4fa07d14e4e6ffc2ee47383332c46b36c766d9437725aa6660156eae50efa0c838719823b50d7c327c4ed42 + languageName: node + linkType: hard + "address@npm:^1.0.1, address@npm:^1.1.2": version: 1.2.2 resolution: "address@npm:1.2.2" @@ -9355,6 +10085,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^6.12.4": + version: 6.15.0 + resolution: "ajv@npm:6.15.0" + dependencies: + fast-deep-equal: "npm:^3.1.1" + fast-json-stable-stringify: "npm:^2.0.0" + json-schema-traverse: "npm:^0.4.1" + uri-js: "npm:^4.2.2" + checksum: 10c0/67966499dd272ecde1c2e467084411132891523d057487587879d39ac04207f4351b7b2324c83198013967fbfa632c1612adc960114a30770fbe07a0773b32c2 + languageName: node + linkType: hard + "algoliasearch-helper@npm:^3.26.0": version: 3.26.0 resolution: "algoliasearch-helper@npm:3.26.0" @@ -9427,7 +10169,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.3.2": +"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.2": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -9436,6 +10178,17 @@ __metadata: languageName: node linkType: hard +"ansi-fragments@npm:^0.2.1": + version: 0.2.1 + resolution: "ansi-fragments@npm:0.2.1" + dependencies: + colorette: "npm:^1.0.7" + slice-ansi: "npm:^2.0.0" + strip-ansi: "npm:^5.0.0" + checksum: 10c0/44e97e558ca2f0b2ca895bfd6ebebeb2e77d674d2e4198ac2d3a05b690193fa35fd185db6e16b92dd0ee854299ea8b4387a99e4155ea62bc8ad4c42154542fd4 + languageName: node + linkType: hard + "ansi-html-community@npm:^0.0.8": version: 0.0.8 resolution: "ansi-html-community@npm:0.0.8" @@ -9445,6 +10198,13 @@ __metadata: languageName: node linkType: hard +"ansi-regex@npm:^4.1.0": + version: 4.1.1 + resolution: "ansi-regex@npm:4.1.1" + checksum: 10c0/d36d34234d077e8770169d980fed7b2f3724bfa2a01da150ccd75ef9707c80e883d27cdf7a0eac2f145ac1d10a785a8a855cffd05b85f778629a0db62e7033da + languageName: node + linkType: hard + "ansi-regex@npm:^5.0.0, ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -9459,7 +10219,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^3.2.1": +"ansi-styles@npm:^3.2.0, ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" dependencies: @@ -9530,6 +10290,13 @@ __metadata: languageName: node linkType: hard +"appdirsjs@npm:^1.2.4": + version: 1.2.7 + resolution: "appdirsjs@npm:1.2.7" + checksum: 10c0/79dd8d7a764cdde2b47efc4383e054814be917ba0cd661ee324bdf3fd11542834548316faea31344f96a7ebc898b5f89c11d1418f825a1d40c396bf1ecb0902b + languageName: node + linkType: hard + "arch@npm:^2.2.0": version: 2.2.0 resolution: "arch@npm:2.2.0" @@ -9785,6 +10552,13 @@ __metadata: languageName: node linkType: hard +"astral-regex@npm:^1.0.0": + version: 1.0.0 + resolution: "astral-regex@npm:1.0.0" + checksum: 10c0/ca460207a19d84c65671e1a85940101522d42f31a450cdb8f93b3464e6daeaf4b58a362826a6c11c57e6cd1976403d197abb0447cfc2087993a29b35c6d63b63 + languageName: node + linkType: hard + "astring@npm:^1.8.0": version: 1.8.6 resolution: "astring@npm:1.8.6" @@ -9794,6 +10568,13 @@ __metadata: languageName: node linkType: hard +"async-limiter@npm:~1.0.0": + version: 1.0.1 + resolution: "async-limiter@npm:1.0.1" + checksum: 10c0/0693d378cfe86842a70d4c849595a0bb50dc44c11649640ca982fa90cbfc74e3cc4753b5a0847e51933f2e9c65ce8e05576e75e5e1fd963a086e673735b35969 + languageName: node + linkType: hard + "async@npm:^3.2.3, async@npm:^3.2.6": version: 3.2.6 resolution: "async@npm:3.2.6" @@ -10089,6 +10870,15 @@ __metadata: languageName: node linkType: hard +"babel-plugin-transform-flow-enums@npm:^0.0.2": + version: 0.0.2 + resolution: "babel-plugin-transform-flow-enums@npm:0.0.2" + dependencies: + "@babel/plugin-syntax-flow": "npm:^7.12.1" + checksum: 10c0/aa9d022d8d4be0e7c4f1ff7e5308fe7e0ff4d6f9099449913e3a11c1e81916623a8f36432da180a9aa3f53ea534dca4401fe33d6528f043f40357cfa790ee778 + languageName: node + linkType: hard + "babel-plugin-transform-import-meta@npm:^2.3.3": version: 2.3.3 resolution: "babel-plugin-transform-import-meta@npm:2.3.3" @@ -10280,6 +11070,17 @@ __metadata: languageName: node linkType: hard +"bl@npm:^4.1.0": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: "npm:^5.5.0" + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.4.0" + checksum: 10c0/02847e1d2cb089c9dc6958add42e3cdeaf07d13f575973963335ac0fdece563a50ac770ac4c8fa06492d2dd276f6cc3b7f08c7cd9c7a7ad0f8d388b2a28def5f + languageName: node + linkType: hard + "bl@npm:^5.0.0": version: 5.1.0 resolution: "bl@npm:5.1.0" @@ -10305,6 +11106,26 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:^1.20.3": + version: 1.20.6 + resolution: "body-parser@npm:1.20.6" + dependencies: + bytes: "npm:~3.1.2" + content-type: "npm:~1.0.5" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:~1.2.0" + http-errors: "npm:~2.0.1" + iconv-lite: "npm:~0.4.24" + on-finished: "npm:~2.4.1" + qs: "npm:~6.15.1" + raw-body: "npm:~2.5.3" + type-is: "npm:~1.6.18" + unpipe: "npm:~1.0.0" + checksum: 10c0/ad477209f1e714c41fa892a7d2fe22e10b23262103cc25cc6492dd3e6f7869cd2d8b00928b543a1a14d3c3663432452a192efd00422fad6f3687b0e38f7e3b07 + languageName: node + linkType: hard + "body-parser@npm:~1.20.5": version: 1.20.5 resolution: "body-parser@npm:1.20.5" @@ -10580,6 +11401,16 @@ __metadata: languageName: node linkType: hard +"buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.1.13" + checksum: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e + languageName: node + linkType: hard + "buffer@npm:^6.0.3": version: 6.0.3 resolution: "buffer@npm:6.0.3" @@ -10746,7 +11577,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.3.1": +"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: 10c0/92ff9b443bfe8abb15f2b1513ca182d16126359ad4f955ebc83dc4ddcc4ef3fdd2c078bc223f2673dc223488e75c99b16cc4d056624374b799e6a1555cf61b23 @@ -11015,6 +11846,13 @@ __metadata: languageName: node linkType: hard +"cjs-module-lexer@npm:^1.0.0": + version: 1.4.3 + resolution: "cjs-module-lexer@npm:1.4.3" + checksum: 10c0/076b3af85adc4d65dbdab1b5b240fe5b45d44fcf0ef9d429044dd94d19be5589376805c44fb2d4b3e684e5fe6a9b7cf3e426476a6507c45283c5fc6ff95240be + languageName: node + linkType: hard + "cjs-module-lexer@npm:^2.1.0": version: 2.1.0 resolution: "cjs-module-lexer@npm:2.1.0" @@ -11070,6 +11908,15 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "cli-cursor@npm:3.1.0" + dependencies: + restore-cursor: "npm:^3.1.0" + checksum: 10c0/92a2f98ff9037d09be3dfe1f0d749664797fb674bf388375a2207a1203b69d41847abf16434203e0089212479e47a358b13a0222ab9fccfe8e2644a7ccebd111 + languageName: node + linkType: hard + "cli-cursor@npm:^4.0.0": version: 4.0.0 resolution: "cli-cursor@npm:4.0.0" @@ -11079,7 +11926,7 @@ __metadata: languageName: node linkType: hard -"cli-spinners@npm:^2.6.1": +"cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.6.1": version: 2.9.2 resolution: "cli-spinners@npm:2.9.2" checksum: 10c0/907a1c227ddf0d7a101e7ab8b300affc742ead4b4ebe920a5bf1bc6d45dce2958fcd195eb28fa25275062fe6fa9b109b93b63bc8033396ed3bcb50297008b3a3 @@ -11117,6 +11964,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^6.0.0": + version: 6.0.0 + resolution: "cliui@npm:6.0.0" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.0" + wrap-ansi: "npm:^6.2.0" + checksum: 10c0/35229b1bb48647e882104cac374c9a18e34bbf0bace0e2cf03000326b6ca3050d6b59545d91e17bfe3705f4a0e2988787aa5cde6331bf5cbbf0164732cef6492 + languageName: node + linkType: hard + "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -11219,6 +12077,13 @@ __metadata: languageName: node linkType: hard +"collect-v8-coverage@npm:^1.0.0": + version: 1.0.3 + resolution: "collect-v8-coverage@npm:1.0.3" + checksum: 10c0/bc62ba251bcce5e3354a8f88fa6442bee56e3e612fec08d4dfcf66179b41ea0bf544b0f78c4ebc0f8050871220af95bb5c5578a6aef346feea155640582f09dc + languageName: node + linkType: hard + "collect-v8-coverage@npm:^1.0.2": version: 1.0.2 resolution: "collect-v8-coverage@npm:1.0.2" @@ -11285,6 +12150,13 @@ __metadata: languageName: node linkType: hard +"colorette@npm:^1.0.7": + version: 1.4.0 + resolution: "colorette@npm:1.4.0" + checksum: 10c0/4955c8f7daafca8ae7081d672e4bd89d553bd5782b5846d5a7e05effe93c2f15f7e9c0cb46f341b59f579a39fcf436241ff79594899d75d5f3460c03d607fe9e + languageName: node + linkType: hard + "colorette@npm:^2.0.10, colorette@npm:^2.0.14": version: 2.0.20 resolution: "colorette@npm:2.0.20" @@ -11313,6 +12185,13 @@ __metadata: languageName: node linkType: hard +"command-exists@npm:^1.2.8": + version: 1.2.9 + resolution: "command-exists@npm:1.2.9" + checksum: 10c0/75040240062de46cd6cd43e6b3032a8b0494525c89d3962e280dde665103f8cc304a8b313a5aa541b91da2f5a9af75c5959dc3a77893a2726407a5e9a0234c16 + languageName: node + linkType: hard + "commander@npm:7, commander@npm:^7.2.0": version: 7.2.0 resolution: "commander@npm:7.2.0" @@ -11383,6 +12262,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^9.4.1": + version: 9.5.0 + resolution: "commander@npm:9.5.0" + checksum: 10c0/5f7784fbda2aaec39e89eb46f06a999e00224b3763dc65976e05929ec486e174fe9aac2655f03ba6a5e83875bd173be5283dc19309b7c65954701c02025b3c1d + languageName: node + linkType: hard + "common-path-prefix@npm:^3.0.0": version: 3.0.0 resolution: "common-path-prefix@npm:3.0.0" @@ -11406,7 +12292,7 @@ __metadata: languageName: node linkType: hard -"compression@npm:1.8.1, compression@npm:^1.8.1": +"compression@npm:1.8.1, compression@npm:^1.7.1, compression@npm:^1.8.1": version: 1.8.1 resolution: "compression@npm:1.8.1" dependencies: @@ -11749,6 +12635,23 @@ __metadata: languageName: node linkType: hard +"create-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "create-jest@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + prompts: "npm:^2.0.1" + bin: + create-jest: bin/create-jest.js + checksum: 10c0/e7e54c280692470d3398f62a6238fd396327e01c6a0757002833f06d00afc62dd7bfe04ff2b9cd145264460e6b4d1eb8386f2925b7e567f97939843b7b0e812f + languageName: node + linkType: hard + "cross-fetch@npm:^4.1.0": version: 4.1.0 resolution: "cross-fetch@npm:4.1.0" @@ -11771,7 +12674,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5, cross-spawn@npm:^7.0.6": +"cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -12673,6 +13576,13 @@ __metadata: languageName: node linkType: hard +"dayjs@npm:^1.8.15": + version: 1.11.21 + resolution: "dayjs@npm:1.11.21" + checksum: 10c0/bd97dfdc4bfea3c66268635690313828b386faa040fbc1f829ff42a2bd748b72c9d9b3c8f9616ce9e61fcb78923f1461a462c969c54b1084458ae1b715898fb0 + languageName: node + linkType: hard + "debounce@npm:^1.2.1": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -12710,6 +13620,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^1.2.0": + version: 1.2.0 + resolution: "decamelize@npm:1.2.0" + checksum: 10c0/85c39fe8fbf0482d4a1e224ef0119db5c1897f8503bcef8b826adff7a1b11414972f6fef2d7dec2ee0b4be3863cf64ac1439137ae9e6af23a3d8dcbe26a5b4b2 + languageName: node + linkType: hard + "decimal.js@npm:^10.5.0": version: 10.5.0 resolution: "decimal.js@npm:10.5.0" @@ -12742,6 +13659,18 @@ __metadata: languageName: node linkType: hard +"dedent@npm:^1.0.0": + version: 1.7.2 + resolution: "dedent@npm:1.7.2" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: 10c0/acaff07cac355b93f17b1b17ebbb84d3cc55af6ab4b7814c3f505e061903e168bc6bf9ddce331552d64dee1525f0b4c549c9ade46aebfac6f69caaed74e90751 + languageName: node + linkType: hard + "dedent@npm:^1.6.0": version: 1.6.0 resolution: "dedent@npm:1.6.0" @@ -12775,7 +13704,7 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:^4.2.2, deepmerge@npm:^4.3.1": +"deepmerge@npm:^4.2.2, deepmerge@npm:^4.3.0, deepmerge@npm:^4.3.1": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044 @@ -12920,7 +13849,7 @@ __metadata: languageName: node linkType: hard -"detect-newline@npm:^3.1.0": +"detect-newline@npm:^3.0.0, detect-newline@npm:^3.1.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" checksum: 10c0/c38cfc8eeb9fda09febb44bcd85e467c970d4e3bf526095394e5a4f18bc26dd0cf6b22c69c1fa9969261521c593836db335c2795218f6d781a512aea2fb8209d @@ -12968,6 +13897,13 @@ __metadata: languageName: node linkType: hard +"diff-sequences@npm:^29.6.3": + version: 29.6.3 + resolution: "diff-sequences@npm:29.6.3" + checksum: 10c0/32e27ac7dbffdf2fb0eb5a84efd98a9ad084fbabd5ac9abb8757c6770d5320d2acd172830b28c4add29bb873d59420601dfc805ac4064330ce59b1adfd0593b2 + languageName: node + linkType: hard + "diffie-hellman@npm:^5.0.3": version: 5.0.3 resolution: "diffie-hellman@npm:5.0.3" @@ -13006,6 +13942,15 @@ __metadata: languageName: node linkType: hard +"doctrine@npm:^3.0.0": + version: 3.0.0 + resolution: "doctrine@npm:3.0.0" + dependencies: + esutils: "npm:^2.0.2" + checksum: 10c0/c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520 + languageName: node + linkType: hard + "dom-accessibility-api@npm:^0.5.9": version: 0.5.16 resolution: "dom-accessibility-api@npm:0.5.16" @@ -13415,8 +14360,17 @@ __metadata: languageName: node linkType: hard -"envinfo@npm:^7.14.0": - version: 7.14.0 +"envinfo@npm:^7.13.0": + version: 7.21.0 + resolution: "envinfo@npm:7.21.0" + bin: + envinfo: dist/cli.js + checksum: 10c0/4170127ca72dbf85be2c114f85558bd08178e8a43b394951ba9fd72d067c6fea3374df45a7b040e39e4e7b30bdd268e5bdf8661d99ae28302c2a88dedb41b5e6 + languageName: node + linkType: hard + +"envinfo@npm:^7.14.0": + version: 7.14.0 resolution: "envinfo@npm:7.14.0" bin: envinfo: dist/cli.js @@ -13449,6 +14403,16 @@ __metadata: languageName: node linkType: hard +"errorhandler@npm:^1.5.1": + version: 1.5.2 + resolution: "errorhandler@npm:1.5.2" + dependencies: + accepts: "npm:~1.3.8" + escape-html: "npm:~1.0.3" + checksum: 10c0/13fc3ba2358893f1f2da43e246105d42a78bf448bf55257b75114c757bd566dcae8b0cd76a3c8777bc451a552a9215979a5e8205bdeee066550cc4acabbfd5af + languageName: node + linkType: hard + "es-abstract@npm:^1.17.0-next.1, es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0, es-abstract@npm:^1.5.1": version: 1.24.0 resolution: "es-abstract@npm:1.24.0" @@ -13758,6 +14722,17 @@ __metadata: languageName: node linkType: hard +"eslint-config-prettier@npm:^8.5.0": + version: 8.10.2 + resolution: "eslint-config-prettier@npm:8.10.2" + peerDependencies: + eslint: ">=7.0.0" + bin: + eslint-config-prettier: bin/cli.js + checksum: 10c0/b5953cf7a86f685e1218b16707bf36643b525513d08495226a6820caccd8b7bfc6b9aa64ac7cb2415dbe2c1f7dc4995832148bdc53ad45777f75a8ded1073b29 + languageName: node + linkType: hard + "eslint-import-context@npm:^0.1.8": version: 0.1.9 resolution: "eslint-import-context@npm:0.1.9" @@ -13841,6 +14816,31 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-eslint-comments@npm:^3.2.0": + version: 3.2.0 + resolution: "eslint-plugin-eslint-comments@npm:3.2.0" + dependencies: + escape-string-regexp: "npm:^1.0.5" + ignore: "npm:^5.0.5" + peerDependencies: + eslint: ">=4.19.1" + checksum: 10c0/c71db824592dc8ea498021572a0bd33d763ef26126bdb3b84a027ca75a1adbe0894ec95024f7de39ef12308560e62cbf8af0d06ffe472be5ba8bd9169c928e96 + languageName: node + linkType: hard + +"eslint-plugin-ft-flow@npm:^2.0.1": + version: 2.0.3 + resolution: "eslint-plugin-ft-flow@npm:2.0.3" + dependencies: + lodash: "npm:^4.17.21" + string-natural-compare: "npm:^3.0.1" + peerDependencies: + "@babel/eslint-parser": ^7.12.0 + eslint: ^8.1.0 + checksum: 10c0/171f6862f7be3c66a415c2ebf14a6e29ade78b661a16f344b78fbefeaeed97fc7f2c710c0d3a2c2df2bbb614b282eaef830993c2aac83b13324cd8c2f9497ea6 + languageName: node + linkType: hard + "eslint-plugin-import@npm:^2.32.0": version: 2.32.0 resolution: "eslint-plugin-import@npm:2.32.0" @@ -13870,6 +14870,27 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jest@npm:^29.0.1": + version: 29.15.4 + resolution: "eslint-plugin-jest@npm:29.15.4" + dependencies: + "@typescript-eslint/utils": "npm:^8.0.0" + peerDependencies: + "@typescript-eslint/eslint-plugin": ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + jest: "*" + typescript: ">=4.8.4 <7.0.0" + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + jest: + optional: true + typescript: + optional: true + checksum: 10c0/4a32cd29cb67b54e5fb5f4acbc749e79c024f50099825e91c93d1d9674caf5b4bfe4810c7931e01d6b30e77cbf72d7619db785a12f459f2b23f5885d39cb1033 + languageName: node + linkType: hard + "eslint-plugin-jsx-a11y@npm:^6.10.2": version: 6.10.2 resolution: "eslint-plugin-jsx-a11y@npm:6.10.2" @@ -13924,6 +14945,39 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-react-hooks@npm:^7.0.1": + version: 7.1.1 + resolution: "eslint-plugin-react-hooks@npm:7.1.1" + dependencies: + "@babel/core": "npm:^7.24.4" + "@babel/parser": "npm:^7.24.4" + hermes-parser: "npm:^0.25.1" + zod: "npm:^3.25.0 || ^4.0.0" + zod-validation-error: "npm:^3.5.0 || ^4.0.0" + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + checksum: 10c0/cee8454915d71ac5d70a0d8f4f260e76eaf45fcd4162747dd4282b792ee5616d187351dabe6cdcff9040c79d0cec625635c4fd0777276be119efa88ebe058525 + languageName: node + linkType: hard + +"eslint-plugin-react-native-globals@npm:^0.1.1": + version: 0.1.2 + resolution: "eslint-plugin-react-native-globals@npm:0.1.2" + checksum: 10c0/ddb4ec5e31f6e72a66d51218c8f0b558b5366d614598fbec1833ac529db2c2dc1724c7ed71c1fcf922251b8438634f704d265c9bedf51aecfe807ec4a0403c09 + languageName: node + linkType: hard + +"eslint-plugin-react-native@npm:^5.0.0": + version: 5.0.0 + resolution: "eslint-plugin-react-native@npm:5.0.0" + dependencies: + eslint-plugin-react-native-globals: "npm:^0.1.1" + peerDependencies: + eslint: ^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + checksum: 10c0/c7c927bc743abf0cb367cc64fea5b28b28ea0c58be2990cab858a050b4855e89d90513afa44d73012c9fd670810ad0da2ac72e3e4bdfedf0ce0cbb65e901af7f + languageName: node + linkType: hard + "eslint-plugin-react@npm:^7.37.5": version: 7.37.5 resolution: "eslint-plugin-react@npm:7.37.5" @@ -13969,6 +15023,16 @@ __metadata: languageName: node linkType: hard +"eslint-scope@npm:^7.2.2": + version: 7.2.2 + resolution: "eslint-scope@npm:7.2.2" + dependencies: + esrecurse: "npm:^4.3.0" + estraverse: "npm:^5.2.0" + checksum: 10c0/613c267aea34b5a6d6c00514e8545ef1f1433108097e857225fed40d397dd6b1809dffd11c2fde23b37ca53d7bf935fe04d2a18e6fc932b31837b6ad67e1c116 + languageName: node + linkType: hard + "eslint-scope@npm:^8.4.0": version: 8.4.0 resolution: "eslint-scope@npm:8.4.0" @@ -13986,7 +15050,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.4.3": +"eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10c0/92708e882c0a5ffd88c23c0b404ac1628cf20104a108c745f240a13c332a11aac54f49a22d5762efbffc18ecbc9a580d1b7ad034bf5f3cc3307e5cbff2ec9820 @@ -14056,6 +15120,54 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^8.19.0": + version: 8.57.1 + resolution: "eslint@npm:8.57.1" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@eslint-community/regexpp": "npm:^4.6.1" + "@eslint/eslintrc": "npm:^2.1.4" + "@eslint/js": "npm:8.57.1" + "@humanwhocodes/config-array": "npm:^0.13.0" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@nodelib/fs.walk": "npm:^1.2.8" + "@ungap/structured-clone": "npm:^1.2.0" + ajv: "npm:^6.12.4" + chalk: "npm:^4.0.0" + cross-spawn: "npm:^7.0.2" + debug: "npm:^4.3.2" + doctrine: "npm:^3.0.0" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^7.2.2" + eslint-visitor-keys: "npm:^3.4.3" + espree: "npm:^9.6.1" + esquery: "npm:^1.4.2" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^6.0.1" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + globals: "npm:^13.19.0" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + is-path-inside: "npm:^3.0.3" + js-yaml: "npm:^4.1.0" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + levn: "npm:^0.4.1" + lodash.merge: "npm:^4.6.2" + minimatch: "npm:^3.1.2" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + strip-ansi: "npm:^6.0.1" + text-table: "npm:^0.2.0" + bin: + eslint: bin/eslint.js + checksum: 10c0/1fd31533086c1b72f86770a4d9d7058ee8b4643fd1cfd10c7aac1ecb8725698e88352a87805cf4b2ce890aa35947df4b4da9655fb7fdfa60dbb448a43f6ebcf1 + languageName: node + linkType: hard + "esm-env@npm:^1.1.4": version: 1.1.4 resolution: "esm-env@npm:1.1.4" @@ -14074,6 +15186,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^9.6.0, espree@npm:^9.6.1": + version: 9.6.1 + resolution: "espree@npm:9.6.1" + dependencies: + acorn: "npm:^8.9.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^3.4.1" + checksum: 10c0/1a2e9b4699b715347f62330bcc76aee224390c28bb02b31a3752e9d07549c473f5f986720483c6469cf3cfb3c9d05df612ffc69eb1ee94b54b739e67de9bb460 + languageName: node + linkType: hard + "esprima@npm:^4.0.0, esprima@npm:~4.0.0": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -14084,6 +15207,15 @@ __metadata: languageName: node linkType: hard +"esquery@npm:^1.4.2": + version: 1.7.0 + resolution: "esquery@npm:1.7.0" + dependencies: + estraverse: "npm:^5.1.0" + checksum: 10c0/77d5173db450b66f3bc685d11af4c90cffeedb340f34a39af96d43509a335ce39c894fd79233df32d38f5e4e219fa0f7076f6ec90bae8320170ba082c0db4793 + languageName: node + linkType: hard + "esquery@npm:^1.5.0": version: 1.6.0 resolution: "esquery@npm:1.6.0" @@ -14253,6 +15385,33 @@ __metadata: languageName: node linkType: hard +"example-benchmark-native@workspace:examples/benchmark-native": + version: 0.0.0-use.local + resolution: "example-benchmark-native@workspace:examples/benchmark-native" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/preset-env": "npm:^7.25.3" + "@babel/runtime": "npm:^7.25.0" + "@data-client/core": "workspace:*" + "@react-native-community/cli": "npm:20.1.0" + "@react-native-community/cli-platform-android": "npm:20.1.0" + "@react-native/babel-preset": "npm:0.86.0" + "@react-native/eslint-config": "npm:0.86.0" + "@react-native/metro-config": "npm:0.86.0" + "@react-native/typescript-config": "npm:0.86.0" + "@types/jest": "npm:^29.5.13" + "@types/react": "npm:^19.2.0" + "@types/react-test-renderer": "npm:^19.1.0" + eslint: "npm:^8.19.0" + jest: "npm:^29.6.3" + prettier: "npm:2.8.8" + react: "npm:19.2.3" + react-native: "npm:0.86.0" + react-test-renderer: "npm:19.2.3" + typescript: "npm:^5.8.3" + languageName: unknown + linkType: soft + "example-benchmark-react@workspace:examples/benchmark-react": version: 0.0.0-use.local resolution: "example-benchmark-react@workspace:examples/benchmark-react" @@ -14304,7 +15463,7 @@ __metadata: languageName: unknown linkType: soft -"execa@npm:^5.1.1": +"execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -14365,6 +15524,13 @@ __metadata: languageName: node linkType: hard +"exit@npm:^0.1.2": + version: 0.1.2 + resolution: "exit@npm:0.1.2" + checksum: 10c0/71d2ad9b36bc25bb8b104b17e830b40a08989be7f7d100b13269aaae7c3784c3e6e1e88a797e9e87523993a25ba27c8958959a554535370672cfb4d824af8989 + languageName: node + linkType: hard + "expect@npm:30.4.1, expect@npm:^30.0.0": version: 30.4.1 resolution: "expect@npm:30.4.1" @@ -14379,6 +15545,19 @@ __metadata: languageName: node linkType: hard +"expect@npm:^29.0.0, expect@npm:^29.7.0": + version: 29.7.0 + resolution: "expect@npm:29.7.0" + dependencies: + "@jest/expect-utils": "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10c0/2eddeace66e68b8d8ee5f7be57f3014b19770caaf6815c7a08d131821da527fb8c8cb7b3dcd7c883d2d3d8d184206a4268984618032d1e4b16dc8d6596475d41 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -14476,7 +15655,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0": +"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.2": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" dependencies: @@ -14510,6 +15689,17 @@ __metadata: languageName: node linkType: hard +"fast-xml-parser@npm:^4.4.1": + version: 4.5.7 + resolution: "fast-xml-parser@npm:4.5.7" + dependencies: + strnum: "npm:^1.0.5" + bin: + fxparser: src/cli/cli.js + checksum: 10c0/5fccf3f53d6b2b83143d73089f04ef5db5343422891193cf10d92d3eb856007b7337818494109e46f6fa47b31b9716be15c4b275ff87a0aeb6f7315aa2edc181 + languageName: node + linkType: hard + "fastest-levenshtein@npm:^1.0.12": version: 1.0.16 resolution: "fastest-levenshtein@npm:1.0.16" @@ -14602,6 +15792,15 @@ __metadata: languageName: node linkType: hard +"file-entry-cache@npm:^6.0.1": + version: 6.0.1 + resolution: "file-entry-cache@npm:6.0.1" + dependencies: + flat-cache: "npm:^3.0.4" + checksum: 10c0/58473e8a82794d01b38e5e435f6feaf648e3f36fdb3a56e98f417f4efae71ad1c0d4ebd8a9a7c50c3ad085820a93fc7494ad721e0e4ebc1da3573f4e1c3c7cdd + languageName: node + linkType: hard + "file-entry-cache@npm:^8.0.0": version: 8.0.0 resolution: "file-entry-cache@npm:8.0.0" @@ -14793,6 +15992,17 @@ __metadata: languageName: node linkType: hard +"flat-cache@npm:^3.0.4": + version: 3.2.0 + resolution: "flat-cache@npm:3.2.0" + dependencies: + flatted: "npm:^3.2.9" + keyv: "npm:^4.5.3" + rimraf: "npm:^3.0.2" + checksum: 10c0/b76f611bd5f5d68f7ae632e3ae503e678d205cf97a17c6ab5b12f6ca61188b5f1f7464503efae6dc18683ed8f0b41460beb48ac4b9ac63fe6201296a91ba2f75 + languageName: node + linkType: hard + "flat-cache@npm:^4.0.0": version: 4.0.1 resolution: "flat-cache@npm:4.0.1" @@ -15118,7 +16328,7 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.5": +"get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde @@ -15341,6 +16551,15 @@ __metadata: languageName: node linkType: hard +"globals@npm:^13.19.0": + version: 13.24.0 + resolution: "globals@npm:13.24.0" + dependencies: + type-fest: "npm:^0.20.2" + checksum: 10c0/d3c11aeea898eb83d5ec7a99508600fbe8f83d2cf00cbb77f873dbf2bcb39428eff1b538e4915c993d8a3b3473fa71eeebfe22c9bb3a3003d1e26b1f2c8a42cd + languageName: node + linkType: hard + "globals@npm:^14.0.0": version: 14.0.0 resolution: "globals@npm:14.0.0" @@ -15445,13 +16664,20 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 languageName: node linkType: hard +"graphemer@npm:^1.4.0": + version: 1.4.0 + resolution: "graphemer@npm:1.4.0" + checksum: 10c0/e951259d8cd2e0d196c72ec711add7115d42eb9a8146c8eeda5b8d3ac91e5dd816b9cd68920726d9fd4490368e7ed86e9c423f40db87e2d8dfafa00fa17c3a31 + languageName: node + linkType: hard + "gulp-filter@npm:^9.0.1": version: 9.0.1 resolution: "gulp-filter@npm:9.0.1" @@ -15792,6 +17018,13 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.25.1": + version: 0.25.1 + resolution: "hermes-estree@npm:0.25.1" + checksum: 10c0/48be3b2fa37a0cbc77a112a89096fa212f25d06de92781b163d67853d210a8a5c3784fac23d7d48335058f7ed283115c87b4332c2a2abaaccc76d0ead1a282ac + languageName: node + linkType: hard + "hermes-estree@npm:0.35.0": version: 0.35.0 resolution: "hermes-estree@npm:0.35.0" @@ -15840,6 +17073,15 @@ __metadata: languageName: node linkType: hard +"hermes-parser@npm:^0.25.1": + version: 0.25.1 + resolution: "hermes-parser@npm:0.25.1" + dependencies: + hermes-estree: "npm:0.25.1" + checksum: 10c0/3abaa4c6f1bcc25273f267297a89a4904963ea29af19b8e4f6eabe04f1c2c7e9abd7bfc4730ddb1d58f2ea04b6fee74053d8bddb5656ec6ebf6c79cc8d14202c + languageName: node + linkType: hard + "history@npm:*, history@npm:5.3.0, history@npm:^5.3.0": version: 5.3.0 resolution: "history@npm:5.3.0" @@ -16315,7 +17557,7 @@ __metadata: languageName: node linkType: hard -"ieee754@npm:^1.2.1": +"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb @@ -16340,6 +17582,13 @@ __metadata: languageName: node linkType: hard +"ignore@npm:^5.0.5": + version: 5.3.2 + resolution: "ignore@npm:5.3.2" + checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337 + languageName: node + linkType: hard + "ignore@npm:^5.2.0, ignore@npm:^5.2.4": version: 5.3.1 resolution: "ignore@npm:5.3.1" @@ -16823,6 +18072,13 @@ __metadata: languageName: node linkType: hard +"is-fullwidth-code-point@npm:^2.0.0": + version: 2.0.0 + resolution: "is-fullwidth-code-point@npm:2.0.0" + checksum: 10c0/e58f3e4a601fc0500d8b2677e26e9fe0cd450980e66adb29d85b6addf7969731e38f8e43ed2ec868a09c101a55ac3d8b78902209269f38c5286bc98f5bc1b4d9 + languageName: node + linkType: hard + "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -16830,7 +18086,7 @@ __metadata: languageName: node linkType: hard -"is-generator-fn@npm:^2.1.0": +"is-generator-fn@npm:^2.0.0, is-generator-fn@npm:^2.1.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" checksum: 10c0/2957cab387997a466cd0bf5c1b6047bd21ecb32bdcfd8996b15747aa01002c1c88731802f1b3d34ac99f4f6874b626418bd118658cf39380fe5fff32a3af9c4d @@ -16883,6 +18139,13 @@ __metadata: languageName: node linkType: hard +"is-interactive@npm:^1.0.0": + version: 1.0.0 + resolution: "is-interactive@npm:1.0.0" + checksum: 10c0/dd47904dbf286cd20aa58c5192161be1a67138485b9836d5a70433b21a45442e9611b8498b8ab1f839fc962c7620667a50535fdfb4a6bc7989b8858645c06b4d + languageName: node + linkType: hard + "is-interactive@npm:^2.0.0": version: 2.0.0 resolution: "is-interactive@npm:2.0.0" @@ -17005,7 +18268,7 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.2": +"is-path-inside@npm:^3.0.2, is-path-inside@npm:^3.0.3": version: 3.0.3 resolution: "is-path-inside@npm:3.0.3" checksum: 10c0/cf7d4ac35fb96bab6a1d2c3598fe5ebb29aafb52c0aaa482b5a3ed9d8ba3edc11631e3ec2637660c44b3ce0e61a08d54946e8af30dec0b60a7c27296c68ffd05 @@ -17201,6 +18464,13 @@ __metadata: languageName: node linkType: hard +"is-unicode-supported@npm:^0.1.0": + version: 0.1.0 + resolution: "is-unicode-supported@npm:0.1.0" + checksum: 10c0/00cbe3455c3756be68d2542c416cab888aebd5012781d6819749fefb15162ff23e38501fe681b3d751c73e8ff561ac09a5293eba6f58fdf0178462ce6dcb3453 + languageName: node + linkType: hard + "is-unicode-supported@npm:^1.1.0": version: 1.3.0 resolution: "is-unicode-supported@npm:1.3.0" @@ -17255,6 +18525,13 @@ __metadata: languageName: node linkType: hard +"is-wsl@npm:^1.1.0": + version: 1.1.0 + resolution: "is-wsl@npm:1.1.0" + checksum: 10c0/7ad0012f21092d6f586c7faad84755a8ef0da9b9ec295e4dc82313cce4e1a93a3da3c217265016461f9b141503fe55fa6eb1fd5457d3f05e8d1bdbb48e50c13a + languageName: node + linkType: hard + "is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0": version: 2.2.0 resolution: "is-wsl@npm:2.2.0" @@ -17380,6 +18657,17 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-source-maps@npm:^4.0.0": + version: 4.0.1 + resolution: "istanbul-lib-source-maps@npm:4.0.1" + dependencies: + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + source-map: "npm:^0.6.1" + checksum: 10c0/19e4cc405016f2c906dff271a76715b3e881fa9faeb3f09a86cb99b8512b3a5ed19cadfe0b54c17ca0e54c1142c9c6de9330d65506e35873994e06634eebeb66 + languageName: node + linkType: hard + "istanbul-lib-source-maps@npm:^5.0.0": version: 5.0.6 resolution: "istanbul-lib-source-maps@npm:5.0.6" @@ -17461,6 +18749,17 @@ __metadata: languageName: node linkType: hard +"jest-changed-files@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-changed-files@npm:29.7.0" + dependencies: + execa: "npm:^5.0.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + checksum: 10c0/e071384d9e2f6bb462231ac53f29bff86f0e12394c1b49ccafbad225ce2ab7da226279a8a94f421949920bef9be7ef574fd86aee22e8adfa149be73554ab828b + languageName: node + linkType: hard + "jest-circus@npm:30.4.2": version: 30.4.2 resolution: "jest-circus@npm:30.4.2" @@ -17489,6 +18788,34 @@ __metadata: languageName: node linkType: hard +"jest-circus@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-circus@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + co: "npm:^4.6.0" + dedent: "npm:^1.0.0" + is-generator-fn: "npm:^2.0.0" + jest-each: "npm:^29.7.0" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + pretty-format: "npm:^29.7.0" + pure-rand: "npm:^6.0.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10c0/8d15344cf7a9f14e926f0deed64ed190c7a4fa1ed1acfcd81e4cc094d3cc5bf7902ebb7b874edc98ada4185688f90c91e1747e0dfd7ac12463b097968ae74b5e + languageName: node + linkType: hard + "jest-cli@npm:30.4.2": version: 30.4.2 resolution: "jest-cli@npm:30.4.2" @@ -17514,6 +18841,32 @@ __metadata: languageName: node linkType: hard +"jest-cli@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-cli@npm:29.7.0" + dependencies: + "@jest/core": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + create-jest: "npm:^29.7.0" + exit: "npm:^0.1.2" + import-local: "npm:^3.0.2" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + yargs: "npm:^17.3.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10c0/a658fd55050d4075d65c1066364595962ead7661711495cfa1dfeecf3d6d0a8ffec532f3dbd8afbb3e172dd5fd2fb2e813c5e10256e7cf2fea766314942fb43a + languageName: node + linkType: hard + "jest-config@npm:30.4.2": version: 30.4.2 resolution: "jest-config@npm:30.4.2" @@ -17556,6 +18909,44 @@ __metadata: languageName: node linkType: hard +"jest-config@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-config@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/test-sequencer": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-jest: "npm:^29.7.0" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + deepmerge: "npm:^4.2.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-circus: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + parse-json: "npm:^5.2.0" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-json-comments: "npm:^3.1.1" + peerDependencies: + "@types/node": "*" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + ts-node: + optional: true + checksum: 10c0/bab23c2eda1fff06e0d104b00d6adfb1d1aabb7128441899c9bff2247bd26710b050a5364281ce8d52b46b499153bf7e3ee88b19831a8f3451f1477a0246a0f1 + languageName: node + linkType: hard + "jest-diff@npm:30.4.1": version: 30.4.1 resolution: "jest-diff@npm:30.4.1" @@ -17568,6 +18959,18 @@ __metadata: languageName: node linkType: hard +"jest-diff@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-diff@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + diff-sequences: "npm:^29.6.3" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/89a4a7f182590f56f526443dde69acefb1f2f0c9e59253c61d319569856c4931eae66b8a3790c443f529267a0ddba5ba80431c585deed81827032b2b2a1fc999 + languageName: node + linkType: hard + "jest-docblock@npm:30.4.0": version: 30.4.0 resolution: "jest-docblock@npm:30.4.0" @@ -17577,6 +18980,15 @@ __metadata: languageName: node linkType: hard +"jest-docblock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-docblock@npm:29.7.0" + dependencies: + detect-newline: "npm:^3.0.0" + checksum: 10c0/d932a8272345cf6b6142bb70a2bb63e0856cc0093f082821577ea5bdf4643916a98744dfc992189d2b1417c38a11fa42466f6111526bc1fb81366f56410f3be9 + languageName: node + linkType: hard + "jest-each@npm:30.4.1": version: 30.4.1 resolution: "jest-each@npm:30.4.1" @@ -17590,6 +19002,19 @@ __metadata: languageName: node linkType: hard +"jest-each@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-each@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + pretty-format: "npm:^29.7.0" + checksum: 10c0/f7f9a90ebee80cc688e825feceb2613627826ac41ea76a366fa58e669c3b2403d364c7c0a74d862d469b103c843154f8456d3b1c02b487509a12afa8b59edbb4 + languageName: node + linkType: hard + "jest-environment-jsdom@npm:^30.0.0": version: 30.4.1 resolution: "jest-environment-jsdom@npm:30.4.1" @@ -17683,6 +19108,16 @@ __metadata: languageName: node linkType: hard +"jest-leak-detector@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-leak-detector@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/71bb9f77fc489acb842a5c7be030f2b9acb18574dc9fb98b3100fc57d422b1abc55f08040884bd6e6dbf455047a62f7eaff12aa4058f7cbdc11558718ca6a395 + languageName: node + linkType: hard + "jest-matcher-utils@npm:30.4.1, jest-matcher-utils@npm:^30.0.5": version: 30.4.1 resolution: "jest-matcher-utils@npm:30.4.1" @@ -17695,6 +19130,18 @@ __metadata: languageName: node linkType: hard +"jest-matcher-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-matcher-utils@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/0d0e70b28fa5c7d4dce701dc1f46ae0922102aadc24ed45d594dd9b7ae0a8a6ef8b216718d1ab79e451291217e05d4d49a82666e1a3cc2b428b75cd9c933244e + languageName: node + linkType: hard + "jest-message-util@npm:30.4.1": version: 30.4.1 resolution: "jest-message-util@npm:30.4.1" @@ -17713,6 +19160,23 @@ __metadata: languageName: node linkType: hard +"jest-message-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-message-util@npm:29.7.0" + dependencies: + "@babel/code-frame": "npm:^7.12.13" + "@jest/types": "npm:^29.6.3" + "@types/stack-utils": "npm:^2.0.0" + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10c0/850ae35477f59f3e6f27efac5215f706296e2104af39232bb14e5403e067992afb5c015e87a9243ec4d9df38525ef1ca663af9f2f4766aa116f127247008bd22 + languageName: node + linkType: hard + "jest-mock@npm:30.4.1, jest-mock@npm:^30.0.0": version: 30.4.1 resolution: "jest-mock@npm:30.4.1" @@ -17724,7 +19188,18 @@ __metadata: languageName: node linkType: hard -"jest-pnp-resolver@npm:^1.2.3": +"jest-mock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-mock@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-util: "npm:^29.7.0" + checksum: 10c0/7b9f8349ee87695a309fe15c46a74ab04c853369e5c40952d68061d9dc3159a0f0ed73e215f81b07ee97a9faaf10aebe5877a9d6255068a0977eae6a9ff1d5ac + languageName: node + linkType: hard + +"jest-pnp-resolver@npm:^1.2.2, jest-pnp-resolver@npm:^1.2.3": version: 1.2.3 resolution: "jest-pnp-resolver@npm:1.2.3" peerDependencies: @@ -17760,6 +19235,16 @@ __metadata: languageName: node linkType: hard +"jest-resolve-dependencies@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve-dependencies@npm:29.7.0" + dependencies: + jest-regex-util: "npm:^29.6.3" + jest-snapshot: "npm:^29.7.0" + checksum: 10c0/b6e9ad8ae5b6049474118ea6441dfddd385b6d1fc471db0136f7c8fbcfe97137a9665e4f837a9f49f15a29a1deb95a14439b7aec812f3f99d08f228464930f0d + languageName: node + linkType: hard + "jest-resolve@npm:30.4.1": version: 30.4.1 resolution: "jest-resolve@npm:30.4.1" @@ -17776,6 +19261,23 @@ __metadata: languageName: node linkType: hard +"jest-resolve@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-pnp-resolver: "npm:^1.2.2" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + resolve: "npm:^1.20.0" + resolve.exports: "npm:^2.0.0" + slash: "npm:^3.0.0" + checksum: 10c0/59da5c9c5b50563e959a45e09e2eace783d7f9ac0b5dcc6375dea4c0db938d2ebda97124c8161310082760e8ebbeff9f6b177c15ca2f57fb424f637a5d2adb47 + languageName: node + linkType: hard + "jest-runner@npm:30.4.2": version: 30.4.2 resolution: "jest-runner@npm:30.4.2" @@ -17802,7 +19304,36 @@ __metadata: jest-worker: "npm:30.4.1" p-limit: "npm:^3.1.0" source-map-support: "npm:0.5.13" - checksum: 10c0/339e630fb1a7db52e208ed9f12f722122733fe9a450d9bd83c0fccc10fbc5142a8808f624c41ab1e25833af02f9c3eca85561554b75a5b3ad75b4a226f72c5cf + checksum: 10c0/339e630fb1a7db52e208ed9f12f722122733fe9a450d9bd83c0fccc10fbc5142a8808f624c41ab1e25833af02f9c3eca85561554b75a5b3ad75b4a226f72c5cf + languageName: node + linkType: hard + +"jest-runner@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runner@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/environment": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + graceful-fs: "npm:^4.2.9" + jest-docblock: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-leak-detector: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-resolve: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + source-map-support: "npm:0.5.13" + checksum: 10c0/2194b4531068d939f14c8d3274fe5938b77fa73126aedf9c09ec9dec57d13f22c72a3b5af01ac04f5c1cf2e28d0ac0b4a54212a61b05f10b5d6b47f2a1097bb4 languageName: node linkType: hard @@ -17836,6 +19367,36 @@ __metadata: languageName: node linkType: hard +"jest-runtime@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runtime@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/fake-timers": "npm:^29.7.0" + "@jest/globals": "npm:^29.7.0" + "@jest/source-map": "npm:^29.6.3" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + cjs-module-lexer: "npm:^1.0.0" + collect-v8-coverage: "npm:^1.0.0" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-mock: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-bom: "npm:^4.0.0" + checksum: 10c0/7cd89a1deda0bda7d0941835434e44f9d6b7bd50b5c5d9b0fc9a6c990b2d4d2cab59685ab3cb2850ed4cc37059f6de903af5a50565d7f7f1192a77d3fd6dd2a6 + languageName: node + linkType: hard + "jest-snapshot@npm:30.4.1": version: 30.4.1 resolution: "jest-snapshot@npm:30.4.1" @@ -17865,6 +19426,34 @@ __metadata: languageName: node linkType: hard +"jest-snapshot@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-snapshot@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@babel/generator": "npm:^7.7.2" + "@babel/plugin-syntax-jsx": "npm:^7.7.2" + "@babel/plugin-syntax-typescript": "npm:^7.7.2" + "@babel/types": "npm:^7.3.3" + "@jest/expect-utils": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-preset-current-node-syntax: "npm:^1.0.0" + chalk: "npm:^4.0.0" + expect: "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + natural-compare: "npm:^1.4.0" + pretty-format: "npm:^29.7.0" + semver: "npm:^7.5.3" + checksum: 10c0/6e9003c94ec58172b4a62864a91c0146513207bedf4e0a06e1e2ac70a4484088a2683e3a0538d8ea913bcfd53dc54a9b98a98cdfa562e7fe1d1339aeae1da570 + languageName: node + linkType: hard + "jest-util@npm:30.4.1": version: 30.4.1 resolution: "jest-util@npm:30.4.1" @@ -17937,6 +19526,22 @@ __metadata: languageName: node linkType: hard +"jest-watcher@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-watcher@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + jest-util: "npm:^29.7.0" + string-length: "npm:^4.0.1" + checksum: 10c0/ec6c75030562fc8f8c727cb8f3b94e75d831fc718785abfc196e1f2a2ebc9a2e38744a15147170039628a853d77a3b695561ce850375ede3a4ee6037a2574567 + languageName: node + linkType: hard + "jest-worker@npm:30.4.1, jest-worker@npm:^30.0.5": version: 30.4.1 resolution: "jest-worker@npm:30.4.1" @@ -17973,6 +19578,25 @@ __metadata: languageName: node linkType: hard +"jest@npm:^29.6.3": + version: 29.7.0 + resolution: "jest@npm:29.7.0" + dependencies: + "@jest/core": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + import-local: "npm:^3.0.2" + jest-cli: "npm:^29.7.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10c0/f40eb8171cf147c617cc6ada49d062fbb03b4da666cb8d39cdbfb739a7d75eea4c3ca150fb072d0d273dce0c753db4d0467d54906ad0293f59c54f9db4a09d8b + languageName: node + linkType: hard + "jest@npm:^30.0.0": version: 30.4.2 resolution: "jest@npm:30.4.2" @@ -18010,7 +19634,7 @@ __metadata: languageName: node linkType: hard -"joi@npm:^17.9.2": +"joi@npm:^17.2.1, joi@npm:^17.9.2": version: 17.13.4 resolution: "joi@npm:17.13.4" dependencies: @@ -18384,7 +20008,7 @@ __metadata: languageName: node linkType: hard -"launch-editor@npm:^2.6.1": +"launch-editor@npm:^2.6.1, launch-editor@npm:^2.9.1": version: 2.14.1 resolution: "launch-editor@npm:2.14.1" dependencies: @@ -18593,6 +20217,16 @@ __metadata: languageName: node linkType: hard +"log-symbols@npm:^4.1.0": + version: 4.1.0 + resolution: "log-symbols@npm:4.1.0" + dependencies: + chalk: "npm:^4.1.0" + is-unicode-supported: "npm:^0.1.0" + checksum: 10c0/67f445a9ffa76db1989d0fa98586e5bc2fd5247260dafb8ad93d9f0ccd5896d53fb830b0e54dade5ad838b9de2006c826831a3c528913093af20dff8bd24aca6 + languageName: node + linkType: hard + "log-symbols@npm:^5.1.0": version: 5.1.0 resolution: "log-symbols@npm:5.1.0" @@ -18603,6 +20237,19 @@ __metadata: languageName: node linkType: hard +"logkitty@npm:^0.7.1": + version: 0.7.1 + resolution: "logkitty@npm:0.7.1" + dependencies: + ansi-fragments: "npm:^0.2.1" + dayjs: "npm:^1.8.15" + yargs: "npm:^15.1.0" + bin: + logkitty: bin/logkitty.js + checksum: 10c0/2067fad55c0856c0608c51ab75f8ffa5a858c5f847fefa8ec0e5fd3aa0b7d732010169d187283b23583a72aa6b80bbbec4fc6801a6c47c3fac0fbb294786002a + languageName: node + linkType: hard + "longest-streak@npm:^3.0.0": version: 3.1.0 resolution: "longest-streak@npm:3.1.0" @@ -20046,6 +21693,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:^2.4.1": + version: 2.6.0 + resolution: "mime@npm:2.6.0" + bin: + mime: cli.js + checksum: 10c0/a7f2589900d9c16e3bdf7672d16a6274df903da958c1643c9c45771f0478f3846dcb1097f31eb9178452570271361e2149310931ec705c037210fc69639c8e6c + languageName: node + linkType: hard + "mimic-fn@npm:^2.1.0": version: 2.1.0 resolution: "mimic-fn@npm:2.1.0" @@ -20547,6 +22203,13 @@ __metadata: languageName: node linkType: hard +"nocache@npm:^3.0.1": + version: 3.0.4 + resolution: "nocache@npm:3.0.4" + checksum: 10c0/66e5db1206bee44173358c2264ae9742259273e9719535077fe27807441bad58f0deeadf3cec2aa62d4f86ccb8a0e067c9a64b6329684ddc30a57e377ec458ee + languageName: node + linkType: hard + "nock@npm:13.3.1": version: 13.3.1 resolution: "nock@npm:13.3.1" @@ -20659,6 +22322,13 @@ __metadata: languageName: node linkType: hard +"node-stream-zip@npm:^1.9.1": + version: 1.15.0 + resolution: "node-stream-zip@npm:1.15.0" + checksum: 10c0/429fce95d7e90e846adbe096c61d2ea8d18defc155c0345d25d0f98dd6fc72aeb95039318484a4e0a01dc3814b6d0d1ae0fe91847a29669dff8676ec064078c9 + languageName: node + linkType: hard + "noms@npm:0.0.0": version: 0.0.0 resolution: "noms@npm:0.0.0" @@ -21257,6 +22927,15 @@ __metadata: languageName: node linkType: hard +"open@npm:^6.2.0": + version: 6.4.0 + resolution: "open@npm:6.4.0" + dependencies: + is-wsl: "npm:^1.1.0" + checksum: 10c0/447115632b4f3939fa0d973c33e17f28538fd268fd8257fc49763f7de6e76d29d65585b15998bbd2137337cfb70a92084a0e1b183a466e53a4829f704f295823 + languageName: node + linkType: hard + "open@npm:^7.0.3": version: 7.4.2 resolution: "open@npm:7.4.2" @@ -21301,6 +22980,23 @@ __metadata: languageName: node linkType: hard +"ora@npm:^5.4.1": + version: 5.4.1 + resolution: "ora@npm:5.4.1" + dependencies: + bl: "npm:^4.1.0" + chalk: "npm:^4.1.0" + cli-cursor: "npm:^3.1.0" + cli-spinners: "npm:^2.5.0" + is-interactive: "npm:^1.0.0" + is-unicode-supported: "npm:^0.1.0" + log-symbols: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + wcwidth: "npm:^1.0.1" + checksum: 10c0/10ff14aace236d0e2f044193362b22edce4784add08b779eccc8f8ef97195cae1248db8ec1ec5f5ff076f91acbe573f5f42a98c19b78dba8c54eefff983cae85 + languageName: node + linkType: hard + "ora@npm:^6.3.1": version: 6.3.1 resolution: "ora@npm:6.3.1" @@ -23290,6 +24986,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:2.8.8, prettier@npm:^2.7.1": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" + bin: + prettier: bin-prettier.js + checksum: 10c0/463ea8f9a0946cd5b828d8cf27bd8b567345cf02f56562d5ecde198b91f47a76b7ac9eae0facd247ace70e927143af6135e8cf411986b8cb8478784a4d6d724a + languageName: node + linkType: hard + "prettier@npm:3.9.5, prettier@npm:^3.0.0": version: 3.9.5 resolution: "prettier@npm:3.9.5" @@ -23299,15 +25004,6 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.7.1": - version: 2.8.8 - resolution: "prettier@npm:2.8.8" - bin: - prettier: bin-prettier.js - checksum: 10c0/463ea8f9a0946cd5b828d8cf27bd8b567345cf02f56562d5ecde198b91f47a76b7ac9eae0facd247ace70e927143af6135e8cf411986b8cb8478784a4d6d724a - languageName: node - linkType: hard - "pretty-error@npm:^4.0.0": version: 4.0.0 resolution: "pretty-error@npm:4.0.0" @@ -23341,7 +25037,7 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^29.7.0": +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" dependencies: @@ -23455,7 +25151,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.4.2": +"prompts@npm:^2.0.1, prompts@npm:^2.4.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -23544,6 +25240,13 @@ __metadata: languageName: node linkType: hard +"pure-rand@npm:^6.0.0": + version: 6.1.0 + resolution: "pure-rand@npm:6.1.0" + checksum: 10c0/1abe217897bf74dcb3a0c9aba3555fe975023147b48db540aa2faf507aee91c03bf54f6aef0eb2bf59cc259a16d06b28eca37f0dc426d94f4692aeff02fb0e65 + languageName: node + linkType: hard + "pure-rand@npm:^7.0.0": version: 7.0.1 resolution: "pure-rand@npm:7.0.1" @@ -24657,6 +26360,13 @@ __metadata: languageName: node linkType: hard +"require-main-filename@npm:^2.0.0": + version: 2.0.0 + resolution: "require-main-filename@npm:2.0.0" + checksum: 10c0/db91467d9ead311b4111cbd73a4e67fa7820daed2989a32f7023785a2659008c6d119752d9c4ac011ae07e537eb86523adff99804c5fdb39cd3a017f9b401bb6 + languageName: node + linkType: hard + "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -24715,6 +26425,13 @@ __metadata: languageName: node linkType: hard +"resolve.exports@npm:^2.0.0": + version: 2.0.3 + resolution: "resolve.exports@npm:2.0.3" + checksum: 10c0/1ade1493f4642a6267d0a5e68faeac20b3d220f18c28b140343feb83694d8fed7a286852aef43689d16042c61e2ddb270be6578ad4a13990769e12065191200d + languageName: node + linkType: hard + "resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.11, resolve@npm:^1.22.4, resolve@npm:^1.22.8": version: 1.22.11 resolution: "resolve@npm:1.22.11" @@ -24782,6 +26499,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "restore-cursor@npm:3.1.0" + dependencies: + onetime: "npm:^5.1.0" + signal-exit: "npm:^3.0.2" + checksum: 10c0/8051a371d6aa67ff21625fa94e2357bd81ffdc96267f3fb0fc4aaf4534028343836548ef34c240ffa8c25b280ca35eb36be00b3cb2133fa4f51896d7e73c6b4f + languageName: node + linkType: hard + "restore-cursor@npm:^4.0.0": version: 4.0.0 resolution: "restore-cursor@npm:4.0.0" @@ -24824,6 +26551,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: bin.js + checksum: 10c0/9cb7757acb489bd83757ba1a274ab545eafd75598a9d817e0c3f8b164238dd90eba50d6b848bd4dcc5f3040912e882dc7ba71653e35af660d77b25c381d402e8 + languageName: node + linkType: hard + "rimraf@npm:^6.0.0": version: 6.1.3 resolution: "rimraf@npm:6.1.3" @@ -25381,7 +27119,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3, semver@npm:^7.7.4, semver@npm:^7.8.0, semver@npm:^7.8.4": +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3, semver@npm:^7.7.4, semver@npm:^7.8.0, semver@npm:^7.8.4": version: 7.8.5 resolution: "semver@npm:7.8.5" bin: @@ -25432,6 +27170,27 @@ __metadata: languageName: node linkType: hard +"send@npm:~0.19.1": + version: 0.19.2 + resolution: "send@npm:0.19.2" + dependencies: + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + fresh: "npm:~0.5.2" + http-errors: "npm:~2.0.1" + mime: "npm:1.6.0" + ms: "npm:2.1.3" + on-finished: "npm:~2.4.1" + range-parser: "npm:~1.2.1" + statuses: "npm:~2.0.2" + checksum: 10c0/20c2389fe0fdf3fc499938cac598bc32272287e993c4960717381a10de8550028feadfb9076f959a3a3ebdea42e1f690e116f0d16468fa56b9fd41866d3dc267 + languageName: node + linkType: hard + "serialize-error@npm:^2.1.0": version: 2.1.0 resolution: "serialize-error@npm:2.1.0" @@ -25485,6 +27244,18 @@ __metadata: languageName: node linkType: hard +"serve-static@npm:^1.13.1": + version: 1.16.3 + resolution: "serve-static@npm:1.16.3" + dependencies: + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + parseurl: "npm:~1.3.3" + send: "npm:~0.19.1" + checksum: 10c0/36320397a073c71bedf58af48a4a100fe6d93f07459af4d6f08b9a7217c04ce2a4939e0effd842dc7bece93ffcd59eb52f58c4fff2a8e002dc29ae6b219cd42b + languageName: node + linkType: hard + "serve-static@npm:^1.16.2, serve-static@npm:~1.16.2": version: 1.16.2 resolution: "serve-static@npm:1.16.2" @@ -25518,6 +27289,13 @@ __metadata: languageName: node linkType: hard +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 10c0/9f8c1b2d800800d0b589de1477c753492de5c1548d4ade52f57f1d1f5e04af5481554d75ce5e5c43d4004b80a3eb714398d6907027dc0534177b7539119f4454 + languageName: node + linkType: hard + "set-function-length@npm:^1.2.2": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" @@ -25876,6 +27654,17 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^2.0.0": + version: 2.1.0 + resolution: "slice-ansi@npm:2.1.0" + dependencies: + ansi-styles: "npm:^3.2.0" + astral-regex: "npm:^1.0.0" + is-fullwidth-code-point: "npm:^2.0.0" + checksum: 10c0/c317b21ec9e3d3968f3d5b548cbfc2eae331f58a03f1352621020799cbe695b3611ee972726f8f32d4ca530065a5ec9c74c97fde711c1f41b4a1585876b2c191 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -26154,7 +27943,7 @@ __metadata: languageName: node linkType: hard -"stack-utils@npm:^2.0.6": +"stack-utils@npm:^2.0.3, stack-utils@npm:^2.0.6": version: 2.0.6 resolution: "stack-utils@npm:2.0.6" dependencies: @@ -26288,7 +28077,7 @@ __metadata: languageName: node linkType: hard -"string-length@npm:^4.0.2": +"string-length@npm:^4.0.1, string-length@npm:^4.0.2": version: 4.0.2 resolution: "string-length@npm:4.0.2" dependencies: @@ -26298,6 +28087,13 @@ __metadata: languageName: node linkType: hard +"string-natural-compare@npm:^3.0.1": + version: 3.0.1 + resolution: "string-natural-compare@npm:3.0.1" + checksum: 10c0/85a6a9195736be500af5d817c7ea36b7e1ac278af079a807f70f79a56602359ee6743ca409af6291b94557de550ff60d1ec31b3c4fc8e7a08d0e12cdab57c149 + languageName: node + linkType: hard + "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -26465,6 +28261,15 @@ __metadata: languageName: node linkType: hard +"strip-ansi@npm:^5.0.0": + version: 5.2.0 + resolution: "strip-ansi@npm:5.2.0" + dependencies: + ansi-regex: "npm:^4.1.0" + checksum: 10c0/de4658c8a097ce3b15955bc6008f67c0790f85748bdc025b7bc8c52c7aee94bc4f9e50624516150ed173c3db72d851826cd57e7a85fe4e4bb6dbbebd5d297fdf + languageName: node + linkType: hard + "strip-ansi@npm:^7.0.1": version: 7.1.2 resolution: "strip-ansi@npm:7.1.2" @@ -26558,6 +28363,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^1.0.5": + version: 1.1.2 + resolution: "strnum@npm:1.1.2" + checksum: 10c0/a0fce2498fa3c64ce64a40dada41beb91cabe3caefa910e467dc0518ef2ebd7e4d10f8c2202a6104f1410254cae245066c0e94e2521fb4061a5cb41831952392 + languageName: node + linkType: hard + "style-to-object@npm:^0.4.0": version: 0.4.4 resolution: "style-to-object@npm:0.4.4" @@ -27500,6 +29312,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.8.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5 + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A4.9.0-dev.20220903#optional!builtin": version: 4.9.0-dev.20220903 resolution: "typescript@patch:typescript@npm%3A4.9.0-dev.20220903#optional!builtin::version=4.9.0-dev.20220903&hash=1a91c8" @@ -27521,6 +29343,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430 + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.19.3 resolution: "uglify-js@npm:3.19.3" @@ -28629,6 +30461,13 @@ __metadata: languageName: node linkType: hard +"which-module@npm:^2.0.0": + version: 2.0.1 + resolution: "which-module@npm:2.0.1" + checksum: 10c0/087038e7992649eaffa6c7a4f3158d5b53b14cf5b6c1f0e043dccfacb1ba179d12f17545d5b85ebd94a42ce280a6fe65d0cbcab70f4fc6daad1dfae85e0e6a3e + languageName: node + linkType: hard + "which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19, which-typed-array@npm:^1.1.2": version: 1.1.19 resolution: "which-typed-array@npm:1.1.19" @@ -28826,6 +30665,15 @@ __metadata: languageName: node linkType: hard +"ws@npm:^6.2.3": + version: 6.2.6 + resolution: "ws@npm:6.2.6" + dependencies: + async-limiter: "npm:~1.0.0" + checksum: 10c0/24245efc3c09b6175df32ddb4a06f85432c4c88abb0e75f43f2551c1d5de456218b1b6c7685c414b8e03a68a167488e4aadf04a895681c8e01502025ce505742 + languageName: node + linkType: hard + "ws@npm:^7, ws@npm:^7.3.1, ws@npm:^7.5.10": version: 7.5.11 resolution: "ws@npm:7.5.11" @@ -28904,6 +30752,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^4.0.0": + version: 4.0.3 + resolution: "y18n@npm:4.0.3" + checksum: 10c0/308a2efd7cc296ab2c0f3b9284fd4827be01cfeb647b3ba18230e3a416eb1bc887ac050de9f8c4fd9e7856b2e8246e05d190b53c96c5ad8d8cb56dffb6f81024 + languageName: node + linkType: hard + "y18n@npm:^5.0.5": version: 5.0.5 resolution: "y18n@npm:5.0.5" @@ -28939,6 +30794,15 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.2.1": + version: 2.9.0 + resolution: "yaml@npm:2.9.0" + bin: + yaml: bin.mjs + checksum: 10c0/f340718df45e97a9551b9bf9dac61c80050bc464513b710debfb5067c380c8472e3b67809cffacb4ab5ffb5e66ef9310816c88b05f371cec60abfedd8c88e0a2 + languageName: node + linkType: hard + "yaml@npm:^2.6.1": version: 2.8.2 resolution: "yaml@npm:2.8.2" @@ -28948,6 +30812,16 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^18.1.2": + version: 18.1.3 + resolution: "yargs-parser@npm:18.1.3" + dependencies: + camelcase: "npm:^5.0.0" + decamelize: "npm:^1.2.0" + checksum: 10c0/25df918833592a83f52e7e4f91ba7d7bfaa2b891ebf7fe901923c2ee797534f23a176913ff6ff7ebbc1cc1725a044cc6a6539fed8bfd4e13b5b16376875f9499 + languageName: node + linkType: hard + "yargs-parser@npm:^20.2.2": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" @@ -28962,6 +30836,25 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^15.1.0": + version: 15.4.1 + resolution: "yargs@npm:15.4.1" + dependencies: + cliui: "npm:^6.0.0" + decamelize: "npm:^1.2.0" + find-up: "npm:^4.1.0" + get-caller-file: "npm:^2.0.1" + require-directory: "npm:^2.1.1" + require-main-filename: "npm:^2.0.0" + set-blocking: "npm:^2.0.0" + string-width: "npm:^4.2.0" + which-module: "npm:^2.0.0" + y18n: "npm:^4.0.0" + yargs-parser: "npm:^18.1.2" + checksum: 10c0/f1ca680c974333a5822732825cca7e95306c5a1e7750eb7b973ce6dc4f97a6b0a8837203c8b194f461969bfe1fb1176d1d423036635285f6010b392fa498ab2d + languageName: node + linkType: hard + "yargs@npm:^16.1.0": version: 16.2.0 resolution: "yargs@npm:16.2.0" @@ -28977,6 +30870,21 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^17.3.1": + version: 17.7.3 + resolution: "yargs@npm:17.7.3" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: 10c0/7a28572f7e785a57886e34fdbddb9b28756dec552e1453d5f6e7cdd00ad8721a4e8c4321d33683f5e61cacb36ad43258adbb48396b71ec4ed14abee0fc0d0c1f + languageName: node + linkType: hard + "yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" @@ -29051,6 +30959,22 @@ __metadata: languageName: node linkType: hard +"zod-validation-error@npm:^3.5.0 || ^4.0.0": + version: 4.0.2 + resolution: "zod-validation-error@npm:4.0.2" + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + checksum: 10c0/0ccfec48c46de1be440b719cd02044d4abb89ed0e14c13e637cd55bf29102f67ccdba373f25def0fc7130e5f15025be4d557a7edcc95d5a3811599aade689e1b + languageName: node + linkType: hard + +"zod@npm:^3.25.0 || ^4.0.0": + version: 4.4.3 + resolution: "zod@npm:4.4.3" + checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3 + languageName: node + linkType: hard + "zwitch@npm:^2.0.0": version: 2.0.4 resolution: "zwitch@npm:2.0.4"