feat(time): Add Timestamp, Timing and EpochClock (JAVA-572) - #6045
Draft
runningcode wants to merge 3 commits into
Draft
feat(time): Add Timestamp, Timing and EpochClock (JAVA-572)#6045runningcode wants to merge 3 commits into
runningcode wants to merge 3 commits into
Conversation
Contributor
|
📲 Install BuildsAndroid
|
This was referenced Sep 2, 2026
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 falls back to subtracting two
wall-clock readings otherwise, silently.
These three types split that apart along the line that matters, which is
whether a value means anything outside this process:
Timestamp an epoch instant. Serialize it, compare it against another
machine's value -- but it offers no arithmetic between
instants, because subtracting two wall-clock readings gives
a duration the device's clock can lengthen or reverse.
Timing a Timestamp anchor plus a Stopwatch, captured together.
end() is the anchor plus the measured duration, which is
what the span protocol needs: it carries two instants and no
duration field, so the server subtracts them.
EpochClock now() to stamp a moment, start() to measure something. A
call site has to say which it is doing, and neither result
can do the other's job.
Timing composes Stopwatch rather than repeating it, so there is one
implementation of a monotonic delta. Both measure on MonotonicClock, which
counts deep sleep: an interval that excluded it, paired with wall-clock
anchors, would produce an end() that falls further behind real time the
longer the device sleeps.
Nothing calls any of it yet.
…der (JAVA-572) DateProviderEpochClock took its instant from options.getDateProvider(), which meant allocating a SentryDate per call to read one long out of it -- and on Android that allocation also takes a System.nanoTime() reading that an EpochClock never looks at, because a Timing measures on the monotonic clock instead. The indirection bought compatibility with a tick nothing here consumes. SystemEpochClock reads the epoch itself, picking precision the way SentryAutoDateProvider does: Instant.now() on JVM 9+, currentTimeMillis() otherwise. Android is always the latter -- Instant is millisecond-granular there whether or not the build desugars it. InstantEpochNanos is a class of its own so the java.time reference is loaded only where it is used, the same reason SentryInstantDate is kept separate. The values are identical on every platform; the dependency is what changes. setDateProvider no longer reaches the epoch clock, so tests override getEpochClock() on SentryOptions, as SentryAndroidOptions already does for getMonotonicClock(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
runningcode
force-pushed
the
no/java-572-timestamp-timing
branch
from
September 3, 2026 15:11
aa53f2e to
7255d5e
Compare
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,TimingandEpochClock, built on theMonotonicClock/Stopwatchprimitives from #6028. Nothing calls any of it yet, exactly like #6028.TimingcomposesStopwatchrather than repeating it, so there is one implementation of amonotonic delta and
Timingowns exactly one operation of its own —end(). Both measure onMonotonicClock, which counts deep sleep: an interval that excluded it, paired with wall-clockanchors, would produce an
end()that falls further behind real time the longer the devicesleeps.
SentryOptions.getEpochClock()is the injection point.SystemEpochClockreads the epoch itselfrather than going through
SentryDateProvider— see below.💡 Motivation and Context
SentryDateis asked to be four things at once: an epoch instant to serialize, one endpoint of amonotonic interval, a carrier of a hidden
System.nanoTime()reading, and an opaque foreigntimestamp (
SentryLongDate, from OTel and the app-start projection). Nothing in the type separatesthem, so the guarantees are decided by the runtime class of both operands:
What that costs today:
SentryAutoDateProviderpicksSentryInstantDateon JVM 9+, which has no monotonic component at all, while Android forces thenanotime provider. Span durations are monotonic on Android and wall-derived on the JVM, where a
clock step mid-span can make one negative.
SpanFrameMetricsCollectorrecovers it withdate.diff(new SentryNanotimeDate(0, 0)), andDriverSpans.computeNanoStartTimestampForChildreturns
null— dropping SQLite sub-span nesting — whenever the date isn't aSentryNanotimeDate.QueuedThreadPoolExecutor.didRejectRecently()isdateProvider.now().diff(lastReject): monotonic on Android, wall clock on the JVM.The split here is along the line that actually matters — whether a value means anything outside this
process. A
MonotonicClocktick never does; aTimestampalways does. Conflating those is what putSystem.nanoTime()andunixDateMillisin the same object with no way for a caller to tell whichone they were getting.
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). The values are therefore byte-identical to what the provider would have returned on every
platform; what changes is that reading one no longer allocates a
SentryDate, and on Android nolonger takes a
System.nanoTime()reading that anEpochClocknever looks at, because aTimingmeasures on the monotonic clock instead.
setDateProviderconsequently does not reach the epoch clock. Faking time for a future consumermeans overriding
getEpochClock()onSentryOptions, the waySentryAndroidOptionsalreadyoverrides
getMonotonicClock(). There is nosetEpochClockbecause there is nothing toconfigure it for yet.
The bridge still cannot run the other way. Reimplementing
SentryDateProvideron top ofEpochClockwould change serialized values today:SentryNanotimeDatecarriesSystem.nanoTime()as its tick, the two sites above compare that tick against
System.nanoTime()directly, and aTimingmeasures on the monotonic clock, which counts deep sleep. Swapping the tick source wouldchange span durations across deep sleep and break both comparisons, so it waits for the major.
💚 How did you test it?
./gradlew :sentry:test :sentry:apiCheck— green. 10 new tests covering the anchor being kept, theend tracking the clock across reads, the duration surviving a wall-clock jump, and
SystemEpochClockanchoring on the wall clock while measuring on the clock it was given.
.apidiff is additions only.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
Stage 2, at the major:
Span/SentryTracerhold aTiming, serialization usesstart()/end()instead of
laterDateNanosTimestampByDiff, the two sentinel hacks get a supported accessor, and JVMdurations become monotonic. Stage 3 retires
SentryDate.diff,SentryNanotimeDate,SentryInstantDate,SentryAutoDateProviderandSentryLongDate— including the open question ofwhether
SentryDateshrinks intoTimestamprather than the two coexisting.QueuedThreadPoolExecutor's wall-clock backoff is internal control flow, so it can be fixed beforethe major, like #6030.