Skip to content

Add UDS/RPC bridge benchmark suite with standalone app#55

Open
gmaclennan wants to merge 10 commits intomainfrom
claude/benchmark-uds-rpc-bridge-1Zahz
Open

Add UDS/RPC bridge benchmark suite with standalone app#55
gmaclennan wants to merge 10 commits intomainfrom
claude/benchmark-uds-rpc-bridge-1Zahz

Conversation

@gmaclennan
Copy link
Copy Markdown
Member

Summary

Introduces a comprehensive benchmarking suite for measuring UDS/RPC bridge performance in isolation from @comapeo/core. This includes a new standalone benchmark app (apps/benchmark/), a stripped-down bench-only backend (backend/index.bench.js), and supporting infrastructure for collecting and analyzing telemetry data.

Key Changes

  • New benchmark app (apps/benchmark/): A slim React Native app with UI for selecting payload sizes, running benchmark sweeps, and viewing results. Includes Maestro flows for automated testing across payload sizes (64B, 1KB, 64KB, 1MB).

  • Bench-only backend (backend/index.bench.js): Minimal Node.js entry point that reuses the production state machine but registers only echo and payload(sizeBytes) RPC methods, excluding @comapeo/core entirely to isolate bridge performance from application-layer noise.

  • Telemetry sink abstraction (backend/lib/telemetry-sink.js): Pluggable span recording interface with three implementations:

    • NoopSink: Zero-overhead default
    • JsonFileSink: NDJSON-to-disk for on-device results
    • HttpSink: Fire-and-forget POST for orchestrated runs
  • Boot span instrumentation (backend/lib/boot-spans.js): Wraps initialization phases with Sentry-shaped span taxonomy (boot.listen-control, boot.init, boot.construct) for consistent observability.

  • RPC server (backend/lib/bench-rpc.js): Minimal request/response handler that emits op:"rpc" spans with per-call metadata (payload size, duration).

  • Host-side receiver (scripts/lib/bench-receiver.ts): HTTP server that collects spans posted by the bench app over BrowserStack Local tunnel, generates per-run NDJSON files, and maintains a CSV summary for cross-device comparison.

  • Consumer isolation via Expo config plugin (apps/benchmark/plugins/with-comapeo-bench/): Idempotent mutations to Android Gradle and iOS Podfile that activate the bench flavor/resources only for the benchmark app, preventing bench artefacts from leaking into production consumer APKs/IPAs.

  • Build system updates:

    • backend/rollup.config.ts: Added BENCH=1 mode to emit bench bundle to sibling paths (android/src/bench/assets/nodejs-project/, ios/nodejs-project-bench/)
    • scripts/build-backend.ts: Added --bench flag to build bench-only bundle without re-packaging native binaries
    • android/build.gradle: Added bench product flavor with matching fallbacks for build types
    • ios/ComapeoCore.podspec: Conditional resource inclusion based on ENV['COMAPEO_BENCH']
  • Module exports: Exposed benchMessagePort from @comapeo/core-react-native for the benchmark app to communicate with the bench backend.

Notable Implementation Details

  • Sentry taxonomy reuse: Span shape and naming follow the Sentry integration plan (§7.4.2) so dashboards and analysis tools work for both transports; the plan's Phase 3 can adopt these helpers without call-site changes.

  • Idempotent Expo plugin: Uses sentinel-tag include checks to ensure expo prebuild re-runs don't duplicate mutations.

  • Fire-and-forget HTTP sink: Intentionally swallows receiver failures so the on-device experience remains unaffected when no host receiver is available.

  • Payload caching: Pre-allocates and caches synthesized payloads (up to 4 MiB) to avoid spending benchmark time in string generation.

  • Percentile calculation: Uses linear interpolation between closest ranks for p50/p95/p99 statistics over ~100 steady-state samples.

https://claude.ai/code/session_01SC1Sc9AvULHQkQSoQ2SMzJ

