Skip to content

perf(android): Replace Date with unix timestamp in SentryNanotimeDate (JAVA-533)#5550

Merged
runningcode merged 8 commits into
mainfrom
no/java-533-replace-date-in-sentrynanotimedate
Jun 17, 2026
Merged

perf(android): Replace Date with unix timestamp in SentryNanotimeDate (JAVA-533)#5550
runningcode merged 8 commits into
mainfrom
no/java-533-replace-date-in-sentrynanotimedate

Conversation

@runningcode

@runningcode runningcode commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

📜 Description

SentryNanotimeDate stored a java.util.Date but only ever read its epoch millis (in compareTo and via DateUtils.dateToNanos). This replaces the stored Date with a long unixDate (milliseconds since the epoch), and the default constructor now calls System.currentTimeMillis() instead of DateUtils.getCurrentDateTime() (which allocates a Calendar).

This is behavior-preserving: Date.getTime() already returns epoch millis, and the UTC Calendar used by getCurrentDateTime() only affected calendar field access — never the absolute instant the class actually used. So Calendar.getInstance(UTC).getTime().getTime() == System.currentTimeMillis().

SentryNanotimeDate is now marked @ApiStatus.Internal — it is the legacy Date+nanoTime precision workaround and is not intended for direct use by consumers (use SentryLongDate/SentryInstantDate to build a SentryDate). Its constructor changed from (Date, long) to (long unixDate, long nanos), and the now-unused internal DateUtils.dateToNanos(Date) is removed. Per the repo's API policy, @ApiStatus.Internal types are not part of the public contract.

💡 Motivation and Context

JAVA-533, part of the "Reduce SDK init time / allocations" effort. On Android, SentryNanotimeDate is always the active date provider (SentryAndroidDateProvider never switches to Instant), so now() runs this on every span/transaction timestamp. Removing the per-timestamp Calendar + Date allocation reduces CPU and GC pressure that scales with tracing volume.

Performance

JVM microbenchmark (JDK 17, isolating the differing part of the constructor; both paths also call System.nanoTime(), excluded):

Path ns/op bytes allocated/op
Old — Calendar.getInstance(UTC).getTime() ~56 ns 528 B
New — System.currentTimeMillis() ~14 ns 0 B

4× faster and zero allocation (the old path allocates a GregorianCalendar + a Date per call).

On-device confirmation — ART method trace, Pixel 3 (Android 12), cold start to idle. Same scenario, per-cold-start call counts:

Method Baseline (Date) Change (long)
SentryNanotimeDate objects created 16 16 (unchanged)
DateUtils.getCurrentDateTime() 8 1
Calendar.getInstance(TimeZone) 8 2
GregorianCalendar.<init> 9 6

The eliminated getCurrentDateTime/Calendar calls map to the no-arg SentryNanotimeDate() constructions — the allocation chain is gone from the timestamp path while the number of SentryNanotimeDate objects is unchanged.

Average time to construct a SentryNanotimeDate() in the (instrumented) trace was ~372 µs baseline → ~24 µs change. ⚠️ These are instrumentation-inflated (ART records every method entry/exit) and exaggerate both the absolute value and the ratio — the real per-call figure is the ~56 ns → ~14 ns microbenchmark above. The trace's value here is the call-count evidence that the Calendar/GregorianCalendar allocations actually disappear on a real device.

Startup time is unaffected (only a handful of these calls during cold start); the win is allocation/GC reduction that scales with tracing volume.

💚 How did you test it?

Updated existing unit tests for the new constructor signature; full sentry and sentry-android-core test suites pass. Behavior verified equivalent (epoch-millis identical to the previous Date-based path).

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

Other internal callers already migrated to the long constructor. Hybrid SDKs that constructed SentryNanotimeDate directly should adopt the new signature (or switch to SentryLongDate); the class is now @ApiStatus.Internal.

@linear-code

linear-code Bot commented Jun 16, 2026

Copy link
Copy Markdown

JAVA-533

@sentry

