Skip to content

Commit e5e4867

Browse files
authored
Merge pull request #247 from winnerspiros/copilot/fix-black-screen-issue-another-one
Bound Android diagnostics to ~18 MiB on disk
2 parents e4673a6 + 70be5b9 commit e5e4867

3 files changed

Lines changed: 100 additions & 11 deletions

File tree

osu.Android/CrashDiagnostics.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ internal static class CrashDiagnostics
3434
public const string CRASH_LOG_NAME = "native_crash.log";
3535
public const string SENTINEL_NAME = "crash_handler_installed.txt";
3636

37+
// Hard size cap on a single native_crash.log file. When reached we rotate the
38+
// file to "<name>.1" (overwriting any previous backup) and start a fresh log.
39+
// This bounds *each* of the internal and external locations to ~2× the cap
40+
// worst-case, regardless of how many crash-restart cycles the device endures.
41+
//
42+
// The cap exists to defeat the failure mode observed in the field where a
43+
// tight ANR-restart loop produced ~480 MB of native_crash.log on the user's
44+
// device storage in a few hours — every restart appended the previous
45+
// process's HangWatchdog dumps to the external log via
46+
// MirrorInternalLogToExternal, with no upper bound. 3 MiB is enough to hold
47+
// ~6 full HangWatchdog hang dumps including the per-thread /proc snapshot,
48+
// i.e. comfortably more than one process's worth of evidence after the new
49+
// HangWatchdog cap (max_dumps_per_process=20) is applied.
50+
private const long native_crash_log_max_bytes = 3L * 1024 * 1024;
51+
private const string crash_log_backup_suffix = ".1";
52+
3753
private static int initialised;
3854
private static int managedHooksInstalled;
3955

@@ -167,6 +183,17 @@ public static void MirrorInternalLogToExternal()
167183

168184
string externalPath = Path.Combine(externalDir, CRASH_LOG_NAME);
169185

186+
// Defeat the unbounded-growth failure mode: a tight ANR-restart
187+
// loop calls MirrorInternalLogToExternal on every startup, each
188+
// of which appends the previous process's full HangWatchdog
189+
// dump set to the external log. Without this rotation the
190+
// external file grew to hundreds of MB on the user's device
191+
// (one report: 480 MB across a single afternoon). Rotating
192+
// *before* the append guarantees the resulting file is at most
193+
// <native_crash_log_max_bytes + this_payload_size>, and a
194+
// single ".1" backup retains the previous generation.
195+
rotateIfTooLarge(externalPath);
196+
170197
try
171198
{
172199
// Append, not overwrite — keep external as the running historical log.
@@ -379,6 +406,14 @@ private static void tryAppend(string? dir, string payload)
379406
try
380407
{
381408
string path = Path.Combine(dir, CRASH_LOG_NAME);
409+
410+
// Bound the file size before opening for append. A pathological
411+
// crash-restart loop would otherwise write hundreds of MB into
412+
// this single file — the rotation cap (one historical backup,
413+
// each ≤ native_crash_log_max_bytes) keeps the worst-case at
414+
// ~2× the cap regardless of how long the loop runs.
415+
rotateIfTooLarge(path);
416+
382417
using var fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
383418
using var sw = new StreamWriter(fs);
384419
sw.Write(payload);
@@ -390,6 +425,44 @@ private static void tryAppend(string? dir, string payload)
390425
}
391426
}
392427

