Skip to content

Commit 181e6ad

Browse files
Throttle FirstChanceException disk floods on Draw thread
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/c49ade03-b6dc-45b5-81e0-3c6d779f6e31 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent 01cb23d commit 181e6ad

1 file changed

Lines changed: 38 additions & 2 deletions

File tree

osu.Android/CrashDiagnostics.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,25 @@ internal static class CrashDiagnostics
3737
private static int initialised;
3838
private static int managedHooksInstalled;
3939

40+
// Global cap on FirstChanceException dumps written per process. A hot-path
41+
// throw loop (e.g. Veldrid "surface lost" thrown every Draw frame while the
42+
// Android Vulkan surface is unavailable during a slow startup) can otherwise
43+
// produce hundreds of full-stack dumps, each one a synchronous file write
44+
// on the throwing thread — which itself stalls the Draw thread and worsens
45+
// the very condition causing the throws.
46+
private const int first_chance_global_cap = 50;
47+
48+
// Per-unique-stack cap. Higher (10) for true fatal kinds caught via
49+
// FirstChanceException-fallback or AppDomain.UnhandledException; lower (3)
50+
// for first-chance noise where seeing the first few occurrences is enough
51+
// to diagnose and the rest are pure log bloat.
52+
private const int per_key_cap_default = 10;
53+
private const int per_key_cap_first_chance = 3;
54+
4055
private static string? internalDir;
4156
private static string? externalDir;
4257
private static readonly ConcurrentDictionary<string, int> exceptionCounts = new ConcurrentDictionary<string, int>();
58+
private static int firstChanceWriteCount;
4359
private static string? sentinelPath;
4460
private static string? installedLogPath;
4561
private static bool sentinelWritten;
@@ -290,8 +306,18 @@ private static void writeManagedException(string source, Exception? ex)
290306
if (ex is EntryPointNotFoundException && ex.Message.Contains("CFStringCreateWithCharacters"))
291307
return;
292308

309+
bool isFirstChance = source.StartsWith("FirstChanceException", StringComparison.Ordinal);
310+
311+
// Global cap on first-chance dumps: a hot-path throw loop on the Draw
312+
// thread can otherwise produce unbounded synchronous file writes, which
313+
// themselves stall the Draw thread and worsen the surface-acquisition
314+
// problem that caused the throws.
315+
if (isFirstChance && Interlocked.Increment(ref firstChanceWriteCount) > first_chance_global_cap)
316+
return;
317+
318+
int perKeyCap = isFirstChance ? per_key_cap_first_chance : per_key_cap_default;
293319
string key = $"{source}_{ex?.GetType().Name}_{ex?.StackTrace?.GetHashCode() ?? 0}";
294-
if (exceptionCounts.AddOrUpdate(key, 1, (_, count) => count + 1) > 10)
320+
if (exceptionCounts.AddOrUpdate(key, 1, (_, count) => count + 1) > perKeyCap)
295321
return;
296322

297323
try
@@ -306,7 +332,17 @@ private static void writeManagedException(string source, Exception? ex)
306332
"\n" +
307333
(ex?.ToString() ?? "<no exception object>") + "\n" +
308334
"=== END OF MANAGED EXCEPTION ===\n\n";
309-
appendToBoth(block);
335+
336+
// For FirstChanceException we deliberately skip the external/FUSE
337+
// write — those writes are tens of milliseconds each and run on the
338+
// throwing thread (often the Draw thread). MirrorInternalLogToExternal
339+
// copies the internal log to external on the next startup, which is
340+
// sufficient for user-facing diagnostics without risking a Draw-thread
341+
// stall in the live process.
342+
if (isFirstChance)
343+
tryAppend(internalDir, block);
344+
else
345+
appendToBoth(block);
310346
}
311347
catch (Exception e)
312348
{

0 commit comments

Comments
 (0)