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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the Android JSON reader helper within ktlint formatting limits after adding the scope sync byte-array bridge. Co-Authored-By: GPT-5.5 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com>
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.
Handle worker request failures without crashing callers and normalize payloads before sending them across isolates. Track JNI strings as they are allocated so partial failures still release native refs. Co-Authored-By: GPT-5.5 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
| 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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Apply formatting to the Android core worker and clarify that its internal queue preserves JNI request order. Co-Authored-By: GPT-5.5 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Route Android core worker scope payloads through the shared normalizer to avoid broadening behavior in this refactor. Co-Authored-By: GPT-5.5 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
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