Skip to content

perf(flutter): Optimize Android scope sync#3708

Merged
buenaflor merged 13 commits into
mainfrom
perf/android-scope-sync-json
May 19, 2026
Merged

perf(flutter): Optimize Android scope sync#3708
buenaflor merged 13 commits into
mainfrom
perf/android-scope-sync-json

Conversation

@buenaflor

@buenaflor buenaflor commented May 19, 2026

Copy link
Copy Markdown
Contributor

📜 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/List objects 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.dart
  • JAVA_HOME="$(/usr/libexec/java_home -v 17)" PATH="$(/usr/libexec/java_home -v 17)/bin:$PATH" ./gradlew :sentry_flutter:compileReleaseKotlin
  • fvm flutter test test/native/sentry_native_java_test.dart
  • fvm flutter test -d emulator-5554 integration_test/native_jni_utils_test.dart
  • fvm 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"
  • Full fvm flutter test -d emulator-5554 integration_test was also run locally; the token-gated e2e captureException test failed because SENTRY_AUTH_TOKEN_E2E was unset.

📝 Checklist

  • I reviewed submitted code
  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPii is enabled
  • I updated the docs if needed
  • All tests passing
  • No breaking changes

🔮 Next steps

@github-actions

Copy link
Copy Markdown
Contributor

🚨 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:

  • packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt

@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


Fixes

Dart

  • Make sentryOnError synchronous in runZonedGuarded by theprantadutta in #3697
  • Route SDK diagnostic logs to browser console on web by theprantadutta in #3698

Other

  • (flutter) Release replay JNI refs by buenaflor in #3699

Enhancements

  • (flutter) Optimize Android scope sync by buenaflor in #3708

Internal Changes

  • (flutter) Align CI with stable SwiftPM defaults by buenaflor in #3710

🤖 This preview updates automatically when you update the PR.

@codecov

codecov Bot commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.91%. Comparing base (2e4277e) to head (8551b07).
✅ All tests successful. No failed tests found.

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     
Flag Coverage Δ
sentry_dio ?
sentry_drift ?
sentry_file ?
sentry_firebase_remote_config 100.00% <ø> (ø)
sentry_flutter 91.53% <100.00%> (+<0.01%) ⬆️
sentry_hive ?
sentry_isar ?
sentry_link ?
sentry_logging ?
sentry_sqflite ?
sentry_supabase 97.27% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Android Performance metrics 🚀

  Plain With Sentry Diff
Startup time 383.65 ms 382.90 ms -0.75 ms
Size 14.55 MiB 15.87 MiB 1.31 MiB

Baseline results on branch: main

Startup times

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

buenaflor and others added 2 commits May 19, 2026 11:05
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]>
@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

iOS Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1253.71 ms 1259.55 ms 5.84 ms
Size 5.83 MiB 6.28 MiB 461.20 KiB

Baseline results on branch: main

Startup times

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]>
@buenaflor
buenaflor force-pushed the perf/android-scope-sync-json branch from 04f26dc to b85800e Compare May 19, 2026 09:08
Comment thread packages/flutter/lib/src/native/utils/utf8_json.dart Outdated
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]>
Comment thread packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt Outdated
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]>
Comment thread packages/flutter/lib/src/native/utils/utf8_json.dart Outdated
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]>
Comment thread packages/flutter/lib/src/native/java/sentry_native_java.dart
@buenaflor
buenaflor marked this pull request as ready for review May 19, 2026 11:25
@buenaflor
buenaflor requested a review from denrase as a code owner May 19, 2026 11:25
Copilot AI review requested due to automatic review settings May 19, 2026 11:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 / setContextFromJsonBytes entry points on the Kotlin SentryFlutterPlugin plus matching JNI bindings.
  • Updated SentryNativeJava to use the new byte-array path via a new jsonToJByteArray helper and encodeUtf8Json utility (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 setUser test 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.

Comment thread packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt Outdated
Comment thread packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt Outdated
Comment thread packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt Outdated
Comment thread packages/flutter/lib/src/native/utils/utf8_json.dart
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]>
Comment thread packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt Outdated
buenaflor and others added 2 commits May 19, 2026 15:12
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]>
@buenaflor
buenaflor requested a review from denrase May 19, 2026 13:15
Comment thread packages/flutter/lib/src/native/java/sentry_native_java.dart
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]>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/flutter/lib/src/native/utils/utf8_json.dart
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]>
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.

Large Android scope payloads can cause JNI overhead

3 participants