@@ -485,11 +485,21 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa
485485 }
486486 }
487487
488- // Bound the read used by DetectPreviousDrawThreadNativeCrash. The native crash
489- // block + register dump + backtrace is ~2 KB; 256 KiB easily covers the most
490- // recent block even when the file also contains many ALIVE markers and a
491- // HangWatchdog dump from the previous process.
492- private const long crash_log_scan_byte_cap = 256L * 1024 ;
488+ // Bound the read used by DetectPreviousDrawThreadNativeCrash.
489+ //
490+ // Footer path (new, fast): crash_handler.cpp appends a compact
491+ // "=== CRASH FOOTER ===" line AFTER the /proc/self/maps dump, so it
492+ // always lands in the last ~1 KiB of the log. We scan only the last
493+ // crash_log_footer_scan_byte_cap bytes to find it quickly.
494+ //
495+ // Header-block fallback (legacy): older builds without the footer
496+ // require scanning far enough back to reach the "[osu!] NATIVE CRASH"
497+ // marker, which can be hundreds of KiB from the end because the
498+ // /proc/self/maps section is typically 400–500 KiB on Android. The
499+ // log is bounded at ~3 MiB by the rotation logic, so 4 MiB covers
500+ // the entire file in the worst case.
501+ private const long crash_log_footer_scan_byte_cap = 32L * 1024 ;
502+ private const long crash_log_scan_byte_cap = 4L * 1024 * 1024 ;
493503
494504 /// <summary>
495505 /// Inspect the on-disk <c>native_crash.log</c> for the most recent
@@ -534,6 +544,25 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa
534544 string path = Path . Combine ( dir , CRASH_LOG_NAME ) ;
535545 if ( ! File . Exists ( path ) ) return null ;
536546
547+ // --- Fast path: look for the compact footer line appended by
548+ // crash_handler.cpp after the /proc/self/maps dump. It is
549+ // always in the last few KiB of the log, so a small read is
550+ // enough. Falls through to the legacy full-header scan if the
551+ // footer is absent (older native builds).
552+ using ( var fs = new FileStream ( path , FileMode . Open , FileAccess . Read , FileShare . ReadWrite ) )
553+ {
554+ long footerStart = Math . Max ( 0 , fs . Length - crash_log_footer_scan_byte_cap ) ;
555+ fs . Seek ( footerStart , SeekOrigin . Begin ) ;
556+ using var sr = new StreamReader ( fs ) ;
557+ string footerTail = sr . ReadToEnd ( ) ;
558+
559+ var fromFooter = tryParseFooter ( footerTail ) ;
560+ if ( fromFooter != null ) return fromFooter ;
561+ }
562+
563+ // --- Legacy path: the full-header "[osu!] NATIVE CRASH" block.
564+ // The /proc/self/maps section can be 400–500 KiB, so we scan
565+ // the last 4 MiB (the log rotation cap) to guarantee we reach it.
537566 string tail ;
538567
539568 using ( var fs = new FileStream ( path , FileMode . Open , FileAccess . Read , FileShare . ReadWrite ) )
@@ -598,6 +627,68 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa
598627 }
599628 }
600629
630+ // Parse the compact "=== CRASH FOOTER ===" line appended by crash_handler.cpp
631+ // after the /proc/self/maps section. Format:
632+ // === CRASH FOOTER sig=<SIGNAL> pid=<PID> uptime_ns=<NS> thread=<NAME> ===
633+ // Returns null if no valid footer line is found or the crash is not a
634+ // fatal Draw-thread event.
635+ private static DrawThreadNativeCrashInfo ? tryParseFooter ( string tail )
636+ {
637+ const string footer_marker = "=== CRASH FOOTER " ;
638+ int last = tail . LastIndexOf ( footer_marker , StringComparison . Ordinal ) ;
639+ if ( last < 0 ) return null ;
640+
641+ int lineEnd = tail . IndexOf ( '\n ' , last ) ;
642+ if ( lineEnd < 0 ) lineEnd = tail . Length ;
643+ string line = tail . Substring ( last , lineEnd - last ) ;
644+
645+ string ? signal = extractFooterField ( line , " sig=" , " " ) ;
646+ string ? pid = extractFooterField ( line , " pid=" , " " ) ;
647+ string ? uptime = extractFooterField ( line , " uptime_ns=" , " " ) ;
648+ string ? thread = extractFooterField ( line , " thread=" , " ===" ) ;
649+
650+ if ( signal == null || thread == null ) return null ;
651+
652+ bool isFatalSignal = signal . StartsWith ( "SIGSEGV" , StringComparison . Ordinal )
653+ || signal . StartsWith ( "SIGBUS" , StringComparison . Ordinal )
654+ || signal . StartsWith ( "SIGABRT" , StringComparison . Ordinal ) ;
655+ if ( ! isFatalSignal ) return null ;
656+
657+ if ( ! thread . StartsWith ( "Draw" , StringComparison . Ordinal ) ) return null ;
658+
659+ string fingerprint = ( uptime != null && pid != null )
660+ ? $ "u{ uptime } -p{ pid } "
661+ : "footer:" + ( ( uint ) line . GetHashCode ( ) ) . ToString ( "x" ) ;
662+
663+ // The footer does not carry a top-frame symbol — report it as such.
664+ return new DrawThreadNativeCrashInfo ( fingerprint , signal , thread , "(footer — no top frame)" ) ;
665+ }
666+
667+ // Extract a field value from a single footer line.
668+ // Reads from after `key` to either the first occurrence of `stopBefore`
669+ // or the end of the line (whichever comes first).
670+ private static string ? extractFooterField ( string line , string key , string ? stopBefore )
671+ {
672+ int idx = line . IndexOf ( key , StringComparison . Ordinal ) ;
673+ if ( idx < 0 ) return null ;
674+
675+ int start = idx + key . Length ;
676+ int end ;
677+
678+ if ( stopBefore != null )
679+ {
680+ end = line . IndexOf ( stopBefore , start , StringComparison . Ordinal ) ;
681+ if ( end < 0 ) end = line . Length ;
682+ }
683+ else
684+ {
685+ end = line . IndexOf ( ' ' , start ) ;
686+ if ( end < 0 ) end = line . Length ;
687+ }
688+
689+ return line . Substring ( start , end - start ) ;
690+ }
691+
601692 private static string ? extractField ( string block , string keyWithEquals )
602693 {
603694 int idx = block . IndexOf ( keyWithEquals , StringComparison . Ordinal ) ;
0 commit comments