claude and others added 4 commits May 1, 2026 00:08
Plans an opt-in, host-app-driven Sentry integration covering:
- error capture across backend (Node), JS/RN, and native layers
- RPC tracing via @comapeo/ipc onRequestHook (mirrors comapeo-mobile)
- forwarding @comapeo/core OpenTelemetry spans (PR digidem/comapeo-core#1051)
- app-specific gating so non-CoMapeo consumers ship no Sentry traffic

https://claude.ai/code/session_01EcVXzczA1TVkhEkgUg9DKX
Closes the FGS-cold-start gap where the prior draft required RN to
be alive before backend Sentry could initialize:

- §4 reworked: Expo config plugin writes DSN/environment/release
  into Android manifest meta-data and iOS Info.plist at prebuild
  time. Native reads those at process start, no JS round-trip,
  before booting @sentry/node and @sentry/android.

- §7.4 added: native telemetry data design mapped onto Sentry
  primitives (breadcrumbs for state transitions, transaction +
  spans for boot/shutdown phases, captureMessage for timeouts,
  tags/contexts for cross-process attribution). Categorizes
  captures as essential vs opt-in and documents a hard
  never-capture list for PII.

- §9 added: persisted "capture application data" toggle with
  restart-to-activate semantics. Snapshot read at boot, embedded
  in the init frame; gates per-RPC spans, sync-session
  transactions, memory checkpoints, and storage-size sampling.
  Never unlocks the never-capture list.

- §10 phasing and §13 file-change list updated. New open
  questions added for release tagging, plugin no-op behavior,
  toggle UI, and boot sample rate.

https://claude.ai/code/session_01EcVXzczA1TVkhEkgUg9DKX
Adds a stripped bench backend (`backend/index.bench.js` + bench RPC
server with echo / payload methods) and a sibling `apps/benchmark/`
app that drives it through the same RN→native→Node UDS path as
production, isolating the framing / IPC / RPC bridge from
@comapeo/core init noise.

Consumer isolation is enforced three ways:
- the bench bundle lands at sibling paths (`android/src/bench/assets/`,
  `ios/nodejs-project-bench/`) the production flavor / podspec don't
  reference;
- a new Android `bench` productFlavor + iOS `ENV['COMAPEO_BENCH']`
  podspec toggle is opt-in only;
- `package.json` files array negates both bench paths so they cannot
  leak via `npm publish`.

`apps/benchmark/` does not check in `android/` or `ios/` — the new
`with-comapeo-bench` Expo config plugin re-applies the variant /
env-var / Xcode rename build phase wiring on every `expo prebuild`.

Standalone-runnable: NDJSON sink + on-screen p50/p95/p99 work without
any host-side infrastructure. Optional HTTP toggle posts spans to the
bundled `bench-receiver.ts` for orchestrated BrowserStack runs.
Maestro flows (bench-rpc + per-payload-size variants) drive the bench
end-to-end.

See `docs/uds-rpc-bridge-benchmark-plan.md` for the full design.

https://claude.ai/code/session_01SC1Sc9AvULHQkQSoQ2SMzJ
Three fixes surfaced when running the bench app end-to-end on a
Pixel 7a API 29 emulator:

- **Replace Android productFlavor with a project property.** The
  `bench` / `production` flavor dimension on the lib triggered AGP /
  Gradle 9 strict variant ambiguity in consuming Expo apps that don't
  declare matching flavors of their own (apps/expo#18315 etc.):
  `missingDimensionStrategy` + `matchingFallbacks` weren't enough to
  disambiguate `benchDebugApiElements` vs. `productionDebugApiElements`.
  The lib now reads `rootProject.findProperty('comapeoBench')` and
  swaps `assets.srcDirs` with `=` (assignment, not `srcDirs '<...>'`
  which AGP treats as additive). Also empties `src/debug/assets` when
  bench is active so the production debug bundle doesn't overlay
  bench in debug builds. The `with-comapeo-bench` config plugin
  switches from `withAppBuildGradle` to `withGradleProperties` and
  writes `comapeoBench=true` into the consuming app's
  `android/gradle.properties`.

- **Pin Expo modules to SDK 55.** `expo-file-system@19.0.18` and
  `expo-sharing@14.0.7` (the latest npm versions) are SDK-incompatible
  with Expo 55 and crashed the JS app at launch with a
  `NoClassDefFoundError: FilePermissionModuleInterface` autolinking
  failure. `npx expo install` resolves them to `~55.0.17` /
  `~55.0.18` which match the rest of the SDK.

- **Add `bench-rpc-ios.yaml` Maestro flow.** The Android flow's
  `clearState: true` triggers a deep-link confirmation dialog on iOS
  that blocks the rest of the run. The iOS flow drops `clearState`
  and dismisses the dialog with a guarded `runFlow.when` block.

Validation results on Pixel 7a API 29 emulator (debug build, RN-thread
RTT in ms, 100 iterations after 10-iteration warmup):

  size  n    p50   p95   p99
  64B   100  1.65  2.56  7.34
  1KB   100  1.68  2.76  4.45
  64KB  100  2.48  4.70  6.29

iOS run blocked by a pre-existing lifecycle issue
(`AppLifecycleDelegate.applicationDidBecomeActive` doesn't fire under
scene-based app lifecycle, so `NodeJSService.start()` is never called)
— same code path the example app uses, so this is not a bench
regression. Tracked separately.

https://claude.ai/code/session_01SC1Sc9AvULHQkQSoQ2SMzJ
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an isolated UDS/RPC bridge benchmarking setup (standalone Expo app + minimal bench backend bundle) plus host-side span collection, with build-time gating to prevent benchmark artefacts from leaking into normal consumer apps/packages.

Changes:

  • Exposes benchMessagePort from @comapeo/core-react-native and adds a bench-only backend entrypoint (backend/index.bench.js) with minimal RPC + span instrumentation helpers.
  • Introduces a standalone Expo benchmark app (apps/benchmark/) and Maestro flows to automate benchmark runs across payload sizes.
  • Adds build/packaging plumbing for a separate bench bundle output tree and a host-side HTTP receiver to collate NDJSON + CSV summaries.

Reviewed changes

Copilot reviewed 28 out of 34 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/index.ts Re-exports benchMessagePort from the module surface.
src/ComapeoCoreModule.ts Exposes the raw CoreMessagePort singleton as benchMessagePort.
scripts/lib/bench-receiver.ts Adds a localhost HTTP receiver that persists spans and rewrites a CSV summary.
scripts/build-backend.ts Adds --bench mode to build only the bench JS bundle into bench-specific output paths.
package.json Updates files allowlist to exclude bench output paths from publishing.
ios/ComapeoCore.podspec Adds ENV['COMAPEO_BENCH'] conditional resource selection for bench bundle.
e2e/.maestro/bench-rpc.yaml Maestro flow for the default benchmark sweep on Android.
e2e/.maestro/bench-rpc-ios.yaml iOS-specific Maestro flow variant (handles the “Open” dialog and avoids clearState).
e2e/.maestro/bench-payload-64KB.yaml Maestro flow for a 64KB-only payload run.
e2e/.maestro/bench-payload-64B.yaml Maestro flow for a 64B-only payload run.
e2e/.maestro/bench-payload-1MB.yaml Maestro flow for a 1MB-only payload run.
e2e/.maestro/bench-payload-1KB.yaml Maestro flow for a 1KB-only payload run.
docs/uds-rpc-bridge-benchmark-plan.md Adds a design/verification plan for the benchmark suite and consumer isolation.
backend/rollup.config.ts Adds BENCH=1 rollup mode and trims static assets for the bench bundle.
backend/lib/telemetry-sink.js Adds pluggable telemetry sinks and span helpers (startSpan).
backend/lib/boot-spans.js Adds startBootSpan helper with a fixed boot-phase taxonomy.
backend/lib/bench-rpc.js Adds a minimal bench RPC server (echo/payload) with payload caching and span emission.
backend/index.bench.js Adds bench-only node entrypoint reusing the lifecycle framing but skipping @comapeo/core.
apps/benchmark/tsconfig.json Bench app TS config + local path mapping to the working tree module source.
apps/benchmark/plugins/with-comapeo-bench/index.js Expo config plugin to opt an app into bench resources (Gradle property + Podfile env + Xcode rename script).
apps/benchmark/package.json Benchmark app package manifest + dependencies and run scripts.
apps/benchmark/metro.config.js Metro config (mirrors example) for monorepo-style dev and avoiding duplicate peers.
apps/benchmark/index.ts Bench app entrypoint registering root component.
apps/benchmark/babel.config.js Bench app Babel config.
apps/benchmark/assets/splash-icon.png Bench app splash asset.
apps/benchmark/assets/icon.png Bench app icon asset.
apps/benchmark/assets/favicon.png Bench app favicon asset.
apps/benchmark/assets/adaptive-icon.png Bench app adaptive icon asset.
apps/benchmark/app.json Bench app Expo config + plugin wiring.
apps/benchmark/App.tsx Bench UI + RPC client + NDJSON writing + optional POST-to-receiver flow.
apps/benchmark/.gitignore Ignores generated native folders and local Expo/Metro artifacts for the bench app.
android/build.gradle Adds comapeoBench property gate to swap module asset source dirs for bench bundle selection.
.gitignore Ignores bench bundle output dirs alongside the existing production bundle outputs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/benchmark/App.tsx Outdated
} from "@comapeo/core-react-native";
import { Directory, File, Paths } from "expo-file-system";
import * as Sharing from "expo-sharing";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useMemo is imported but never used in this file. This will trip lint/TS unused import checks; please remove it or use it.

Suggested change
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";

Copilot uses AI. Check for mistakes.
Comment thread apps/benchmark/App.tsx Outdated
Comment on lines +113 to +118
// Linear interpolation between closest ranks. For our sample sizes
// (~100), `Math.floor((n-1) * p)` is good enough and avoids the
// off-by-one trap of `Math.floor(n * p)` (which would index past the
// end at p=1).
const idx = Math.floor((sortedAsc.length - 1) * p);
return sortedAsc[idx]!;
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

percentile() claims linear interpolation, but the implementation returns a nearest-rank element (Math.floor((n-1)*p)). Either implement the stated interpolation (so p95/p99 match the documented method) or adjust the comment/docs to match the actual behavior.

Suggested change
// Linear interpolation between closest ranks. For our sample sizes
// (~100), `Math.floor((n-1) * p)` is good enough and avoids the
// off-by-one trap of `Math.floor(n * p)` (which would index past the
// end at p=1).
const idx = Math.floor((sortedAsc.length - 1) * p);
return sortedAsc[idx]!;
// Linear interpolation between closest ranks.
const position = (sortedAsc.length - 1) * p;
const lowerIdx = Math.floor(position);
const upperIdx = Math.ceil(position);
if (lowerIdx === upperIdx) return sortedAsc[lowerIdx]!;
const lower = sortedAsc[lowerIdx]!;
const upper = sortedAsc[upperIdx]!;
const weight = position - lowerIdx;
return lower + (upper - lower) * weight;

Copilot uses AI. Check for mistakes.
Comment thread apps/benchmark/App.tsx Outdated
Comment on lines +101 to +107
request(method: string, params?: unknown): Promise<{ result?: unknown; error?: { message: string } }> {
this.ensureListener();
const id = `bench-${this.nextId++}`;
return new Promise((resolve) => {
this.pending.set(id, resolve);
benchMessagePort.postMessage({ id, method, params } as never);
});
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BenchClient.request() has no timeout or rejection path. If the backend never replies (crash, disconnect, lost frame), the promise will never resolve and the benchmark run can hang indefinitely with the pending map retaining entries. Consider adding a per-request timeout (and cleanup) and/or handling a messageerror/disconnect signal to fail fast.

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +80
function percentile(sortedAsc: number[], p: number): number {
if (sortedAsc.length === 0) return Number.NaN;
return sortedAsc[Math.floor((sortedAsc.length - 1) * p)]!;
}
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

percentile() currently uses a nearest-rank lookup (Math.floor((n-1)*p)). The PR description/plan mention linear interpolation for p50/p95/p99; if that's the intended definition, this summary CSV will not match it. Either implement the intended interpolation here or document that the receiver uses nearest-rank percentiles.

Copilot uses AI. Check for mistakes.
Comment thread docs/uds-rpc-bridge-benchmark-plan.md Outdated
Comment on lines +98 to +104
- Android: the plugin uses `withAppBuildGradle` to append
`flavorDimensions += "comapeo"` and
`missingDimensionStrategy 'comapeo', 'bench'` to the bench app's
`android/app/build.gradle` `defaultConfig`. The module's own
`android/build.gradle` declares the `bench` flavor + sourceSet;
consumers that don't activate it (`apps/example/`, third-party
apps) get the default flavor and never see `src/bench/`.
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc describes Android consumer isolation in terms of a bench productFlavor and missingDimensionStrategy, but the implementation in this PR uses a comapeoBench=true Gradle property (set via withGradleProperties) to swap assets.srcDirs instead. Please update this section so the plan matches what actually shipped (otherwise it's misleading for anyone following it).

Copilot uses AI. Check for mistakes.
Comment thread apps/benchmark/App.tsx Outdated
Comment on lines +23 to +26
* stripped `backend/index.bench.js` (via the `bench` Android
* productFlavor / `ENV['COMAPEO_BENCH']` iOS opt-in) — so timings
* isolate the framing / IPC / JSON-RPC bridge from `@comapeo/core` init
* noise. See `docs/uds-rpc-bridge-benchmark-plan.md`.
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header comment says the bench backend is selected via an Android bench productFlavor, but the actual wiring in this PR uses the comapeoBench=true Gradle property (see android/build.gradle). Please update the comment so it matches the implementation.

Suggested change
* stripped `backend/index.bench.js` (via the `bench` Android
* productFlavor / `ENV['COMAPEO_BENCH']` iOS opt-in) so timings
* isolate the framing / IPC / JSON-RPC bridge from `@comapeo/core` init
* noise. See `docs/uds-rpc-bridge-benchmark-plan.md`.
* stripped `backend/index.bench.js` (via the `comapeoBench=true`
* Gradle property on Android / `ENV['COMAPEO_BENCH']` iOS opt-in)
* so timings isolate the framing / IPC / JSON-RPC bridge from
* `@comapeo/core` init noise. See
* `docs/uds-rpc-bridge-benchmark-plan.md`.

Copilot uses AI. Check for mistakes.
Comment thread apps/benchmark/App.tsx Outdated
* noise. See `docs/uds-rpc-bridge-benchmark-plan.md`.
*
* UI surface:
* - boot status (state observer): waits for "READY" before enabling
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI comment says the app waits for "READY" before enabling the run button, but the code gates on serviceState === "STARTED" (and ComapeoState doesn't include "READY"). Please update the comment to avoid confusion about which state is required for RPC.

Suggested change
* - boot status (state observer): waits for "READY" before enabling
* - boot status (state observer): waits for "STARTED" before enabling

Copilot uses AI. Check for mistakes.
Comment thread scripts/build-backend.ts Outdated
Comment on lines +46 to +49
// Bench bundle output. Lives under `src/bench/assets/` so AGP's
// per-flavor sourceSet merging picks it up only when the consuming app
// has activated the `bench` productFlavor (see android/build.gradle —
// `apps/benchmark/` activates this; `apps/example/` does not).
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says the bench Android assets are picked up via a bench productFlavor/sourceSet merge, but the module now switches assets via the comapeoBench Gradle property (see android/build.gradle). Please update the comment so it matches the current mechanism.

Copilot uses AI. Check for mistakes.
Comment thread backend/rollup.config.ts Outdated
Comment on lines +44 to +45
* - `android/src/bench/assets/nodejs-project/` (overlaid by the
* `bench` Android productFlavor — see android/build.gradle)
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bench-mode comment refers to the Android bench productFlavor for asset overlay, but Android selection is now controlled by the comapeoBench Gradle property (not flavors). Please update the comment to avoid sending readers to a mechanism that no longer exists.

Suggested change
* - `android/src/bench/assets/nodejs-project/` (overlaid by the
* `bench` Android productFlavor see android/build.gradle)
* - `android/src/bench/assets/nodejs-project/` (selected by the
* Android build when the `comapeoBench` Gradle property is enabled;
* this is no longer controlled by an Android productFlavor)

Copilot uses AI. Check for mistakes.
claude and others added 2 commits May 1, 2026 17:10
BLOCKER (iOS rename ordering): the previous design added an Xcode Run
Script build phase via the config plugin's `withXcodeProject`, but
CocoaPods 1.x doesn't reliably position user script phases after
`[CP] Copy Pods Resources` — the rename ran before the bench files
were on disk and silently no-op'd, leaving bench builds with no
`<App>.app/nodejs-project/` and a non-bootable runtime. Switch to
pod-install-time staging in `ComapeoCore.podspec`: when COMAPEO_BENCH=1
the podspec stages a copy of `nodejs-project-bench/` to
`.bench-staging/nodejs-project/` and adds it to `s.resources`
ALONGSIDE the production `nodejs-project/`. CocoaPods rsyncs both into
`<App>.app/nodejs-project/` in declaration order, with the bench
overlay landing on top — no script phase, no ordering footgun.

MAJOR (iOS resource fallback): previous design REPLACED `nodejs-project`
with `nodejs-project-bench`, so any rename failure left the app
non-bootable. New shape ships both: bench overlays prod, but if the
bench bundle is missing (forgot to run `--bench`) the prod bundle
remains as fallback.

MAJOR (shutdown race): an in-flight `SocketMessagePort.postMessage`
landing in streamx's deferred microtask after the AF_UNIX socket has
been ended raises `ERR_STREAM_WRITE_AFTER_END` past every listener.
The race is benign (the message was already destined for a torn-down
peer). Add a state-check + underlying-socket error listener in
`message-port.js`, and a targeted `uncaughtException` /
`unhandledRejection` filter in `index.bench.js` that swallows the
specific code while a graceful shutdown is in progress. Smoke test
now exits 0 with all spans + responses recorded; previous run hit
`fatal during runtime` and exit 1.

Copilot review feedback addressed:
- App.tsx: drop unused `useMemo`; replace nearest-rank percentile
  with linear-interpolation (matches PR description); add 30s
  per-request timeout + pending-map cleanup so a lost frame doesn't
  hang the run; update stale "READY" comment to "STARTED".
- bench-receiver.ts: same linear-interpolation fix so on-device and
  host-side numbers agree.
- Stale productFlavor / withXcodeProject references in App.tsx,
  scripts/build-backend.ts, backend/rollup.config.ts, and the plan
  doc updated to describe the actual `comapeoBench` Gradle property
  + podspec staging mechanism.

https://claude.ai/code/session_01SC1Sc9AvULHQkQSoQ2SMzJ
…rpc-bridge-1Zahz

* origin/main:
  fix(android): fold waitForFile into connect retry loop (#52)
@gmaclennan gmaclennan force-pushed the claude/benchmark-uds-rpc-bridge-1Zahz branch from 47024a2 to 5acd807 Compare May 5, 2026 10:56
@gmaclennan gmaclennan changed the base branch from claude/plan-sentry-integration-9dt0T to main May 5, 2026 10:59
gmaclennan and others added 4 commits May 5, 2026 12:45
Adds a generic config knob for consumers that ship their own backend
JS bundle: `comapeoBackendDir` Gradle property → BuildConfig field on
Android, `ComapeoBackendDir` Info.plist key on iOS. Default is
`nodejs-project` so behavior is unchanged for current consumers.

This unblocks moving bench-specific wiring out of the module: the bench
app can now ship its bundle in a sibling directory and just flip this
override, instead of relying on an in-module `comapeoBench=true` toggle
that swaps Android sourceSets and runs an iOS pod-install staging copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves all bench-only backend source (`index.bench.js`, `bench-rpc.js`,
`boot-spans.js`, `telemetry-sink.js`) and its rollup config out of the
production module and into `apps/benchmark/backend/`. The bench bundle
is built from there with its own simplified rollup config: one ESM
output, no per-platform split, no native-addon banner (the bench code
imports no addons). Shared framing helpers (server-helper.js,
simple-rpc.js, message-port.js) stay in the module's `backend/lib/`
and are path-imported from the bench source so wire framing stays
bit-identical to production.

Rewrites `with-comapeo-bench` plugin against the new
`comapeoBackendDir` override hook: drops `comapeoBench=true` Gradle
toggle, drops `ENV['COMAPEO_BENCH']` Podfile mutation, drops the iOS
`.bench-staging` rsync trick. Now sets the override property/Info.plist
key and copies the bench bundle into the consumer app's own native
asset/resource trees (Android assets dir + iOS folder reference). Same
shape as `expo-asset`'s plugin, minus its file-extension allowlist and
flat-structure constraints which don't fit a JS bundle.

Strips `BENCH=1` mode from the module's rollup.config.ts and `--bench`
mode from scripts/build-backend.ts. Dead bench wiring still in the
module (`android/src/bench/`, `ios/nodejs-project-bench/`, podspec env
branch) is removed in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With the bench app moved to apps/benchmark/ and using the new
comapeoBackendDir override hook, the production module no longer
needs:

- comapeoBench Gradle property + conditional sourceSet swap in
  android/build.gradle (sourceSets revert to AGP defaults)
- ENV['COMAPEO_BENCH'] branch + .bench-staging rsync in
  ios/ComapeoCore.podspec (s.resources is just ['nodejs-project'])
- !android/src/bench/ and !ios/nodejs-project-bench/ exclusions in
  package.json files (those dirs no longer exist in the module)
- Bench-specific .gitignore entries

Also removes the (build-artifact, gitignored) android/src/bench/ and
ios/nodejs-project-bench/ directories, and updates two stale comments
in retained source files plus a header note in the planning doc
pointing at the v2 implementation in apps/benchmark/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The export was always misnamed: it isn't a benchmark-specific API,
it's the raw `MessagePort`-shaped escape hatch one level below the
`comapeo` client. Anything paired with a custom backend bundle (the
bench app being the canonical example) goes through this port.

`unstable_` matches React's `unstable_batchedUpdates` /
`unstable_setExceptionDecorator` convention — signals "may change
without notice" without burning the API on a name like
`INTERNAL_messagePort` that implies stronger guarantees about
internal-only access. Lowercase because it's an instance, not a class.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants