perf(flutter): Move Android JNI work to core worker to avoid work on main isolate#3713
Conversation
Send large Android scope payloads as JSON bytes instead of recursively constructing Java maps and lists through JNI. This keeps nested user data structured while reducing per-entry JNI object churn. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Clarify that large or arbitrary Dart collection payloads should cross JNI as JSON bytes, while primitives and small controlled payloads can use direct conversion. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Keep the Android JSON reader helper within ktlint formatting limits after adding the scope sync byte-array bridge. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Wrap all native JSON conversion when branches consistently so ktlint accepts the multiline Kotlin helper added for Android scope sync. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Use the existing native boundary normalizer before encoding Android scope payloads as JSON bytes instead of maintaining a second normalization helper. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Keep null values when converting JSON object and array payloads on Android so the bridge remains lossless before native model deserialization. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Cache JSON deserializers for scope sync payloads and replace the broad Any extension with a private helper function for Kotlin JSON conversion. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Use the Java SDK JSON reader for context payload parsing instead of a custom recursive org.json conversion helper. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Document that the Android JSON reader accepts root-level primitives so future changes do not replace it with object-only parsing. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Route Android context values through sentry-java's typed overloads so primitive Dart context values are serialized as valid context objects. Regenerate JNI bindings after removing the unused object overload entrypoint. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Release replay callback JNI handles after processing replay privacy options, and avoid creating duplicate JNI strings when loading debug images. This reduces leaked global refs in Android replay and debug image paths. Fixes #3696 Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Update the native context sync test to match the Android bridge's valid serialized shape for primitive context values. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Use a single arena for replay callback JNI temporaries and avoid releasing map keys before removing privacy options from the payload. Refs #3696 Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
…r-android-jni-leaks
Route Android native scope and context work through the core worker so JNI calls can run off the main isolate when the worker is available. Keep current-isolate fallbacks for calls made before startup. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Semver Impact of This PR🟢 Patch (bug fixes) 📋 Changelog PreviewThis is how your changes will appear in the changelog. FixesDart
Flutter
EnhancementsFlutter
Internal Changes
🤖 This preview updates automatically when you update the PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3713 +/- ##
==========================================
+ Coverage 86.96% 91.57% +4.60%
==========================================
Files 336 105 -231
Lines 11982 3737 -8245
==========================================
- Hits 10420 3422 -6998
+ Misses 1562 315 -1247
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
…/ref/android-core-worker-fire-forget
Co-Authored-By: OpenAI <[email protected]>
There was a problem hiding this comment.
Pull request overview
Moves Android JNI-heavy operations (envelope capture, debug image loading, native contexts, and scope updates) behind a new AndroidCoreWorker that can run on a background isolate when available, reducing main-isolate blocking and preserving synchronous fallbacks before worker startup.
Changes:
- Replace
AndroidEnvelopeSenderwith a broaderAndroidCoreWorkerthat serializes JNI work in a worker isolate. - Delegate
SentryNativeJavaJNI entrypoints (envelopes, debug images, contexts, breadcrumb/user/context updates) through the worker. - Update/remove/replace real VM tests to validate worker request/response and one-way messaging behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/flutter/lib/src/native/java/android_core_worker.dart | Introduces the new worker, message/request types, and JNI implementations. |
| packages/flutter/lib/src/native/java/android_envelope_sender.dart | Removes the old dedicated envelope-sender worker implementation. |
| packages/flutter/lib/src/native/java/sentry_native_java.dart | Switches Android JNI calls to delegate to AndroidCoreWorker. |
| packages/flutter/lib/src/native/java/sentry_native_java_init.dart | Adjusts replay callback JNI handle management and masking-option updates. |
| packages/flutter/test/native/android_core_worker_test.dart | Updates conditional import wiring to the new worker test entrypoint. |
| packages/flutter/test/native/android_core_worker_test_web.dart | Updates web “stub” test file references to the new real test file. |
| packages/flutter/test/native/android_core_worker_test_real.dart | Adds VM tests covering worker spawn/config, requests, and one-way updates. |
| packages/flutter/test/native/android_envelope_sender_test_real.dart | Deletes obsolete VM tests for the removed AndroidEnvelopeSender. |
| packages/flutter/test/native/sentry_native_java_test_real.dart | Updates initialization test to use the new AndroidCoreWorker.factory. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Android Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 3f47ea3 | 368.20 ms | 388.14 ms | 19.94 ms |
| cdf371b | 367.64 ms | 377.02 ms | 9.38 ms |
| 3615e19 | 468.38 ms | 504.71 ms | 36.33 ms |
| 78c3c07 | 393.32 ms | 394.94 ms | 1.61 ms |
| 1777727 | 438.67 ms | 447.11 ms | 8.44 ms |
| 1f639ee | 429.98 ms | 476.60 ms | 46.62 ms |
| 54acf91 | 487.24 ms | 529.60 ms | 42.36 ms |
| 3135a81 | 424.33 ms | 423.63 ms | -0.70 ms |
| 1fff351 | 423.68 ms | 408.96 ms | -14.72 ms |
| 8af916c | 367.83 ms | 379.60 ms | 11.77 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 3f47ea3 | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| cdf371b | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| 3615e19 | 6.54 MiB | 7.70 MiB | 1.16 MiB |
| 78c3c07 | 14.30 MiB | 15.49 MiB | 1.19 MiB |
| 1777727 | 14.30 MiB | 15.49 MiB | 1.19 MiB |
| 1f639ee | 13.93 MiB | 15.00 MiB | 1.06 MiB |
| 54acf91 | 6.54 MiB | 7.70 MiB | 1.17 MiB |
| 3135a81 | 14.30 MiB | 15.49 MiB | 1.19 MiB |
| 1fff351 | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| 8af916c | 14.31 MiB | 15.56 MiB | 1.25 MiB |
Previous results on branch: buenaflor/ref/android-core-worker-fire-forget
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5d9efbc | 377.98 ms | 384.94 ms | 6.96 ms |
| b163866 | 348.09 ms | 354.72 ms | 6.63 ms |
| 310effb | 366.33 ms | 344.30 ms | -22.04 ms |
| 099cefb | 397.64 ms | 404.37 ms | 6.72 ms |
| f4d9e97 | 348.36 ms | 354.15 ms | 5.78 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5d9efbc | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| b163866 | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 310effb | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 099cefb | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| f4d9e97 | 14.55 MiB | 15.87 MiB | 1.31 MiB |
iOS Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 8bfae64 | 1237.43 ms | 1238.94 ms | 1.51 ms |
| 114239b | 1225.74 ms | 1230.17 ms | 4.43 ms |
| d2356d0 | 1257.04 ms | 1257.94 ms | 0.89 ms |
| b6c8720 | 1252.65 ms | 1266.61 ms | 13.96 ms |
| 4298701 | 1243.56 ms | 1262.29 ms | 18.72 ms |
| c002f00 | 1252.47 ms | 1258.78 ms | 6.31 ms |
| 0265ce5 | 1261.66 ms | 1250.42 ms | -11.24 ms |
| ec78888 | 1251.37 ms | 1269.40 ms | 18.04 ms |
| 73dca78 | 1246.65 ms | 1265.42 ms | 18.76 ms |
| c58ce03 | 1245.18 ms | 1247.00 ms | 1.82 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 8bfae64 | 5.66 MiB | 6.10 MiB | 451.85 KiB |
| 114239b | 5.53 MiB | 5.96 MiB | 444.85 KiB |
| d2356d0 | 5.66 MiB | 6.09 MiB | 448.38 KiB |
| b6c8720 | 7.86 MiB | 9.44 MiB | 1.58 MiB |
| 4298701 | 20.70 MiB | 22.46 MiB | 1.76 MiB |
| c002f00 | 5.65 MiB | 6.09 MiB | 448.38 KiB |
| 0265ce5 | 5.66 MiB | 6.09 MiB | 448.36 KiB |
| ec78888 | 7.86 MiB | 9.44 MiB | 1.58 MiB |
| 73dca78 | 7.86 MiB | 9.44 MiB | 1.58 MiB |
| c58ce03 | 5.73 MiB | 6.17 MiB | 455.86 KiB |
Previous results on branch: buenaflor/ref/android-core-worker-fire-forget
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 310effb | 1241.37 ms | 1244.26 ms | 2.89 ms |
| 5d9efbc | 1246.83 ms | 1248.34 ms | 1.51 ms |
| b163866 | 1243.33 ms | 1253.69 ms | 10.36 ms |
| 099cefb | 1247.62 ms | 1257.91 ms | 10.29 ms |
| f4d9e97 | 1250.96 ms | 1248.98 ms | -1.98 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 310effb | 5.83 MiB | 6.28 MiB | 462.24 KiB |
| 5d9efbc | 5.83 MiB | 6.28 MiB | 461.83 KiB |
| b163866 | 5.83 MiB | 6.28 MiB | 461.83 KiB |
| 099cefb | 5.83 MiB | 6.28 MiB | 461.84 KiB |
| f4d9e97 | 5.83 MiB | 6.28 MiB | 462.24 KiB |
Return worker request futures for Android scope updates so awaited scope observer calls complete after native scope synchronization finishes. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f4d9e97. Configure here.
Route paired Android scope mutations through the same worker queue so call order is preserved across breadcrumb and context updates. Handle worker startup failures without delaying native SDK initialization. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
| if (_startFuture != null) return _startFuture; | ||
| _startFuture = _start(); | ||
| return _startFuture; |
There was a problem hiding this comment.
Note: _startFuture deduplicates concurrent start() calls so multiple callers share a single in-flight spawn rather than racing to create multiple workers.
Return null for Android core worker read paths after shutdown so closed workers do not fall back to JNI work on the main isolate. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Apply formatting to the Android core worker and clarify that its internal queue preserves JNI request order. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Use explicit worker request helpers for each Android scope mutation so request construction and error logging stay local to each operation. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
Route Android core worker scope payloads through the shared normalizer to avoid broadening behavior in this refactor. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
denrase
left a comment
There was a problem hiding this comment.
We could try to move more work into the worker, if it makes sense. Otherwise looking good.
Align Android worker scope payload normalization with the shared helper without changing the surrounding worker request flow. Co-Authored-By: GPT-5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
|
failures are because of Github.. |

📜 Description
Moves the Android envelope sender into a broader
AndroidCoreWorkerthat can also handle native debug images, native contexts, breadcrumbs, users, and context updates. Value-returning APIs use worker request/response when availableNow running on background isolate (also confirmed these are safe to be called from non-main threads):
💡 Motivation and Context
This keeps more Android JNI work off the main isolate when the worker is available, while preserving current-isolate fallbacks for calls made before worker startup. The worker serializes JNI work to keep native calls ordered.
Relevant #2440, #3348
Fixes #3536
💚 How did you test it?
../../.fvm/flutter_sdk/bin/flutter analyze lib/src/native/java/android_core_worker.dart lib/src/native/java/sentry_native_java.dart test/native/android_core_worker_test_real.dart test/native/sentry_native_java_test_real.dart../../.fvm/flutter_sdk/bin/flutter test test/native/android_core_worker_test_real.dart test/native/sentry_native_java_test_real.dartmelos exec -- dart analyze --fatal-warningsandmelos exec -- flutter analyze --fatal-warnings📝 Checklist
sendDefaultPiiis enabled🔮 Next steps
None.
Made with Cursor