fix(android): Anchor the frame-metrics time projection (JAVA-579) - #6098
Draft
runningcode wants to merge 2 commits into
Draft
fix(android): Anchor the frame-metrics time projection (JAVA-579)#6098runningcode wants to merge 2 commits into
runningcode wants to merge 2 commits into
Conversation
SpanFrameMetricsCollector places a span on the frame timeline, which is stamped by Choreographer on System.nanoTime(). Spans carrying a SentryNanotimeDate are already on that timeline, but a wall-stamped one — an app start span built from TimeSpan — has to be projected onto it, and toNanoTime read the offset between the two clocks at the moment the span finished. That offset is not a constant. A wall-clock step moves one clock, and time spent suspended moves the other, since System.nanoTime() stops in suspend and the wall clock does not. So the projection charged the span for every step and every suspend since it started, and an app start span could be placed far away from the frames it actually overlapped, taking its frames_slow/frozen/delay data with it. Take one wall reading and one tick at construction and project through that fixed anchor instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📲 Install BuildsAndroid
|
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.
📜 Description
SpanFrameMetricsCollectormatches spans against frames, and the frame timeline is stamped byChoreographeronSystem.nanoTime(). A span carrying aSentryNanotimeDateis already on that timeline and needs no conversion. A wall-stamped span — an app start span built fromTimeSpan, which is aSentryLongDate— has to be projected onto it, andtoNanoTimedid that by reading the offset between the wall clock andSystem.nanoTime()at the moment the span finished:That offset is not a constant. A wall-clock step (NTP, carrier, user) moves one clock, and time spent suspended moves the other, because
System.nanoTime()stops in suspend on Android while the wall clock keeps going. Reading it at finish time therefore charged the span for every step and every suspend that happened since it started, placing it — and theframes_slow/frames_frozen/frames_delaydata derived from it — away from the frames it actually overlapped. An app start span that outlives a doze is off by the length of the doze.This takes one wall reading and one tick at construction and projects through that fixed anchor instead, so a span lands where it did when the SDK started rather than where the accumulated drift puts it.
Note the anchor deliberately uses
JavaMonotonicTicker(System.nanoTime()) rather thanoptions.getMonotonicTicker(), which iselapsedRealtimeNanos()on Android. The frame timeline isnanoTime, so the anchor has to be on the same base; there is a comment saying so at the call site.💡 Motivation and Context
Audit finding §C5 from the clock-usage audit.
💚 How did you test it?
A unit test in
SpanFrameMetricsCollectorTestdrives a wall-stamped (SentryLongDate) span with one slow frame inside it, advances the frame ticker by an hour between the span starting and finishing, and asserts the frame is still attributed to the span. Verified that this test fails against the oldtoNanoTimebody and passes with the anchor. The existing 16 tests in the class are unchanged and still pass — they all useSentryNanotimeDatespans, which take the untouched fast path.The projection is now injectable (a package-private constructor taking an
EpochClockand aMonotonicTicker), which is what makes the drift testable at all — previously both clock reads were static calls inside the method.📝 Checklist
sendDefaultPIIis enabled.🔗 Relationship to #6055
#6055 rewrites this same method, so the two will conflict textually — but they fix different branches, and this one is not superseded by it.
#6055 splits
toNanoTimeon whether the span has an anchor. Anchored spans invert the anchor exactly; everything else falls through to the old wall-clock projection, kept verbatim, live offset read and all. Its own comment names "an app-start projection" as a case that lands in that fallback, which is precisely theSentryLongDate-from-TimeSpanspan this PR is about. So after #6055 the bug fixed here is still present, on the only path app-start spans take.The other direction matters too: #6055's anchored branch inverts a tick on
MonotonicTicker, which isCLOCK_BOOTTIMEon Android, while frames areCLOCK_MONOTONIC— a new timebase mismatch that it documents as an unbridged TODO. This PR avoids that by anchoring onJavaMonotonicTicker(System.nanoTime()), the same timebase as the frames.Suggested order: this lands first (targets
main, changes no serialized value, mergeable now), and #6055 — a DO-NOT-MERGE demonstrator with no9.xbranch to target yet — carries the frozen anchor into its fallback branch when it rebases.Worth noting that #6055's TODO argues a span that crossed deep sleep should be skipped rather than shifted, since there are no frames during sleep. That is a reasonable position, and a different one from this PR's, which narrows the error rather than eliminating it: the frozen anchor puts the span's start where it belongs, but the projected end of a doze-crossing span still overshoots by the length of the doze. Dropping such launches outright is JAVA-642's call.
🔮 Next steps
This does not change any serialized timestamp — only which frames a span is credited with — so it is not gated behind the v9 work. §C4 (
TimeSpanback-projection), which produces the wall-stamped dates this code has to project, does change serialized app-start timestamps and is v9-gated.🤖 Generated with Claude Code