Skip to content

feat(perf): performance lab with baselines and bottleneck scorecard - #73

Closed
jkasprzyk17 wants to merge 6 commits into
mainfrom
feat/performance-lab
Closed

feat(perf): performance lab with baselines and bottleneck scorecard#73
jkasprzyk17 wants to merge 6 commits into
mainfrom
feat/performance-lab

Conversation

@jkasprzyk17

@jkasprzyk17 jkasprzyk17 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

A production-grade performance lab for react-native-better-maps, plus the first baselines and a measured bottleneck report. No optimization is applied; the library changes are compile-time timing probes (no-ops unless a build opts in) and the two build fixes this machine needed (podspec lambdas, Android codegen root).

  • performance/ — seeded fixtures (pinned hashes), 97 scenarios (markers 100…100k, camera × marker count, marker mutation benchmark, polylines/polygons 100…100k points, clustering 1k…100k, combined workloads, 5/15-minute stability), an in-app runner and a bun perf CLI (list, run, baseline, compare, check, report, scorecard, bench, fixtures, build, install).
  • example/modules/perf-lab — local Expo module: CADisplayLink/Choreographer frame intervals (+ Android FrameMetrics), memory (phys_footprint/PSS, Java/native heap, ART GC counters, malloc blocks), CPU, thermal/battery, a native ping of the JS message queue (Android), probe drain, result files and log lines.
  • PerfProbe.swift / PerfProbe.kt — spans around marker set/fingerprint/index/viewport/cluster/diff/apply, shape setters, camera/region, annotation views. Compiled in only with betterMaps.perfProbes (Podfile.properties) / -PNitroMaps_perfProbes=true; release builds carry nothing.
  • performance/PERFORMANCE.md — generated scorecard for both baselines, top 10 bottlenecks with evidence, ranked backlog, and the recommended first optimization.

Baselines recorded

Target Build Representative Runs
iPhone 17 Pro simulator, iOS 26.5, MapKit release + probes no (simulator) 2 (repeatability checked)
Android emulator API 35 arm64, Google Maps release + probes no (emulator) 3
Realme RMX3081 (Android 13) APK built would be not recorded — device locked with a secure lock screen and offline during the session

Headline findings (details and numbers in PERFORMANCE.md)

  1. Updating 1 marker out of 10k costs the same JS commit as updating all 10k (exponent −0.01): ~12–17 ms per update on desktop-class CPUs, ~1–1.8 MB allocated per update — the whole array is re-parsed by Nitro and re-normalized.
  2. On Android the full array is additionally copied into Java objects on the UI thread: commit → native latency 13–16 ms per 10k update vs 1.1–1.5 ms on iOS.
  3. Native re-fingerprints and re-indexes all markers per update; the array is applied twice and fingerprinted three times at mount on iOS.
  4. The Swift cluster engine is ~7× slower than the Kotlin one at 50k (p95 144 ms vs 16–20 ms per refresh) — copy-on-write bucket copies in the grid pass.
  5. Shape overlays are destroyed and re-created on every update, including style-only changes (46 ms per update of 200 polygons on iOS).
  6. Zoom changes apply up to ~2k marker adds/removes in one main-thread diff (p95 up to 15–24 ms on the emulator).

Recommended first optimization: an id-keyed delta update path next to the bulk markers prop (P0); the mutations-10k scenario measures it directly.

How to run

bun perf build android --install     # or: bun perf build ios --install
bun perf run --suite quick --platform android
bun perf baseline --platform android
bun perf compare && bun perf check   # thresholds in performance/perf.config.ts, warn-only until baselines are proven stable

