Skip to content

perf(core): Remove per-sample allocations from performance collection - #5933

Closed
runningcode wants to merge 2 commits into
mainfrom
no/perf-collection-data-allocations
Closed

perf(core): Remove per-sample allocations from performance collection#5933
runningcode wants to merge 2 commits into
mainfrom
no/perf-collection-data-allocations

Conversation

@runningcode

Copy link
Copy Markdown
Contributor

While a transaction or profile chunk is running, DefaultCompositePerformanceCollector collects performance data every 100ms for up to 30 seconds (~300 samples per transaction). Two allocation sources on that path were doing avoidable work.

Boxed measurements

PerformanceCollectionData stored its measurements as @Nullable Double/@Nullable Long, using null for "no collector reported this". Heap sizes are far outside the Long cache, so every sample allocated a box per measurement — 2 per sample on Android, ~600 per 30s transaction.

The fields are now primitives with an explicit presence flag, and consumers call hasUsedHeapMemory() etc. instead of null-checking.

I first tried sentinel values (-1 for memory, matching the pre-8.0 MemoryCollectionData, and NaN for cpu) to avoid the extra fields. AndroidCpuCollectorTest caught that AndroidCpuCollector can legitimately produce NaN — it computes cpuNanosDiff / realTimeNanosDiff and observes a zero-length interval on the first collection after setup(). Since no in-band value is safe, the presence flags are explicit.

Redundant clock reads

CompositeData.addDataAndCheckTimeout called options.getDateProvider().now().nanoTimestamp() once per in-flight transaction to check the 30s timeout, allocating a SentryNanotimeDate (plus System.currentTimeMillis() + System.nanoTime()) each time — even though the enclosing timer task had just read the same clock for the sample it was distributing. That reading is now passed in, so a collection round reads the clock once rather than once per transaction.

Notes

No behavior change: which measurements get reported, and their values, are unchanged.

PerformanceCollectionData is @ApiStatus.Internal, so the signature changes in sentry.api are not part of the public surface.

sentry-android-core did not have Truth wired up; added testImplementation(libs.google.truth) per AGENTS.md for the tests touched here.

This came out of a review of the memory collector. Two things I found but deliberately left alone:

  • AndroidMemoryCollector computes native memory as Debug.getNativeHeapSize() - Debug.getNativeHeapFreeSize(), which is two mallinfo() calls where getNativeHeapAllocatedSize() would be one. On Scudo, mallinfo() reaches GlobalStats::get(), which takes a global mutex and walks every thread's stats cache — so each sample takes that allocator-wide lock twice, 10x/second, while profiling. Worth fixing, but usmblks - fordblks and uordblks are not the same number, so it would change reported memory_native_footprint values. That deserves its own PR and a measurement.
  • The double Runtime.getRuntime() call in AndroidMemoryCollector is a static field read that folds away; not worth touching.

While a transaction or profile chunk is running, performance data is
collected every 100ms for up to 30 seconds. Two allocation sources on
that path were doing avoidable work:

PerformanceCollectionData stored its measurements as boxed Double/Long.
Heap sizes are far outside the Long cache, so each sample allocated a
box per measurement. The fields are now primitives with an explicit
presence flag. A sentinel value was considered instead, but
AndroidCpuCollector can legitimately produce NaN when it observes a
zero-length interval, so no in-band value is safe.

CompositeData.addAndCheckTimeout read the clock once per in-flight
transaction to check for a timeout, allocating a SentryNanotimeDate each
time, even though the enclosing timer task had just read it for the
sample it was distributing. That reading is now passed in, so a
collection round reads the clock once instead of once per transaction.

No behavior change: which measurements are reported, and their values,
are unchanged. PerformanceCollectionData is @ApiStatus.Internal, so the
signature changes in sentry.api are not part of the public surface.
@sentry

sentry Bot commented Aug 11, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-android Build Distribution Settings

@github-actions

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 315.78 ms 358.54 ms 42.76 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
6b019b7 343.31 ms 417.23 ms 73.91 ms
d15471f 286.65 ms 314.68 ms 28.03 ms
d217708 409.83 ms 474.72 ms 64.89 ms
d500866 326.13 ms 378.70 ms 52.58 ms
fcec2f2 314.96 ms 373.66 ms 58.70 ms
d501a7e 314.55 ms 343.34 ms 28.79 ms
7414e9b 322.49 ms 378.88 ms 56.39 ms
fcec2f2 357.47 ms 447.32 ms 89.85 ms
a416a65 316.52 ms 359.67 ms 43.15 ms
983e0f0 350.64 ms 386.44 ms 35.79 ms

App size

Revision Plain With Sentry Diff
6b019b7 0 B 0 B 0 B
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
d217708 1.58 MiB 2.10 MiB 532.97 KiB
d500866 0 B 0 B 0 B
fcec2f2 1.58 MiB 2.12 MiB 551.50 KiB
d501a7e 0 B 0 B 0 B
7414e9b 0 B 0 B 0 B
fcec2f2 1.58 MiB 2.12 MiB 551.50 KiB
a416a65 1.58 MiB 2.12 MiB 555.26 KiB
983e0f0 0 B 0 B 0 B

@runningcode

Copy link
Copy Markdown
Contributor Author

Split into two independent PRs so each concern can be reviewed on its own:

Both are branched off current main and touch disjoint files, so they can merge in either order.

@runningcode
runningcode deleted the no/perf-collection-data-allocations branch August 11, 2026 13:47
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