sentry Bot commented Jun 16, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.43.2 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 323.04 ms 376.62 ms 53.58 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
a416a65 333.78 ms 410.37 ms 76.59 ms
d15471f 304.55 ms 408.43 ms 103.87 ms
ca6b6d8 380.45 ms 460.38 ms 79.93 ms
c8125f3 397.65 ms 485.14 ms 87.49 ms
b936425 302.69 ms 372.86 ms 70.17 ms
ed33deb 334.19 ms 362.30 ms 28.11 ms
cf708bd 408.35 ms 458.98 ms 50.63 ms
eb95ded 317.51 ms 369.08 ms 51.57 ms
a5ab36f 320.47 ms 389.77 ms 69.30 ms
80fd6ad 321.06 ms 375.79 ms 54.73 ms

App size

Revision Plain With Sentry Diff
a416a65 1.58 MiB 2.12 MiB 555.26 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
ca6b6d8 0 B 0 B 0 B
c8125f3 1.58 MiB 2.10 MiB 532.32 KiB
b936425 0 B 0 B 0 B
ed33deb 1.58 MiB 2.13 MiB 559.52 KiB
cf708bd 1.58 MiB 2.11 MiB 539.71 KiB
eb95ded 0 B 0 B 0 B
a5ab36f 1.58 MiB 2.12 MiB 555.26 KiB
80fd6ad 0 B 0 B 0 B

Previous results on branch: no/java-533-replace-date-in-sentrynanotimedate

Startup times

Revision Plain With Sentry Diff
2dcaea8 319.58 ms 362.76 ms 43.18 ms
be3d34b 330.71 ms 393.79 ms 63.08 ms

App size

Revision Plain With Sentry Diff
2dcaea8 0 B 0 B 0 B
be3d34b 0 B 0 B 0 B

@markushi markushi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

Comment thread sentry/src/main/java/io/sentry/SentryNanotimeDate.java Outdated
runningcode and others added 4 commits June 17, 2026 13:39
… (JAVA-533)

SentryNanotimeDate stored a java.util.Date but only ever read its epoch
millis. Storing the millis directly avoids a Calendar allocation on every
timestamp, which on Android backs every span/transaction timestamp.

The default constructor now uses System.currentTimeMillis() instead of
DateUtils.getCurrentDateTime() (Calendar with UTC). This is behavior-
preserving: the UTC TimeZone only affects calendar field access, not the
epoch-millis value the class used.

BREAKING: the public SentryNanotimeDate(Date, long) constructor is
replaced by SentryNanotimeDate(long unixDate, long nanos).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SentryNanotimeDate is the legacy Date+nanoTime precision workaround and is
not intended for direct use by consumers. Marking it @ApiStatus.Internal
signals this and means the constructor change in this PR is not a public
API break per the repo's API policy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Name the long field for its unit so it is clear it holds the unix
timestamp in milliseconds since the epoch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@runningcode runningcode force-pushed the no/java-533-replace-date-in-sentrynanotimedate branch from b80927b to 48adb69 Compare June 17, 2026 11:40

@0xadam-brown 0xadam-brown left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So good to see us getting rid of Date. Nice! 💯

A few quick comments, one of which could block us. Let me know what you think.

Comment thread sentry/api/sentry.api
Comment thread sentry/src/main/java/io/sentry/SentryNanotimeDate.java Outdated
runningcode and others added 4 commits June 17, 2026 13:47
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(JAVA-533)

The previous change replaced the (Date, long) constructor with a
(long, long) constructor, which was a breaking API change. Add the
Date constructor back, delegating to the millis-based one, and mark
it deprecated to steer callers toward the new constructor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…VA-533)

Error Prone flagged the deprecated (Date, long) constructor as
inlineable, failing the build. Suppress the suggestion to match the
existing convention in Sentry.java, keeping the constructor available
for backwards compatibility.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Error Prone's JavaUtilDate check flagged date.getTime() in the
deprecated constructor, failing the build. Suppress it, matching the
existing suppression used elsewhere in this class.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread sentry/api/sentry.api
Comment thread sentry/src/main/java/io/sentry/SentryNanotimeDate.java Outdated
Comment thread sentry/src/main/java/io/sentry/SentryNanotimeDate.java Outdated
Comment thread sentry/api/sentry.api
@runningcode runningcode merged commit 3a7603a into main Jun 17, 2026
68 checks passed
@runningcode runningcode deleted the no/java-533-replace-date-in-sentrynanotimedate branch June 17, 2026 13:45
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.

4 participants