Validation

  • bun run lint, bun run typecheck, bun run typecheck:provider-types, bun run build, package tests (32) and lab tests (22, bun run perf:test) pass.
  • Library compiles with and without the probe flag (Kotlin) and the iOS release build with probes was used for the baselines.
  • Relationship to the open harness PR feat(example): add a frame-time benchmark harness #66: this lab is broader (deterministic fixtures, CLI, baselines, compare/check, per-layer probes) and lives on main, so it measures the unoptimized library; the scenarios in feat(example): add a frame-time benchmark harness #66 map onto markers-*, camera-*, cluster-*, polyline-*/polygon-* here.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…defs

CocoaPods evaluates a podspec with eval, and on Ruby 4.0.6 + CocoaPods 1.17.0
a method defined that way is not visible inside the Pod::Spec.new block, so
pod install fails with "undefined method 'better_maps_ios_google_provider_enabled?'
for module Pod". Hold the helpers in local lambdas instead; behavior is unchanged.
Release builds failed with "Type com.facebook.fbreact.specs.NativeAccessibilityInfoSpec
is defined multiple times". The library applies com.facebook.react, whose codegen
root defaults to the package directory; with an isolated installer (bun, pnpm)
that directory contains node_modules/react-native, so the plugin generated React
Native's own core specs into this library and they collided with react-android
when the release dex was merged. Debug builds hide it because project and
library dex files are merged separately. Point jsRootDir at src, which holds no
React Native codegen specs; nitrogen generates this library's bindings.
…peline

Timing probes around marker set/fingerprint/index/viewport/cluster/diff/apply,
shape setters, camera and region application, MapKit annotation views and
Google marker visuals. They compile in only with -DNITROMAPS_PERF_PROBES
(betterMaps.perfProbes in Podfile.properties.json) on iOS and
-PNitroMaps_perfProbes=true (BuildConfig.PERF_PROBES) on Android; otherwise
every call site is an inlined no-op or a folded constant check. Profile builds
also emit os_signpost intervals and android.os.Trace sections. A small
Objective-C/reflection bridge lets the performance lab drain the spans.

Adds opt-in JVM and XCTest micro-benchmarks for the pipeline's pure functions
and guards the Google-only iOS test with canImport(GoogleMaps).
A reproducible profiling and benchmarking environment under performance/:
seeded fixtures with pinned hashes, 97 scenarios across markers, camera,
mutations, geometry, clustering, combined and stability workloads, an in-app
runner that records display-link/Choreographer frame intervals, JS-thread lag
(animation frames on iOS, a native JS message-queue ping on Android), React
commit timing, commit-to-native latency, native probe spans, memory, Hermes
and ART allocation counters, CPU and thermal state, and the JS-to-native
transfer profile per scenario. A bun perf CLI builds the lab variant of the
example app, drives runs over a deep link, harvests results from the device
log and result files, stores baselines, compares runs and applies regression
thresholds. The lab UI ships only with EXPO_PUBLIC_PERF_LAB=1 and lives in
the example app plus a local Expo module; nothing in the published package
changes.
…k scorecard

Baselines for the iPhone 17 Pro simulator (MapKit) and the Android API 35
arm64 emulator (Google Maps), both release builds with probes, plus JVM
pipeline and bun micro-benchmark output. PERFORMANCE.md holds the generated
scorecard, the top ten measured bottlenecks with evidence, the optimization
backlog and the first optimization to attempt. No optimization is applied.
Simulator and emulator numbers are marked not production-representative; the
physical-device baseline is still pending.
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 194 files, which is 44 over the limit of 150.

To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to Team to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: d658e174-d652-485b-a84e-d45e16eb903c

📥 Commits

Reviewing files that changed from the base of the PR and between 1c38c93 and 2ee7e78.

