diff --git a/CHANGELOG.md b/CHANGELOG.md index b8fc67b6b53..c2e8119a422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Performance - Reduce the number of SDK threads: `LifecycleWatcher` now schedules the session-end task on the shared timer executor instead of creating a dedicated `java.util.Timer` thread ([#5819](https://github.com/getsentry/sentry-java/pull/5819)) +- Speed up deserialization of arbitrary JSON objects by typing numbers without throwing exceptions ([#5783](https://github.com/getsentry/sentry-java/pull/5783)) ## 8.50.1 diff --git a/sentry/src/main/java/io/sentry/JsonObjectDeserializer.java b/sentry/src/main/java/io/sentry/JsonObjectDeserializer.java index 0916f6e82d5..cd1d99d66fb 100644 --- a/sentry/src/main/java/io/sentry/JsonObjectDeserializer.java +++ b/sentry/src/main/java/io/sentry/JsonObjectDeserializer.java @@ -172,17 +172,16 @@ private boolean handlePrimitive(NextValue callback) throws IOException { } private Object nextNumber(JsonObjectReader reader) throws IOException { - try { - return reader.nextInt(); - } catch (Exception exception) { - // Need to try/fail as there are no int/double/long tokens. + // JSON has no int/double token distinction. Probing with reader.nextInt() and catching the + // NumberFormatException it throws for every non-integer (e.g. timestamps) dominated the cost of + // deserializing arbitrary objects. Read once as a double and narrow to an int only when the + // value is integral and fits, which reproduces the previous return types without any throws. + final double value = reader.nextDouble(); + final int intValue = (int) value; + if (intValue == value) { + return intValue; } - try { - return reader.nextDouble(); - } catch (Exception exception) { - // Need to try/fail as there are no int/double/long tokens. - } - return reader.nextLong(); + return value; } private @Nullable Token getCurrentToken() { diff --git a/sentry/src/test/java/io/sentry/JsonObjectDeserializerTest.kt b/sentry/src/test/java/io/sentry/JsonObjectDeserializerTest.kt index 04e2aaceba0..f2bb1d79044 100644 --- a/sentry/src/test/java/io/sentry/JsonObjectDeserializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonObjectDeserializerTest.kt @@ -1,8 +1,10 @@ package io.sentry +import java.io.IOException import java.io.StringReader import java.lang.Exception import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNull import kotlin.test.fail import org.junit.Test @@ -42,6 +44,54 @@ class JsonObjectDeserializerTest { assertEquals(1.1, actual) } + // A value that is integral and fits an int is typed as Integer (regardless of how it was + // written); anything else is a Double. This matches the behavior prior to removing the + // exception-based number typing. + + @Test + fun `deserialize negative int`() { + assertEquals(-5, deserialize("-5")) + } + + @Test + fun `deserialize negative double`() { + assertEquals(-3.14, deserialize("-3.14")) + } + + @Test + fun `deserialize integral exponent notation as int`() { + assertEquals(100, deserialize("1e2")) + assertEquals(100, deserialize("1E2")) + } + + @Test + fun `deserialize fractional exponent notation as double`() { + assertEquals(0.0025, deserialize("2.5e-3")) + } + + @Test + fun `deserialize whole-valued decimal as int`() { + assertEquals(1, deserialize("1.0")) + } + + @Test + fun `deserialize integer larger than int range as double`() { + assertEquals(1.0e10, deserialize("10000000000")) + assertEquals(2147483648.0, deserialize("2147483648")) + } + + @Test + fun `deserialize max int as int`() { + assertEquals(Int.MAX_VALUE, deserialize("2147483647")) + } + + @Test + fun `deserialize rejects literal overflowing to infinity`() { + // Strict JSON forbids non-finite numbers, so an out-of-range literal must fail rather than be + // stored as Infinity. + assertFailsWith { deserialize("1e400") } + } + @Test fun `deserialize array`() { val json = "[\"a\",\"b\"]"