perf(json): Avoid exceptions when typing JSON numbers (JAVA-536) - #5783
Merged
Conversation
📲 Install BuildsAndroid
|
runningcode
force-pushed
the
no/java-536-json-number-parsing
branch
from
July 17, 2026 13:33
5f8ae11 to
0998a8a
Compare
Contributor
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 05aa61d | 326.06 ms | 385.46 ms | 59.40 ms |
| bb0ff41 | 315.84 ms | 350.76 ms | 34.92 ms |
| 806307f | 357.85 ms | 424.64 ms | 66.79 ms |
| d501a7e | 307.33 ms | 341.94 ms | 34.61 ms |
| 0ee65e9 | 321.06 ms | 361.24 ms | 40.18 ms |
| ed33deb | 334.19 ms | 362.30 ms | 28.11 ms |
| 9fbb112 | 401.87 ms | 515.87 ms | 114.00 ms |
| b8bd880 | 314.56 ms | 336.50 ms | 21.94 ms |
| 5b1a06b | 315.40 ms | 353.33 ms | 37.94 ms |
| 6edfca2 | 316.43 ms | 398.90 ms | 82.46 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 05aa61d | 0 B | 0 B | 0 B |
| bb0ff41 | 0 B | 0 B | 0 B |
| 806307f | 1.58 MiB | 2.10 MiB | 533.42 KiB |
| d501a7e | 0 B | 0 B | 0 B |
| 0ee65e9 | 0 B | 0 B | 0 B |
| ed33deb | 1.58 MiB | 2.13 MiB | 559.52 KiB |
| 9fbb112 | 1.58 MiB | 2.11 MiB | 539.18 KiB |
| b8bd880 | 1.58 MiB | 2.29 MiB | 722.92 KiB |
| 5b1a06b | 0 B | 0 B | 0 B |
| 6edfca2 | 1.58 MiB | 2.13 MiB | 559.07 KiB |
runningcode
marked this pull request as ready for review
July 17, 2026 14:08
runningcode
requested review from
0xadam-brown,
adinauer,
markushi and
romtsn
as code owners
July 17, 2026 14:08
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 0998a8a. Configure here.
romtsn
approved these changes
Jul 24, 2026
romtsn
left a comment
Member
There was a problem hiding this comment.
pre-approving, but I'd double-check the bot comment
runningcode
force-pushed
the
no/java-536-json-number-parsing
branch
from
July 27, 2026 10:42
0998a8a to
64d79e7
Compare
JsonObjectDeserializer typed numbers by calling nextInt() and catching the NumberFormatException it throws for every non-integer value, then falling back to nextDouble(). For payloads full of floating-point values (timestamps, measurements) this threw and filled a stack trace on nearly every number, dominating the cost of deserializing arbitrary objects. Parse the value as a double once and narrow it back to an int only when it is integral and fits, which avoids the throws. Return types are unchanged (Integer for values that fit an int, Double otherwise), so callers that read integers out of the generic object tree are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
runningcode
force-pushed
the
no/java-536-json-number-parsing
branch
from
July 27, 2026 10:54
64d79e7 to
bf765cf
Compare
runningcode
enabled auto-merge (squash)
July 27, 2026 11:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

📜 Description
JsonObjectDeserializer.nextNumber()(used when deserializing arbitrary/unknown JSON into a genericMap/Listtree) decided whether a JSON number was anintor adoubleby callingreader.nextInt()and catching theNumberFormatExceptionit throws for every non-integer value, then falling back toreader.nextDouble(). BecauseNumberFormatExceptionfills in a stack trace, every floating-point value (timestamps, measurements, …) paid the cost of a thrown exception.This replaces the exception-driven typing by reading the literal as a
doubleonce and narrowing it back to anintonly when the value is integral and fits:Return types are intentionally unchanged —
Integerfor values that are integral and fit anint,Doubleotherwise (fractional or out of int range). This matters: callers read integers back out of the generic tree and cast them (e.g.ReplayRecordingcasts rrwebsource/typevalues toInteger/int), and defined fields such asMeasurementValue.valueand context values likethread.idwould otherwise change on the wire (4→4.0). Only the internal path to those same results changes; no exceptions are thrown.Note that
reader.nextDouble()is used rather thanDouble.parseDouble(reader.nextString()): it reuses the reader's already-buffered numeric token instead of materializing aStringand re-parsing it, and it keeps the reader's non-finite guard, so a literal that overflows to infinity (1e400) is still rejected as malformed JSON instead of being stored asInfinity. (NaN/Infinityliterals never reach this method — the reader tokenizes them asSTRING, notNUMBER, even in lenient mode.)For context: Gson and Moshi both return
Doublefor all numbers when deserializing arbitrary JSON. We deliberately do not match that here, because the generic tree feeds code that depends on the int/double distinction.One deliberate narrowing versus the old code: the previous
reader.nextLong()fallback is gone, sonextNumber()no longer returnsLong. Integers beyond int range were already returned asDoubleby the old path (nextInt()failed,nextDouble()then succeeded), so this only removes a branch that was unreachable for well-formed numbers; values above 2^53 still lose precision exactly as before.💡 Motivation and Context
Part of JAVA-536 (optimize vendored libraries for startup performance). While benchmarking whether the vendored gson JSON streaming code should be replaced with moshi, this exception-based number typing showed up as a self-inflicted cost. The moshi comparison itself concluded a swap is not worthwhile (the vendored gson engine is as fast or faster, and moshi would add Okio + churn ~80 files); full data is in JAVA-536.
📈 Benchmark
Parsing one
sentry_event.jsoninto a generic object tree, median ms per 1000 iterations (desktop JVM, 5 warmup rounds, median of 10):JsonObjectDeserializer.nextObjectOrNull()Removing the exceptions roughly halves the number-typing work in isolation. End-to-end the win is more modest (~5%), because
JsonObjectDeserializer's token-stack allocations dominate that path; payloads with many floating-point values (e.g. profiling measurements) benefit more.Numbers were measured with a local micro-benchmark (not committed, to keep this PR to the fix + tests); the before/after was obtained by stashing the change and re-running. Methodology: preload the payload into memory, parse it
nextObjectOrNull×1000 per timed run, 2–5 warmup rounds discarded, report the median. The benchmark measured theDouble.parseDouble(nextString())variant; the committednextDouble()version avoids an additionalStringallocation and re-parse per number, so it should be at least as fast. Full moshi-vs-gson comparison data is in JAVA-536.💚 How did you test it?
sentryunit test suite passes (3434 tests, 0 failures).JsonObjectDeserializerTestcovering negative ints/doubles, integral vs. fractional exponent notation (1e2→Integer,2.5e-3→Double), whole-valued decimals (1.0→Integer), integers beyond int range (→Double, neverLong), theInt.MAX_VALUEboundary, and a literal that overflows to infinity (1e400→ rejected) — locking in the return types that must not change.📝 Checklist
sendDefaultPIIis enabled.