Keep the stack traces we log, and read them as traces - #840
Merged
Conversation
android.util.Log drops a throwable's trace whenever anything in its cause chain is an UnknownHostException. getStackTraceString returns "" and Log.e(tag, msg, tr) prints the message alone; AOSP does it on purpose, to "reduce the amount of log spew that apps do in the non-error condition of the network being unavailable". Nothing looks wrong when it happens — only the trace is missing. For an app whose network use is a DoH resolver, a module store and a GitHub feed, that filter removes traces from exactly the failures worth reporting. CrashRecorder was writing 114-byte crash files that held the header and no trace at all, which is how the crash card came to have nothing on it. VectorDns logged "dns: DoH lookup of $host failed" with no trace, DnsOverHttps signalling failure by throwing UnknownHostException. VectorContext.log and XposedBridge.log(Throwable), both module-facing, answered a module's report of a failed request with a blank line. So format traces ourselves and never hand a throwable to the framework: logE/logW/logI in Logging.kt for the manager, and Utils.Log's own getStackTraceString for the framework side. The helpers also break long entries on line boundaries, which Log.e used to do for us and Log.println does not — a deep trace would otherwise lose its tail. With traces to show, the crash card had to stop being a paragraph of monospace. It states four facts now — what threw, what it said, the nearest frame that is ours, and when — and "See the trace" opens a screen where the trace is a list: our frames marked and emphasised, the platform's dimmed, "Caused by" as headings, each frame tappable to copy. Copy all still copies every record, whether or not it could be parsed.
The log panel was losing half of every stack trace. LogFile.readRows attaches a line to the entry above it only when the line is indented, and printStackTrace writes the throwable's own header flush left, then every "Caused by:" flush left too. Both ended the run, so the cause chain and all of its frames became one LogRow.Marker per line — the half of the trace that names what actually failed was the half that fell out of the entry that owned it, and the "N frames" expander was counting only what was left. So a throwable header counts as a continuation as well, though only where an entry owns the line above, which is what keeps "----part 7 start----" and the daemon's own banners the standalone markers they are. The filter pass had to learn the same rule, or a filtered view would drop what the unfiltered one keeps; and entryStart spends one extra read on a line beginning with a letter, so jumping into the middle of a long trace no longer opens on an orphan. With the whole trace in hand there was no reason to keep rendering it as raw monospace behind an expander. StackTrace, extracted from the crash screen, is now what draws both: our frames marked and emphasised, the platform's dimmed, "Caused by" as a divider, each frame tappable to copy. A trace written by the daemon, by a module or by the manager is the same printStackTrace output whoever wrote it, so it is read one way. Where it opens is a setting. Inline by default, because the log is read with a filter applied and a scroll position worth keeping and a route push costs both; off, the expander opens a screen instead, which is the better answer for a trace long enough that having it in the list is the thing in the way. One correction the parser needed before that worked. parseStackTrace opened a section only on a header line, so a trace that begins at a frame — which is what the panel hands it, the header having landed on the entry's own line — fell to the branch that reads headers and spent the first frame on one. The heading read "java:248)", the tail of the frame it had just eaten, and the count was one short. A frame now opens an untyped section and the renderer draws no heading for it; the row recovers the real type from the entry's message instead, taking only the type, since the line above is already saying the rest in full.
Fifteen of them, across five files. The four in RepoRepository are the ones the logging sweep turned up and left alone as not being its business; sweeping the module found the same thing in four more files, so they go together rather than leaving a known-identical problem in place. None of this is reachable code, and the compiler had been saying so on every build. Delegate operators — getValue, setValue — are excluded from the sweep: `by remember` uses them without ever naming them, so a search for the name finds nothing and deleting the import breaks the build.
The panel decided what continued an entry by the look of a line, and lines do not look like anything in particular. Indented ones passed and everything else was cut loose into a marker row of its own, which meant a twenty-line report arrived as twenty rows with a divider drawn between each and no visible connection to the entry that wrote it. zygisk's "--- Parsed Mount Argument Vector Fossil ---" block is the case on screen; a stack trace's Caused by: was the same bug, patched narrowly last time. The writer settles it. logcat.cpp emits an entry as one writev whose first iovec is the prefix, so every line after the first has none — an unprefixed line under an entry is a continuation by construction. The only unprefixed lines that are not are the four LogRaw and the rotation code write, and those can be enumerated from the source rather than guessed at, so the test is now for those. A line carrying a prefix is still an entry, whatever precedes it; asking only whether a line could continue swallowed the entire log into its first entry, 105 continuations deep, which is worth the assertion the predicate's doc now carries. Attaching arbitrary text meant "N frames" was no longer true of most of it, so a block with no frames in it says "N more lines" and opens as plain text, laid out as the writer set it out — the columns in a mount argument report are the whole of its legibility — while a real trace keeps the structured renderer. The tag badge now carries the level's colour rather than one fixed container pair, so a scan down the page separates on the widest thing in the row instead of on the rail at its edge and the letter at its start. A wash rather than a fill, with the tag left onSurface: the palette runs down to outlineVariant for debug, and tag text in that colour on a tint of itself is a badge nobody could read at the two levels a log is mostly made of.
A stack trace past the logger's payload arrives as two entries, not one. android.util.Log.e(tag, msg, tr) does not hand liblog an oversized message: printlns walks the string and emits it in pieces, breaking at the last newline that fits, and each piece is a real entry with its own prefix. logd stores them separately -- `logcat -b crash -v long` counts two for one crash -- so the panel showed a 5.7 KB crash as a row ending mid-frame and a second row opening on "at androidx.compose...", 40 frames and 28 frames where there are 69. The manager's own logE splits the same way and for the same reason. So the split is undone. This is the one place the "a prefixed line is an entry" rule is knowingly overruled, and it is kept narrow to earn that: same tag, process, thread and level, immediately adjacent, and a message that opens the way a continuation does rather than the way a message does. Timestamps are deliberately not compared -- the pieces leave printlns microseconds apart and the format keeps milliseconds, so a boundary straddling a tick would strand exactly the long trace this exists for. All three passes learned it together, because they disagreeing is the bug this keeps reintroducing: readRows folds the tail in, scan skips it for the facets and lets it follow its entry into a filtered view, and entryStart steps over it so a window landing on the tail still opens on the entry that owns it. entryStart tests only the message, since the entry it would be compared against is further back than the line above and walking one line too far only widens the window. Copying an entry no longer reproduces the second prefix. What is copied is the message that was written rather than the transport that carried it, and a trace with a timestamp wedged into the middle of it is one nobody can paste anywhere useful.
Sixteen units across eighteen languages: the four labels on the crash card, the sentence for a trace with none of our frames in it, the two screen titles, "Caused by", the elided-frame and extra-line counts, the copied-frame confirmation, the unreadable-record notice, and the setting that decides where a trace in the log opens. Each plural takes the quantity set its language already uses in this file, which is the full CLDR set where the count is genuinely numeric — six forms in Arabic, four in Hebrew, Polish, Russian and Ukrainian, one in the languages that do not inflect for number. The terms follow the ones already chosen there rather than being coined again: the word each translator picked for a frame, and the word they picked for a trace in crash_recorded_summary. crash_frames_elided moves from %d to %1$d, which is what every other counted plural in these files uses and what the translations were written against.
JingMatrix
force-pushed
the
manager-log-trace
branch
from
August 1, 2026 11:31
ed043ac to
6f9ad02
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.
android.util.Logdrops a throwable's trace whenever anything in its cause chain is anUnknownHostException, deliberately. The message still prints, so nothing looks wrong; only the trace is gone. Our network use is a DoH resolver, a module store and a GitHub feed, so it removes traces from exactly the failures worth reporting:dns: DoH lookup of $host failedwas guaranteed to arrive without one, sinceDnsOverHttpssignals failure by throwing it. The same call wrote header-only crash files, and handed a blank line to any module reporting a failed request. So the trace is formatted here and appended to the message, with only the unfilteredLog.printlncalled;Utils.Log.getStackTraceStringdoes the same for the daemon and the legacy bridge.The log panel was losing half of every trace of its own accord. It judged what continued an entry by the look of a line: indented ones passed and everything else became a marker row, so
printStackTrace's flush-left header and everyCaused by:ended the run, and zygisk's mount-argument block shredded into twenty rows. The writer settles it instead —logcat.cppemits an entry as onewritevwhose firstiovecis the prefix, so an unprefixed line under an entry is a continuation by construction, and the only ones that are not are the four the daemon writes raw.One exception undoes a split rather than guessing at one:
printlnsbreaks an oversized message at the last newline that fits and emits the pieces as separate entries, so a 5.7 KB trace reached the panel as two rows. They rejoin on same tag, process, thread and level, immediate adjacency, and a tail that opens the way a continuation does. All three passes over the file apply the same rules; them disagreeing is the bug this kept reintroducing.With whole traces in hand, one renderer draws them everywhere: our frames marked, the platform's dimmed,
Caused byas a divider, each frame tappable to copy. The crash card states four facts rather than previewing monospace — what threw, what it said, the nearest frame that is ours, and when — with the trace a tap away, and in the log under its line or on a screen of its own.Sixteen new strings, filled in for all eighteen locales. Fifteen imports that nothing imports are deleted across five files.