Skip to content

ref(core): Measure the performance-collection budget on a monotonic ticker (JAVA-579) - #6101

Draft
runningcode wants to merge 2 commits into
mainfrom
no/java-579-c6-collection-budget
Draft

ref(core): Measure the performance-collection budget on a monotonic ticker (JAVA-579)#6101
runningcode wants to merge 2 commits into
mainfrom
no/java-579-c6-collection-budget

Conversation

@runningcode

Copy link
Copy Markdown
Contributor

📜 Description

DefaultCompositePerformanceCollector decided a transaction had been collecting for 30 seconds by subtracting two options.getDateProvider() readings:

this.startTimestamp = options.getDateProvider().now().nanoTimestamp();
// ...
return transaction != null
    && nowNanos > startTimestamp + TimeUnit.MILLISECONDS.toNanos(TRANSACTION_COLLECTION_TIMEOUT_MILLIS);

Those readings are wall-clock on every platform, including Android. It looks like it might be monotonic because SentryNanotimeDate carries a System.nanoTime() component, but nanoTimestamp() returns DateUtils.millisToNanos(unixDateMillis) — the nanoTime component is only used by diff(), which this code does not call. So the extra precision that type exists for never entered this comparison, and a device time change either ended collection early or kept it running well past the budget.

Each CompositeData now holds a Deadline on a MonotonicTicker. The budget is not a serialized value — it only decides when the collector stops — so this is not gated behind the v9 work.

Note the sample timestamps themselves (new PerformanceCollectionData(options.getDateProvider().now().nanoTimestamp())) are deliberately untouched: those are serialized into profile measurements, and re-anchoring them is §C1 / JAVA-578.

💡 Motivation and Context

Audit finding §C6 from the clock-usage audit, which lists four wall-clock TTL/cleanup sites. This is one of them. See "Next steps" for where the other three went.

💚 How did you test it?

The two existing 30 second tests in DefaultCompositePerformanceCollectorTest now advance a ticker instead of stubbing dateProvider.now() with a positional sequence of four return values:

whenever(mockDateProvider.now()).thenReturn(dates[0], dates[0], dates[0], dates[1])

That was brittle against any change in how often the date provider gets called, and it is what let the wall-clock dependency sit here unnoticed. All 20 tests in the class pass.

📝 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.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.

Three details worth a reviewer's eye:

  1. The 30 second boundary is now inclusiveDeadline.hasPassed() is >=, where the old comparison was a strict >. A sample landing exactly on 30.000s now ends collection instead of being kept.
  2. addDataAndCheckTimeout no longer takes a shared nowNanos. That parameter existed so one clock reading was shared across every transaction in a collection round; with a per-transaction Deadline there is nothing to share, and the nanosecond spread across one loop cannot matter to a 30 second budget. Say the word if you would rather keep the shared reading.
  3. TestMonotonicTicker's backing field is now @Volatile, because the collector's timer thread reads a ticker that the test thread advances. (The companion HostnameCache PR does not need this — executorService.submit gives that path a happens-before edge.)

🔮 Next steps

§C6 also mentions the 100ms sampling loop in this same class running on java.util.Timer, whose deadlines are wall-clock and whose Object.wait() does not progress in Android deep sleep. Deliberately not in this PR: it is a scheduling change rather than a TTL one, it needs either a periodic API on ISentryExecutorService or a self-rescheduling task, and six tests in this class assert directly against an injected mock Timer. It deserves its own review.

The other two §C6 sites — profiling-trace cleanup in Sentry.java and envelope rotation in CacheStrategy — both compare against File.lastModified(), which is inherently a wall-clock value written by another process; there is no monotonic quantity to compare it against, so they are not fixable as stated.

🤖 Generated with Claude Code

…icker (JAVA-579)

DefaultCompositePerformanceCollector decided a transaction had been
collecting for 30s by subtracting two options.getDateProvider()
readings. Those are wall-clock on every platform, including Android:
SentryNanotimeDate.nanoTimestamp() returns millisToNanos(unixDateMillis),
not its nanoTime component, so the extra precision that type exists for
never entered this comparison. A device time change therefore ended
collection early or kept it running past the budget.

Each CompositeData now holds a Deadline on a MonotonicTicker. The budget
is not a serialized value, so this only changes when the collector stops.

Two side effects worth review: the 30s boundary is now inclusive, where
the old strict `>` let a sample land exactly at 30s; and
addDataAndCheckTimeout no longer takes the shared clock reading, since
each transaction owns its own deadline. TestMonotonicTicker's field is
now volatile, as the timer thread reads a ticker the test thread
advances.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Sep 11, 2026

Copy link
Copy Markdown

JAVA-579

@sentry

sentry Bot commented Sep 11, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-android Build Distribution Settings

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.

1 participant