feat(time): Add Timestamp, EpochClock and AnchoredClock (JAVA-572) - #6045
Merged
Conversation
Contributor
|
📲 Install BuildsAndroid
|
This was referenced Sep 2, 2026
runningcode
force-pushed
the
no/java-572-timestamp-timing
branch
from
September 3, 2026 15:11
aa53f2e to
7255d5e
Compare
runningcode
force-pushed
the
no/java-571-clock-abstractions
branch
from
September 3, 2026 15:41
deb42e1 to
271cf66
Compare
runningcode
force-pushed
the
no/java-572-timestamp-timing
branch
2 times, most recently
from
September 4, 2026 08:17
a372da0 to
bec70fb
Compare
runningcode
force-pushed
the
no/java-571-clock-abstractions
branch
from
September 4, 2026 08:29
271cf66 to
ab5fba1
Compare
runningcode
force-pushed
the
no/java-572-timestamp-timing
branch
from
September 4, 2026 08:29
bec70fb to
686cc80
Compare
runningcode
force-pushed
the
no/java-572-timestamp-timing
branch
from
September 4, 2026 15:11
686cc80 to
655adfb
Compare
runningcode
marked this pull request as ready for review
September 7, 2026 14:02
runningcode
requested review from
0xadam-brown,
adinauer,
markushi and
romtsn
as code owners
September 7, 2026 14:02
0xadam-brown
reviewed
Sep 8, 2026
0xadam-brown
left a comment
Member
There was a problem hiding this comment.
Nice! 🥇
Some comments for your consideration. (Apologies in advance for the doc suggestions, but hope they'll be helpful in a cut-and-paste way – seems worthwhile one-up the clanker because these APIs are so important.) Looking great though 💯
runningcode
force-pushed
the
no/java-571-clock-abstractions
branch
from
September 8, 2026 13:19
ab5fba1 to
d7bc897
Compare
runningcode
force-pushed
the
no/java-572-timestamp-timing
branch
2 times, most recently
from
September 8, 2026 15:09
0969abb to
005fa73
Compare
0xadam-brown
approved these changes
Sep 8, 2026
0xadam-brown
left a comment
Member
There was a problem hiding this comment.
Thanks! Responded to your major questions here, but nothing that has to block us 👍
runningcode
force-pushed
the
no/java-572-timestamp-timing
branch
from
September 9, 2026 07:29
005fa73 to
8d4dce5
Compare
runningcode
force-pushed
the
no/java-572-timestamp-timing
branch
from
September 9, 2026 08:28
8d4dce5 to
690617d
Compare
SentryDate is asked to be four things at once: an epoch instant to
serialize, one endpoint of a monotonic interval, a carrier of a hidden
System.nanoTime() reading, and an opaque foreign timestamp. Nothing in the
type separates them, so the guarantees are decided by the runtime class of
both operands -- SentryNanotimeDate.diff() is monotonic only when the other
date is also a SentryNanotimeDate, and silently subtracts two wall-clock
readings otherwise. On the JVM, where SentryAutoDateProvider picks
SentryInstantDate, neither endpoint has a monotonic component and span
durations are not monotonic at all.
The fix is not to type the instants more carefully. It is to stop producing
them independently. A group of instants that will be compared against each
other -- the spans of a transaction, the samples of a profile chunk, the
segments of a replay -- reads the epoch once and builds the rest from the
monotonic ticker:
Timestamp an epoch instant. No arithmetic between instants; equality
is by instant.
EpochClock the wall clock, for stamping a moment that leaves the
process. Not for computing durations.
AnchoredClock one wall-clock reading pinned to one tick, from which now()
builds.
Subtracting two instants from one anchor is subtracting two ticks, so a
duration is monotonic by construction rather than by convention, and a clock
step cannot make a child span start before its parent. It also gives Android
nanosecond resolution it cannot read directly, the epoch being
millisecond-granular there -- the workaround SentryNanotimeDate describes,
applied once per group instead of between each pair of readings.
OpenTelemetry's SDK anchors per local root span for the same two reasons.
Only what a caller needs today is exposed. Placing an externally measured
tick on the anchor's timeline, and recovering the tick a timestamp was built
from, are both exact and both trivial to add, but neither is reachable until
spans carry an anchor. They belong to the change that migrates them rather
than to this one, where they would arrive with no caller.
Timing is dropped rather than kept. It paired one Timestamp with one
Stopwatch, which is what AnchoredClock does for a whole group, and no call
site would have wanted the single-interval version.
Nothing calls any of it yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
runningcode
force-pushed
the
no/java-572-timestamp-timing
branch
from
September 9, 2026 12:38
690617d to
d484855
Compare
runningcode
enabled auto-merge (squash)
September 9, 2026 12:41
3 tasks
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.
PR Stack (Clock semantics hardening)
📜 Description
Adds the wall-clock half of the time API —
Timestamp,EpochClockandAnchoredClock— on top ofthe
MonotonicClock/Stopwatchprimitives from #6028. Nothing calls any of it yet, like #6028.SentryOptions.getEpochClock()is the injection point.💡 Motivation and Context
Why anchoring, rather than more careful types
The bug class is not "wall vs monotonic". It is pairwise arithmetic between two independently
produced instants. Better types narrow that; they do not close it, because a mixed pair is still
reachable and still has to answer.
So the fix is to stop producing the instants independently. A group that will be compared against
each other — the spans of a transaction, the samples of a profile chunk, the segments of a replay —
reads the epoch once and projects the rest through the monotonic clock:
The projection is affine with slope 1, so subtracting two instants from one anchor is subtracting
two ticks. A duration is then monotonic by construction rather than by convention, and a clock step
cannot make a child span start before its parent. It also buys resolution the wall clock does not
have: Android's epoch is millisecond-granular, so a directly read instant is truncated while a
projected one carries nanoseconds. OpenTelemetry's SDK does the same thing, per local root span, for
the same two reasons.
tickOf()refusing an instant it did not project is what makes this safer rather than merelytidier, and it is why
Timestampreferences its anchor at all: mixing domains raises anIllegalArgumentExceptioninstead of returning a plausible-looking wrong number. An instant readstraight from the wall clock, or stated by something outside the process, has no anchor and can only
be serialized.
The epoch clock does not go through SentryDateProvider
SystemEpochClockreads the wall clock directly, picking precision the waySentryAutoDateProviderdoes:
Instant.now()on JVM 9+,System.currentTimeMillis()otherwise. Android is always thelatter —
Instantis millisecond-granular there whether or not the build desugars it (#2451). Thevalues are byte-identical to what the provider returns on every platform; what changes is that
reading one no longer allocates a
SentryDate, and on Android no longer takes aSystem.nanoTime()reading that an
EpochClocknever looks at.setDateProvidertherefore does not reach the epoch clock. Faking time means overridinggetEpochClock()onSentryOptions, the waySentryAndroidOptionsalready overridesgetMonotonicClock(). There is nosetEpochClockbecause nothing consumes it yet.💚 How did you test it?
./gradlew :sentry:test— 3551 tests, 0 failures.spotlessApply apiDumpclean; the.apidiffagainst the merge base is additions only, with no
<init>leaks.14 new tests on
AnchoredClock, including the ones that were impossible to write before:and the differences between them do not move
tickOfinverts a projection exactly, and throws for a bare instant and for another anchor's instantdriftNanos()is zero while the wall clock keeps pace, and reports the signed size of a step📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
SentryTracercreates oneAnchoredClockper transaction, before its rootSpan, and everySpanbelow projects from that anchor.
Spanthen holds twoTimestamps instead of twoSentryDates,serialization reads
end().epochNanos()instead oflaterDateNanosTimestampByDiff, and the twosentinel hacks call
anchor.tickOf(...). That retireslaterDateNanosTimestampByDiff,SentryNanotimeDate'sdiff/compareTo/nanotimeDiffoverrides,SentryAutoDateProvider/SentryInstantDate, and twoSentryDateallocations per span endpoint.Two things that PR has to settle, flagged here so they get argued before code exists:
Choreographerhands us frame timestamps in theSystem.nanoTime()timebase, whileAndroidMonotonicClockisSystemClock.elapsedRealtimeNanos(); the two differ by accumulatedsuspend. The plan is to keep a single
MonotonicClockand put that last hop insideSpanFrameMetricsCollector, which already has anonSpanStartedhook to capture both readings andcan detect sleep by comparing the deltas — skipping attribution honestly rather than returning a
plausible-but-wrong projection.
start_timestampby sub-millisecondamounts (root spans are unaffected — the anchor is read at root start). Under the "serialized values
are frozen until the major" rule that puts the flip behind v9, even though it is an improvement.
Separately, JAVA-572's original subject — the tracer idle/deadline timeout — is stale on the timer half
(
SentryTraceralready usesgetTimerExecutorService()), but "clamp the finish timestamp when thedeadline fires late" is still real and is the actual fix for the multi-hour
ui.loadartifact. Itwants its own ticket.
QueuedThreadPoolExecutor's wall-clock backoff is internal control flow, so itcan be fixed before the major, like #6030.