428+
// If <path> exists and is at or above the size cap, move it to
429+
// "<path>.1" (overwriting any previous backup) so the next write starts
430+
// a fresh file. Best-effort and never throws — diagnostics paths must
431+
// not introduce new failure modes.
432+
private static void rotateIfTooLarge(string path)
433+
{
434+
try
435+
{
436+
if (!File.Exists(path)) return;
437+
438+
long length;
439+
try { length = new FileInfo(path).Length; }
440+
catch { return; }
441+
442+
if (length < native_crash_log_max_bytes) return;
443+
444+
string backup = path + crash_log_backup_suffix;
445+
446+
try { if (File.Exists(backup)) File.Delete(backup); }
447+
catch (Exception e) { Debug.WriteLine($"[osu!] CrashDiagnostics.rotateIfTooLarge: could not delete prior backup {backup}: {e.Message}"); }
448+
449+
try { File.Move(path, backup); }
450+
catch (Exception e)
451+
{
452+
// If rename fails (e.g. cross-device on some FUSE setups),
453+
// fall back to in-place truncation rather than leaving the
454+
// file unbounded.
455+
Debug.WriteLine($"[osu!] CrashDiagnostics.rotateIfTooLarge: rename failed ({e.Message}); truncating in place");
456+
try { File.WriteAllText(path, string.Empty); }
457+
catch (Exception inner) { Debug.WriteLine($"[osu!] CrashDiagnostics.rotateIfTooLarge: truncate also failed: {inner.Message}"); }
458+
}
459+
}
460+
catch (Exception e)
461+
{
462+
Debug.WriteLine($"[osu!] CrashDiagnostics.rotateIfTooLarge outer failure for {path}: {e.Message}");
463+
}
464+
}
465+
393466
private static void resolveDirs(Context context)
394467
{
395468
try

osu.Android/HangWatchdog.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ namespace osu.Android
3030
///
3131
/// <para>
3232
/// The hang threshold is intentionally short (5s): the runtime log can grow
33-
/// to ~70MB on the user's device, so we'd rather over-dump than miss a
34-
/// stall, but we still rate-limit re-dumps of the same hang to one every
35-
/// 10s so we don't fill the log in a single second of frozen state.
33+
/// quickly on a stuck device, so we'd rather over-detect than miss a stall.
34+
/// We rate-limit re-dumps of the same hang to one every 30s and cap the
35+
/// per-process dump count at 20 so a permanently-hung process cannot on its
36+
/// own fill the bounded <c>native_crash.log</c> budget — earlier (200-dump,
37+
/// 10s-cooldown) settings were the dominant contributor to the ~480 MB
38+
/// log explosion observed in the field on devices stuck in an ANR-restart
39+
/// loop, where every restart appended the previous process's full dump set
40+
/// to the external log via <c>MirrorInternalLogToExternal</c>.
3641
/// </para>
3742
/// </summary>
3843
internal static class HangWatchdog
@@ -47,14 +52,22 @@ internal static class HangWatchdog
4752
private const int heartbeat_interval_ms = 1_000;
4853

4954
// Minimum gap between two consecutive snapshots while still hung. Without
50-
// this, a 60s hang would generate 12 full /proc/self/task dumps and
51-
// potentially blow the log size cap in a few seconds.
52-
private const int redump_cooldown_ms = 10_000;
55+
// this, a 60s hang would generate many full /proc/self/task dumps and
56+
// potentially blow the log size cap in a few seconds. 30s is plenty for
57+
// a "still hung" signal — the HangWatchdog is for diagnosing *that* the
58+
// thread hung and *roughly when*, not for sampling its state every
59+
// second; the high-frequency sampling rate in earlier iterations was
60+
// the dominant contributor to the 480 MB log explosion observed in the
61+
// field on devices stuck in an ANR-restart loop.
62+
private const int redump_cooldown_ms = 30_000;
5363

5464
// Maximum number of distinct hang dumps written for the lifetime of the
5565
// process. Prevents pathological "permanent hang plus runaway watchdog"
5666
// from filling the log indefinitely if the cooldown logic ever misbehaves.
57-
private const int max_dumps_per_process = 200;
67+
// 20 dumps × ~6 KB ≈ 120 KB worst-case per process, well within the
68+
// CrashDiagnostics rotation cap (~3 MiB) so a single hung process can
69+
// never on its own exhaust the bounded native_crash.log budget.
70+
private const int max_dumps_per_process = 20;
5871

5972
private static int started;
6073
private static Thread? monitorThread;

osu.Android/LogManagement.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,13 @@ namespace osu.Android
3939
internal static class LogManagement
4040
{
4141
// Hard cap on the total bytes consumed by *.log files in the log directory.
42-
// 20 MiB matches the explicit user request and is large enough to retain
43-
// ~30 successive Important-level launches even when each one logs an
44-
// unhandled exception with full stack trace.
45-
public const long MAX_LOG_BYTES = 20L * 1024 * 1024;
42+
// 6 MiB is chosen so that the user's overall on-disk diagnostics budget
43+
// (~20 MiB target) divides into ~6 MiB runtime logs + ~6 MiB internal
44+
// native_crash.log (capped via CrashDiagnostics rotation) + ~6 MiB
45+
// external native_crash.log (same cap). Important-level logs are very
46+
// small per launch (~327 bytes for a clean cold start observed in the
47+
// field), so 6 MiB still retains thousands of successive launches.
48+
public const long MAX_LOG_BYTES = 6L * 1024 * 1024;
4649

4750
// Subdirectory under the game storage root where the framework logger
4851
// writes per-session log files. Mirrors osu.Game/IO/OsuStorage.cs:140

0 commit comments

Comments
 (0)