perf(flutter): Optimize Android scope sync#3708
Conversation
🚨 Detected changes in high risk code 🚨High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
|
Semver Impact of This PR🟢 Patch (bug fixes) 📋 Changelog PreviewThis is how your changes will appear in the changelog. FixesDart
Other
Enhancements
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 #3708 +/- ##
==========================================
+ Coverage 87.19% 91.91% +4.72%
==========================================
Files 149 105 -44
Lines 5513 3737 -1776
==========================================
- Hits 4807 3435 -1372
+ Misses 706 302 -404
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
Android Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| c5cf749 | 379.18 ms | 360.60 ms | -18.58 ms |
| 3ddf010 | 431.14 ms | 438.10 ms | 6.96 ms |
| 6e7d494 | 397.35 ms | 378.91 ms | -18.43 ms |
| dbd526b | 504.88 ms | 569.02 ms | 64.15 ms |
| 753d4ac | 426.28 ms | 423.45 ms | -2.83 ms |
| 575ebaa | 478.00 ms | 585.76 ms | 107.76 ms |
| f3771ea | 379.02 ms | 363.91 ms | -15.11 ms |
| c58ce03 | 469.42 ms | 472.40 ms | 2.98 ms |
| 38a22c2 | 418.68 ms | 436.54 ms | 17.86 ms |
| c1e775e | 482.36 ms | 608.98 ms | 126.62 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| c5cf749 | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| 3ddf010 | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| 6e7d494 | 13.93 MiB | 15.06 MiB | 1.13 MiB |
| dbd526b | 6.54 MiB | 7.69 MiB | 1.15 MiB |
| 753d4ac | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| 575ebaa | 6.54 MiB | 7.69 MiB | 1.15 MiB |
| f3771ea | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| c58ce03 | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| 38a22c2 | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| c1e775e | 6.54 MiB | 7.70 MiB | 1.17 MiB |
Previous results on branch: perf/android-scope-sync-json
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| e15b49d | 441.27 ms | 432.31 ms | -8.95 ms |
| c666afd | 394.31 ms | 398.04 ms | 3.73 ms |
| b85800e | 382.05 ms | 371.26 ms | -10.78 ms |
| a713631 | 378.04 ms | 366.07 ms | -11.98 ms |
| 7d8ec5f | 378.61 ms | 382.80 ms | 4.19 ms |
| e2bd84b | 354.40 ms | 350.51 ms | -3.89 ms |
| 04f26dc | 369.18 ms | 357.13 ms | -12.05 ms |
| f9a84b1 | 356.82 ms | 358.37 ms | 1.55 ms |
| 0c34206 | 394.20 ms | 385.98 ms | -8.22 ms |
| b43bb60 | 375.35 ms | 371.24 ms | -4.11 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| e15b49d | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| c666afd | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| b85800e | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| a713631 | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| 7d8ec5f | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| e2bd84b | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 04f26dc | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| f9a84b1 | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 0c34206 | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| b43bb60 | 14.31 MiB | 15.49 MiB | 1.19 MiB |
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]>
iOS Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| c58ce03 | 1245.18 ms | 1247.00 ms | 1.82 ms |
| 101a9c7 | 1225.80 ms | 1236.04 ms | 10.25 ms |
| 67de70c | 1260.09 ms | 1266.79 ms | 6.71 ms |
| ea06d23 | 1254.02 ms | 1252.60 ms | -1.42 ms |
| 29e8ebe | 1260.46 ms | 1262.60 ms | 2.14 ms |
| 93b7728 | 1247.23 ms | 1264.87 ms | 17.64 ms |
| cdf371b | 1246.24 ms | 1251.10 ms | 4.86 ms |
| 2cb90b9 | 1272.90 ms | 1285.55 ms | 12.66 ms |
| 6b69699 | 1254.80 ms | 1273.31 ms | 18.52 ms |
| e5ae2a6 | 1240.48 ms | 1253.26 ms | 12.78 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| c58ce03 | 5.73 MiB | 6.17 MiB | 455.86 KiB |
| 101a9c7 | 5.73 MiB | 6.17 MiB | 455.47 KiB |
| 67de70c | 5.73 MiB | 6.17 MiB | 455.45 KiB |
| ea06d23 | 5.53 MiB | 5.96 MiB | 443.26 KiB |
| 29e8ebe | 5.53 MiB | 6.01 MiB | 488.12 KiB |
| 93b7728 | 7.86 MiB | 9.44 MiB | 1.58 MiB |
| cdf371b | 5.53 MiB | 6.02 MiB | 501.23 KiB |
| 2cb90b9 | 7.86 MiB | 9.54 MiB | 1.69 MiB |
| 6b69699 | 7.86 MiB | 9.44 MiB | 1.58 MiB |
| e5ae2a6 | 5.65 MiB | 6.09 MiB | 446.96 KiB |
Previous results on branch: perf/android-scope-sync-json
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| b43bb60 | 1249.37 ms | 1254.39 ms | 5.02 ms |
| 0c34206 | 1238.37 ms | 1243.72 ms | 5.35 ms |
| 7d8ec5f | 1244.50 ms | 1248.21 ms | 3.71 ms |
| c666afd | 1251.53 ms | 1258.02 ms | 6.49 ms |
| e15b49d | 1238.53 ms | 1248.98 ms | 10.45 ms |
| f9a84b1 | 1248.19 ms | 1247.64 ms | -0.54 ms |
| 04f26dc | 1247.53 ms | 1247.93 ms | 0.40 ms |
| e2bd84b | 1254.02 ms | 1260.48 ms | 6.45 ms |
| a713631 | 1255.24 ms | 1250.47 ms | -4.77 ms |
| b85800e | 1241.09 ms | 1246.09 ms | 5.00 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| b43bb60 | 5.73 MiB | 6.18 MiB | 461.67 KiB |
| 0c34206 | 5.83 MiB | 6.28 MiB | 461.21 KiB |
| 7d8ec5f | 5.73 MiB | 6.18 MiB | 461.67 KiB |
| c666afd | 5.73 MiB | 6.18 MiB | 461.66 KiB |
| e15b49d | 5.73 MiB | 6.18 MiB | 461.76 KiB |
| f9a84b1 | 5.83 MiB | 6.28 MiB | 461.20 KiB |
| 04f26dc | 5.73 MiB | 6.18 MiB | 463.07 KiB |
| e2bd84b | 5.83 MiB | 6.28 MiB | 461.20 KiB |
| a713631 | 5.73 MiB | 6.18 MiB | 461.66 KiB |
| b85800e | 5.73 MiB | 6.18 MiB | 461.76 KiB |
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]>
04f26dc to
b85800e
Compare
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]>
There was a problem hiding this comment.
Pull request overview
Optimizes Android scope sync by passing breadcrumb, user, and context payloads across JNI as UTF-8 JSON byte arrays (deserialized on the Kotlin side via Sentry's JsonObjectReader / JSONTokener) instead of recursively constructing Java Map/List objects through per-entry JNI calls. This also fixes a previous limitation where nested Map/List values inside User.data were stringified.
Changes:
- Added new
addBreadcrumbFromJsonBytes/setUserFromJsonBytes/setContextFromJsonBytesentry points on the KotlinSentryFlutterPluginplus matching JNI bindings. - Updated
SentryNativeJavato use the new byte-array path via a newjsonToJByteArrayhelper andencodeUtf8Jsonutility (which normalizes non-primitive values first). - Updated integration tests: added a "large scope maps" Android test and removed the Android-specific stringification branch in the existing
setUsertest now that nested structures round-trip correctly.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/flutter/lib/src/native/utils/utf8_json.dart | New encodeUtf8Json helper that JSON-encodes Dart data to UTF-8 bytes. |
| packages/flutter/lib/src/native/java/sentry_native_java.dart | Switches breadcrumb/user/context sync to JSON-byte JNI path; adds jsonToJByteArray. |
| packages/flutter/lib/src/native/java/binding.dart | Generated JNI bindings for new Kotlin static methods. |
| packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | Adds Kotlin entry points that deserialize JSON bytes via JsonObjectReader/JSONTokener. |
| packages/flutter/example/integration_test/native_jni_utils_test.dart | Tests for normalize and jsonToJByteArray. |
| packages/flutter/example/integration_test/integration_test.dart | Adds large-scope sync test; removes Android-specific stringified assertions. |
| packages/flutter/AGENTS.md | Documents the "JSON bytes for arbitrary payloads" JNI guideline. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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]>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f9a84b1. Configure here.
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]>
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]>

