ADFA-5526: include carousel metrics in crash reports - #1801
Open
davidschachterADFA wants to merge 5 commits into
Open
ADFA-5526: include carousel metrics in crash reports#1801davidschachterADFA wants to merge 5 commits into
davidschachterADFA wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
davidschachterADFA
force-pushed
the
feature/ADFA-5534-metrics-in-feedback
branch
from
September 7, 2026 12:28
c193d08 to
2405b68
Compare
davidschachterADFA
force-pushed
the
feature/ADFA-5526-metrics-in-crash-reports
branch
from
September 7, 2026 12:34
7524dac to
a82ab2c
Compare
Two changes that pay for each other. copyInto writes a ring buffer into an array the caller already owns. toLongArray now delegates to it. Snapshotting the watchers otherwise takes eleven fresh arrays, and the next commit needs a caller that allocates nothing at all -- a crash handler must not ask for memory, because the crash it is reporting may be the heap running out. It requires an exact-length destination: a short one truncates the history and a long one leaves a stale tail behind it, and both read as data. MAX_USAGE_ENTRIES drops from 10,000 to 3,600. Ten thousand samples is nearly three hours at the default rate, of which the chart shows sixty at a time, and eleven buffers of it is 859KB held for the life of the process. At 3,600 the live buffers plus the pre-allocated destinations cost 619KB together -- less than the live buffers alone did before. Note what the constant is not: it counts samples, not time, so at the fastest offered rate of 100ms it is six minutes rather than an hour. If the fast rates are meant to give hours of history, this is the wrong number, and it is one constant. Also relaxes readUsages to internal so a test can drive one sample without starting the sampling loop, matching what ADFA-5514 does to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j
…hable A crash handler is the wrong place to ask for memory. The crash being reported may be the heap running out, and a handler that throws replaces a useful report with no report. So the eleven destinations a snapshot needs are taken at startup, where failing to get them is survivable and obvious, and filled rather than allocated when a snapshot is taken. MetricsScratch is single-use at a time rather than thread-confined: claim hands it to one caller and release gives it back. A caller that cannot claim it allocates for itself instead of waiting, because two writers into one array is a scrambled file and a crash must not block on an export. withSnapshot scopes the claim to a block, because the snapshot points into the scratch and so the scratch has to outlive whatever reads it -- which is a file write, not the call that assembled it. MetricsSource is the other half: the watchers live in an activity-scoped ViewModel, which is right for the carousel and no use to a crash handler that has no activity and arrives on whatever thread threw. The ViewModel registers itself and clears on onCleared, conditionally, since an activity recreation can register the replacement before the outgoing one is cleared. The assembler's @UiThread comes off. Every read it makes takes the watcher's own history lock, so it was always safe from any thread; the annotation was conservative rather than load-bearing, and a crash does not get to choose its thread. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j
A crash arrives with a stack and no idea what the machine was doing. The minutes of memory, network, temperature and power before it are what turn "it died" into a diagnosis, and for an out-of-memory kill they are most of the answer. Registered as a Sentry EventProcessor beside GlitchTipDiagnosticsContext rather than on the uncaught exception handler, so it also covers the non-fatal captureException calls the IDE makes deliberately, and events raised from anywhere rather than only uncaught throws. Everything is inside runCatching, which in Kotlin catches Throwable. That is deliberate: this runs while the process is dying, and an OutOfMemoryError raised in here would cost the whole report rather than just the attachment. Losing the attachment is the right way to fail. Attaches nothing when the editor has never run -- onboarding, the project chooser and direct boot have no history, and direct boot has no credential-protected cache to write to either -- and nothing when nothing has been sampled, since a header-only file on every early crash would be noise rather than context. This does not need ADFA-5494, despite having been blocked on it. A crash is the one loss cause with a hookable moment, which is why it can be served alone; the low-memory kill 5494 exists for produces no report at all, because nothing runs on a SIGKILL. Verified by unit test: an attachment appears with history and gunzips to a valid CSV, nothing is attached without a source or without samples, and the event comes back unchanged when the attachment throws. NOT verified on device: `am crash` aborts the WebView renderer natively, which never reaches the Java uncaught handler, and I did not find another reachable path that raises a reported Java exception in the main process. The device leg of this is open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j
The processor putting an Attachment on the Hint is our half of the hop; whether Sentry carries it into the envelope it sends is the SDK's, and asserting on our own call says nothing about that. So this runs the real SDK against a transport that keeps what it is handed, captures an exception, and reads the envelope back: an attachment item named *.csv.gz, content type application/gzip, whose bytes gunzip to a CSV with the expected header and at least one data row. Confirmed to fail when the addAttachment call is removed. The negative case is pinned too -- a session with no samples produces an envelope with no metrics attachment. Why this rather than a device run. A crash on device does reach the processor: verified on a Pixel 6 Pro with a build carrying a non-resolvable DSN, where `am crash` on the app's main pid wrote 2026_09_07_01_48_52_523.csv.gz at crash time. But no event envelope survives to disk to read back, because the IDE's own uncaught handler captures and then calls exitProcess without waiting for Sentry to flush. Session and log envelopes cache fine, so the cache works; the crash event never gets written. That looks like ordinary crash reports being lost whatever this ticket does, and it is worth its own ticket rather than a change here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j
Three stray braces from resolving this branch onto the row-alignment fix, where copyUsageInto and copyHistoryInto grew a times destination. No behaviour: the files did not parse before this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j
davidschachterADFA
force-pushed
the
feature/ADFA-5534-metrics-in-feedback
branch
from
September 7, 2026 12:43
2405b68 to
6b40850
Compare
davidschachterADFA
force-pushed
the
feature/ADFA-5526-metrics-in-crash-reports
branch
from
September 7, 2026 12:43
a82ab2c to
fab3b7d
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.
Every report the IDE sends now carries what the machine was doing in the minutes before it. A crash arrives with a stack and no idea whether memory was climbing, the daemon was thrashing, or the device was thermally throttled — and for an out-of-memory kill that history is most of the answer.
Four commits: a pruning fix, the buffer work, the pre-allocated destinations, then the attachment.
It did not need its blocker
This was marked blocked by ADFA-5494. It isn't. A crash is the one loss cause with a hookable moment —
IDEApplicationalready installs anUncaughtExceptionHandler— which is exactly why it can be served on its own. The low-memory kill that 5494 exists for produces no report at all, because nothing runs on a SIGKILL, so it was never this ticket's case. The two tickets had been conflated.Not allocating while dying
A crash handler is the wrong place to ask for memory: the crash being reported may be the heap running out, and a handler that throws replaces a useful report with no report. So the eleven destinations a snapshot needs are taken at startup, where failing to get them is survivable and obvious.
copyIntowrites a ring buffer into an array the caller already owns;toLongArraydelegates to it. It requires an exact-length destination — a short one truncates the history and a long one leaves a stale tail, and both read as data.MetricsScratchis single-use at a time rather than thread-confined.claimhands it to one caller,releasegives it back, and a caller that cannot claim it allocates for itself instead of waiting — two writers into one array is a scrambled file, and a crash must not block on an export.withSnapshotscopes the claim to a block, because the snapshot points into the scratch, so the scratch must outlive whatever reads it, which is a file write rather than the call that assembled it.runCatching, which in Kotlin catchesThrowable. That is deliberate: anOutOfMemoryErrorraised in here would cost the whole report. Losing the attachment is the right way to fail.And it costs less memory than before
MAX_USAGE_ENTRIESdrops from 10,000 to 3,600. Ten thousand is nearly three hours at the default rate, of which the chart shows sixty samples at a time.One thing to check rather than take from me: the constant counts samples, not time, so at the fastest offered rate of 100 ms it is six minutes rather than an hour. If the fast rates are meant to give hours of history, 3,600 is the wrong number — it is one constant, and I would rather change it now than have it noticed later.
ADFA-5536 is filed for the follow-on: storing each series in a type and unit appropriate to it, which would save a further ~310 KB. Not done here — it makes the storage layer mixed-unit, which is the shape that produces a double-multiply bug in the numbers the feature exists to report, and the cheaper lever above saves more.
Reaching the watchers from a crash
They live in an activity-scoped ViewModel, which is right for the carousel and no use to a handler with no activity that arrives on whatever thread threw.
MetricsSourceis the one indirection; the ViewModel registers itself and clears ononCleared, conditionally, since an activity recreation can register the replacement before the outgoing one is cleared.The assembler's
@UiThreadcomes off. Every read it makes already takes the watcher's own history lock, so it was always safe from any thread — the annotation was conservative rather than load-bearing.A pruning bug found on the way
pruneTochose "the oldest n" across every file and then skipped the one just written, so whenever that one sorted into the set it deleted one file too few. Two exports inside a single filesystem timestamp make that routine, and the directory crept one over the limit per collision. Pre-existing in the chart snapshots; reusingpruneTofor the CSV exports is what surfaced it, as a bounded-directory test finding four files where three were allowed. Fixed, with a test that ties the timestamps deliberately rather than relying on the collision happening.Verification, including what is not verified
Unit tests: an attachment appears when there is history and gunzips to a valid CSV with the expected header; nothing is attached without a source, or without samples; the event comes back unchanged when the attachment throws. Plus the scratch's claim semantics, its destination sizes against
MAX_USAGE_ENTRIES,installbeing idempotent,copyInto's ordering, identity and length check, and the stale-tail case. Full:appunit suite (689 tests) andspotlessCheckgreen.The device leg is open, and I want that on the record rather than buried.
adb shell am crashaborts the WebView renderer natively, which never reaches the Java uncaught handler — so it exercised nothing, and produced no attachment, correctly. I did not find another reachable path that raises a reported Java exception in the main process, and stopped hunting rather than spend more time on it. What that leaves unproven is only the last hop — that Sentry carries theHintattachment into the envelope — since the file production and the decision logic are covered above. The cheapest way to close it is a debug-only crash trigger; say if that is wanted and I will add one behindBuildConfig.DEBUG.Font scale
No UI.
Review fixes
The pruning fix has moved down to ADFA-5534, where the test that catches it lives — that PR was red on its own
both directories stay boundedwithout it. Nothing is lost here; the tie-case test that shipped with it was vacuous and has been rewritten there. See #1800.The scratch path carries the sample times. ADFA-5531 now reads each watcher's times and values in one critical section, so
copyUsageIntoandcopyHistoryIntotake a times destination alongside the value destinations and hand both back together.MetricsScratchalready pre-allocated the three times arrays; they are filled through the same call as the values now rather than through a separate locked read, so the pre-allocated path has the same guarantee as the allocating one — a row of the crash attachment is one moment.🤖 Generated with Claude Code
https://claude.ai/code/session_017CCQUU7tBzZL61EmQhJP8j