📒 Files selected for processing (194)
  • .gitignore
  • .prettierignore
  • CONTRIBUTING.md
  • docs/roadmap.md
  • eslint.config.mjs
  • example/app.json
  • example/index.js
  • example/modules/perf-lab/android/build.gradle
  • example/modules/perf-lab/android/src/main/AndroidManifest.xml
  • example/modules/perf-lab/android/src/main/java/expo/modules/perflab/FrameRecorder.kt
  • example/modules/perf-lab/android/src/main/java/expo/modules/perflab/JsQueueProbe.kt
  • example/modules/perf-lab/android/src/main/java/expo/modules/perflab/PerfLabModule.kt
  • example/modules/perf-lab/android/src/main/java/expo/modules/perflab/ProbeBridge.kt
  • example/modules/perf-lab/android/src/main/java/expo/modules/perflab/ProcessStats.kt
  • example/modules/perf-lab/expo-module.config.json
  • example/modules/perf-lab/index.ts
  • example/modules/perf-lab/ios/FrameRecorder.swift
  • example/modules/perf-lab/ios/PerfLab.podspec
  • example/modules/perf-lab/ios/PerfLabModule.swift
  • example/modules/perf-lab/ios/ProbeBridge.swift
  • example/modules/perf-lab/ios/ProcessStats.swift
  • example/modules/perf-lab/package.json
  • example/modules/perf-lab/src/PerfLabNative.ts
  • example/tsconfig.json
  • package.json
  • package/android/build.gradle
  • package/android/src/main/java/com/margelo/nitro/nitromaps/GoogleMapProviderAdapter.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/PerfProbe.kt
  • package/android/src/test/java/com/margelo/nitro/nitromaps/PipelineBenchmarkTest.kt
  • package/ios/AppleMapProviderAdapter.swift
  • package/ios/GoogleMapOverlayController.swift
  • package/ios/GoogleMapProviderAdapter.swift
  • package/ios/HybridMapViewDelegate.swift
  • package/ios/MapOverlayController.swift
  • package/ios/MarkerClusterEngine.swift
  • package/ios/PerfProbe.swift
  • package/iosTests/GoogleMarkerVisualApplierTests.swift
  • package/iosTests/PipelineBenchmarkTests.swift
  • package/react-native-better-maps.podspec
  • performance/PERFORMANCE.md
  • performance/README.md
  • performance/app/MapHost.tsx
  • performance/app/PerfLabApp.tsx
  • performance/app/__tests__/deepLink.test.ts
  • performance/app/__tests__/frameStats.test.ts
  • performance/app/__tests__/probes.test.ts
  • performance/app/__tests__/publish.test.ts
  • performance/app/base64.ts
  • performance/app/deepDiffer.ts
  • performance/app/deepLink.ts
  • performance/app/metrics/frameStats.ts
  • performance/app/metrics/hermes.ts
  • performance/app/metrics/jsThread.ts
  • performance/app/metrics/memory.ts
  • performance/app/metrics/probes.ts
  • performance/app/metrics/stats.ts
  • performance/app/metrics/transfer.ts
  • performance/app/publish.ts
  • performance/app/result.ts
  • performance/app/runner.ts
  • performance/benchmarks/collection.bench.test.ts
  • performance/benchmarks/geometry.bench.test.ts
  • performance/benchmarks/lib/bench.ts
  • performance/benchmarks/native/README.md
  • performance/benchmarks/serialization.bench.test.ts
  • performance/fixtures/__tests__/fixtures.test.ts
  • performance/fixtures/hash.ts
  • performance/fixtures/index.ts
  • performance/fixtures/markers.ts
  • performance/fixtures/mutations.ts
  • performance/fixtures/polygons.ts
  • performance/fixtures/polylines.ts
  • performance/fixtures/prng.ts
  • performance/fixtures/regions.ts
  • performance/perf.config.ts
  • performance/results/README.md
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-continuous-pan-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-fast-pan-0.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-fast-pan-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-fast-pan-1k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-fast-pan-50k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-idle-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-pitch-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-rapid-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-rapid-zoom-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-rotate-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-slow-pan-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-zoom-in-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/camera-zoom-out-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/cluster-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/cluster-1k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/cluster-50k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/combined-10k-camera.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/combined-10k-cluster-camera.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/combined-10k-polygon.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/combined-10k-polyline.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/combined-10k-updates.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/combined-all.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/markers-100.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/markers-10k-rich.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/markers-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/markers-1k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/markers-50k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/markers-children-1k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/mutations-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/mutations-1k-children.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/mutations-continuous-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/mutations-continuous-1k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/polygon-100.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/polygon-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/polygon-1k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/polygons-200x20.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/polyline-100.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/polyline-100k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/polyline-10k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/polyline-1k.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/polylines-200x50.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/run.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/stability-5m.json
  • performance/results/baseline/android/google-sdk-gphone64-arm64-release-probes/summary.md
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-continuous-pan-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-fast-pan-0.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-fast-pan-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-fast-pan-1k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-fast-pan-50k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-idle-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-pitch-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-rapid-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-rapid-zoom-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-rotate-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-slow-pan-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-zoom-in-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/camera-zoom-out-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/cluster-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/cluster-1k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/cluster-50k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/combined-10k-camera.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/combined-10k-cluster-camera.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/combined-10k-polygon.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/combined-10k-polyline.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/combined-10k-updates.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/combined-all.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/markers-100.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/markers-10k-rich.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/markers-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/markers-1k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/markers-50k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/markers-children-1k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/mutations-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/mutations-1k-children.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/mutations-continuous-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/mutations-continuous-1k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/polygon-100.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/polygon-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/polygon-1k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/polygons-200x20.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/polyline-100.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/polyline-100k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/polyline-10k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/polyline-1k.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/polylines-200x50.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/run.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/stability-5m.json
  • performance/results/baseline/ios/apple-iphone18-1-release-probes/summary.md
  • performance/results/baseline/js-bench/collection.json
  • performance/results/baseline/js-bench/geometry.json
  • performance/results/baseline/js-bench/serialization.json
  • performance/results/baseline/native/jvm-pipeline.jsonl
  • performance/results/bench/.gitkeep
  • performance/results/runs/.gitkeep
  • performance/scenarios/camera.ts
  • performance/scenarios/clustering.ts
  • performance/scenarios/combined.ts
  • performance/scenarios/geometry.ts
  • performance/scenarios/helpers.ts
  • performance/scenarios/index.ts
  • performance/scenarios/markers.ts
  • performance/scenarios/mutations.ts
  • performance/scenarios/stability.ts
  • performance/scenarios/types.ts
  • performance/scripts/__tests__/compare.test.ts
  • performance/scripts/build-android.sh
  • performance/scripts/build-ios.sh
  • performance/scripts/lib/compare.mjs
  • performance/scripts/lib/devices.mjs
  • performance/scripts/lib/digest.mjs
  • performance/scripts/lib/evidence.mjs
  • performance/scripts/lib/logs.mjs
  • performance/scripts/lib/metrics.mjs
  • performance/scripts/lib/results.mjs
  • performance/scripts/lib/util.mjs
  • performance/scripts/perf.mjs

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

React Doctor found 6 issues in 3 files · 2 errors & 4 warnings · score 64 / 100 (Needs work) · full project

Errors

4 warnings

App.tsx

  • ⚠️ L729 Side effect inside a state updater function no-side-effect-in-state-updater-function
  • ⚠️ L734 Side effect inside a state updater function no-side-effect-in-state-updater-function
  • ⚠️ L735 Side effect inside a state updater function no-side-effect-in-state-updater-function

src/components/MapView.tsx

  • ⚠️ L31 React function has high control-flow complexity no-high-complexity-react-function

Reviewed by React Doctor for commit 2ee7e78. See inline comments for fixes.

The root .gitignore ignores every lib/ directory (library build output), which
silently dropped performance/scripts/lib and performance/benchmarks/lib from
the previous commit; the CLI and the micro-benchmarks import from them.
@jkasprzyk17
jkasprzyk17 deleted the feat/performance-lab branch September 10, 2026 14:49
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.

1 participant