📜 Description
Optimize Android scope sync by sending breadcrumb, user, and context payloads over JNI as UTF-8 JSON bytes instead of recursively constructing Java
Map/Listobjects from Dart.This keeps primitive JNI calls direct, preserves structured nested user data through native deserialization, and reduces per-entry JNI object churn for large user-controlled scope payloads.
💡 Motivation and Context
Large arbitrary maps in breadcrumbs, contexts, or user data can be expensive on lower-end Android devices when each nested value crosses JNI as an object. Passing the serialized payload once and deserializing on the Kotlin/Java side keeps the bridge coarse-grained and aligns with the existing byte-based native context/debug-image paths.
This lead to ANRs or even crashes in lower end devices.
Fixes #3709
💚 How did you test it?
fvm flutter analyze lib/src/native/utils/utf8_json.dart lib/src/native/java/sentry_native_java.dart example/integration_test/native_jni_utils_test.dartJAVA_HOME="$(/usr/libexec/java_home -v 17)" PATH="$(/usr/libexec/java_home -v 17)/bin:$PATH" ./gradlew :sentry_flutter:compileReleaseKotlinfvm flutter test test/native/sentry_native_java_test.dartfvm flutter test -d emulator-5554 integration_test/native_jni_utils_test.dartfvm flutter test -d emulator-5554 integration_test/integration_test.dart --plain-name "syncs large scope maps to native on Android"fvm flutter test -d emulator-5554 integration_test/integration_test.dart --plain-name "setUser syncs to native"fvm flutter test -d emulator-5554 integration_testwas also run locally; the token-gatede2e captureExceptiontest failed becauseSENTRY_AUTH_TOKEN_E2Ewas unset.📝 Checklist
sendDefaultPiiis enabled🔮 Next steps