[DO NOT MERGE] feat(time): Anchor span timestamps on one wall-clock reading (JAVA-572) - #6055
Draft
runningcode wants to merge 1 commit into
Draft
Conversation
Contributor
Instructions and example for changelogPlease add an entry to Example: ## Unreleased
### Features
- Anchor span timestamps on one wall-clock reading (JAVA-572) ([#6055](https://github.com/getsentry/sentry-java/pull/6055))If none of the above apply, you can opt out of this check by adding |
📲 Install BuildsAndroid
|
Demonstrates what AnchoredClock and Timestamp look like once Span and
SentryTracer actually use them. Not for merge before the major: it changes a
serialized value.
SentryTracer takes one epoch reading when it is constructed, pins it to one
monotonic tick, and every span below it projects from that anchor rather than
reading the wall clock again. Because the projection is affine with slope 1,
subtracting any two instants in a transaction is subtracting two ticks, so
serialization no longer derives anything:
- this.timestamp = DateUtils.nanosToSeconds(
- span.getStartDate().laterDateNanosTimestampByDiff(span.getFinishDate()));
+ this.timestamp = DateUtils.nanosToSeconds(span.endTimestamp().epochNanos());
That is the whole point. laterDateNanosTimestampByDiff exists to rescue a
monotonic end out of two dates whose classes happen to cooperate; with one
anchor there is nothing to rescue. The two sites that reverse-engineered the
hidden System.nanoTime() tick out of SentryNanotimeDate ask the anchor instead,
and both sentinels -- SentryNanotimeDate(0, 0) -- are gone.
DriverSpans.computeNanoStartTimestampForChild collapses to anchor().now(), and
no longer silently drops to millisecond precision when the parent's date is not
the class it hoped for.
ISpan gains startTimestamp(), endTimestamp() and anchor(). getStartDate() and
getFinishDate() stay, as epoch-only views, so the public API still works; at the
major they go and the SentryDate overloads of finish() and updateEndDate() go
with them.
Spans whose instants come from outside the process -- OTel, an app-start
projection, a caller-supplied startTimestamp -- report a null anchor and keep
wall-clock semantics. That case cannot be improved, because the input carries
nothing but an epoch; what changes is that it is now a named branch rather than
an instanceof that falls through.
Why this waits for the major: a child span's start_timestamp moves by
sub-millisecond amounts, because it is projected rather than read. Root spans do
not move, the anchor being read at root start.
Two things this turned up, both left open on purpose:
- The frame timebase. Choreographer reports frames on CLOCK_MONOTONIC while
the anchor's tick is CLOCK_BOOTTIME on Android, so SpanFrameMetricsCollector
needs an offset it cannot read from statics without becoming untestable, and
a decision about spans that spanned deep sleep. Marked TODO [MAJOR] rather
than half-solved.
- SentryDate has no value equality, so getStartDate() returning a fresh view
breaks identity comparisons. The tests here move to Timestamp, which does.
Also fixes a test that stubbed dateProvider.now() as a fixed call sequence and
so depended on how often unrelated code read the provider.
runningcode
force-pushed
the
no/java-571-deprecate-date-providers
branch
from
September 4, 2026 15:11
727a95b to
edcf5cd
Compare
runningcode
force-pushed
the
no/span-anchored-clock-demo
branch
from
September 4, 2026 15:11
5ab4737 to
c071882
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.
Warning
Do not merge. This is a demonstrator for the v9 shape of span timing. It changes a
serialized value, so it cannot land before the major, and there is no
9.x.xbranch to targetyet. It is deliberately not part of the clock PR stack — it sits on top of it so the diff
shows only the integration.
Based on #6043 (the top of the stack), which brings in #6028 → #6045 → #6029 → #6030 → #6032 → #6041.
📜 What this shows
#6045 adds
Timestamp,EpochClockandAnchoredClockwith no consumer. This is the consumer.SentryTracertakes one epoch reading when it is constructed, pins it to one monotonic tick,and every span below it projects from that anchor instead of reading the wall clock again:
The projection is affine with slope 1, so subtracting any two instants in a transaction is
subtracting two ticks. Serialization stops deriving anything:
laterDateNanosTimestampByDiffexists only to rescue a monotonic end out of two dates whose runtimeclasses happen to cooperate. With one anchor there is nothing to rescue.
💡 What it removes
laterDateNanosTimestampByDifffrom the span path — durations are monotonic by constructionSpanFrameMetricsCollector.toNanoTime'sinstanceof+ wall-clock projection (the¯\_(ツ)_/¯one)DriverSpans.computeNanoStartTimestampForChild, which collapses toanchor().now()and no longersilently drops to millisecond precision when the parent's date isn't the class it hoped for
SentryNanotimeDate(0, 0)sentinelsSentryDateallocations per span endpointISpangainsstartTimestamp(),endTimestamp()andanchor().getStartDate()/getFinishDate()stay as epoch-only views so the public API keeps working; at the major they go, and the
SentryDateoverloads of
finish()andupdateEndDate()go with them.Spans whose instants come from outside the process — OTel, the app-start projection, a
caller-supplied
startTimestamp— report a null anchor and keep wall-clock semantics. That casecannot be improved, because the input carries nothing but an epoch. What changes is that it is a
named branch instead of an
instanceofthat falls through.A child span's
start_timestampmoves by sub-millisecond amounts, because it is projected ratherthan read. Root spans do not move — the anchor is read at root start. Small, systematic, and a
serialized value, so it sits behind the v9 gate.
🔍 Two things building this turned up
Both left open on purpose rather than half-solved:
Choreographerreports frames onCLOCK_MONOTONIC; the anchor's tick isCLOCK_BOOTTIMEon Android. Bridging costs an offset thatSpanFrameMetricsCollectorcannot readfrom statics without becoming untestable, and it changes on every suspend — so it also needs a
decision about what to do with a span that spanned deep sleep. There are no frames during sleep,
so skipping such a span is more honest than shifting it. Marked
TODO [MAJOR].SentryDatehas no value equality. OncegetStartDate()returns a fresh view rather than thecaller's own instance, every identity comparison breaks. That accounts for most of the test diff
here: the assertions move to
Timestamp, which does have value equality. Worth knowing before thereal migration — it is invisible until you try it.
💚 How did you test it?
5425 tests, 0 failures (scan):
sentrysentry-android-coresentry-android-sqlitesentry-opentelemetry-corespotlessApply apiDumpclean; the.apidiff is additions only.Two real bugs found by the suite while writing this, both fixed here:
trimStartcomparison — the trim tests caught itDefaultCompositePerformanceCollectorTeststubbeddateProvider.now()as a fixed call sequence,so it silently depended on how often unrelated code read the provider. Anchoring removes some of
those reads. Restubbed by role — first reading anchors, later ones are ticks — rather than by count.
🔮 If this direction is accepted
9.0.0milestone — nothing clock-related is in it today, andthe plan treats that milestone as the authoritative "must wait" list
java.util.Timer; the surviving halfof it is "clamp the finish timestamp when the deadline fires late", which is orthogonal to this
and is the actual fix for the multi-hour
ui.loadartifact9.x.xonce that branch is cut