forked from ppy/osu
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAndroidHighPerformanceSessionManager.cs
More file actions
180 lines (159 loc) · 7.96 KB
/
Copy pathAndroidHighPerformanceSessionManager.cs
File metadata and controls
180 lines (159 loc) · 7.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game.Performance;
namespace osu.Android.Performance
{
public class AndroidHighPerformanceSessionManager : IHighPerformanceSessionManager
{
public bool IsSessionActive => activeSessions > 0;
private int activeSessions;
private GCLatencyMode originalGCMode;
/// <summary>
/// One-shot disable. Mono on Android throws <see cref="PlatformNotSupportedException"/>
/// from the <see cref="GCSettings.LatencyMode"/> setter (and, on some runtimes, the
/// getter). We must not let that exception escape — it would crash the
/// game every time the user enters <c>PlayerLoader</c>, holds a mouse
/// button, or otherwise triggers a high-performance session, since
/// <see cref="BeginSession"/> is invoked on the update thread and the
/// throw propagates up through <c>UpdateSubTree</c>.
/// </summary>
private static bool gcLatencyModeSupported = true;
/// <summary>
/// One-shot disable for <see cref="GC.TryStartNoGCRegion(long, bool)"/>.
/// The API is not implemented on Mono for Android and will throw
/// <see cref="NotImplementedException"/> or <see cref="PlatformNotSupportedException"/>
/// on older runtimes. We disable it permanently on the first failure.
/// </summary>
private static bool noGCRegionSupported = true;
/// <summary>Whether the current session successfully started a no-GC region.</summary>
private bool noGCRegionActive;
/// <summary>
/// Heap budget in bytes for <see cref="GC.TryStartNoGCRegion(long, bool)"/>.
/// 64 MB covers typical per-map allocation rates (~4–8 MB/s × ~8 s per map load).
/// When this budget is exhausted the runtime silently reverts to normal GC, so
/// the value acts as an upper-bound guarantee rather than a hard limit.
/// </summary>
private const long NO_GC_REGION_BUDGET_BYTES = 64 * 1024 * 1024;
public IDisposable BeginSession()
{
enterSession();
return new InvokeOnDisposal<AndroidHighPerformanceSessionManager>(this, static m => m.exitSession());
}
private void enterSession()
{
if (Interlocked.Increment(ref activeSessions) > 1)
{
Logger.Log($"High performance session requested ({activeSessions} running in total)");
return;
}
Logger.Log("Starting high performance session (Android)");
if (!gcLatencyModeSupported)
return;
// Pre-drain accumulated garbage before entering the low-latency window.
// SustainedLowLatency suppresses Gen2 (major) GC, so any garbage already
// on the heap will persist for the entire session. A non-blocking hint here
// asks the runtime to schedule a collection immediately — the call returns
// in microseconds and the GC runs in background. On .NET runtimes that
// support it, this eliminates the most common source of a multi-frame GC
// stall right at the start of gameplay (the "first-note hitbox miss"
// symptom observed across multiple field sessions).
//
// GCCollectionMode.Optimized + blocking:false requires .NET Core 3.0+ / .NET 5+.
// On Mono (older .NET for Android runtimes) it throws NotSupportedException,
// and on some niche OEM runtimes it may throw PlatformNotSupportedException.
// The catch-all deliberately swallows these: the call is a best-effort hint
// and the cost of it failing is exactly zero (the code path below proceeds
// identically).
try
{
GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized, blocking: false);
}
catch
{
// Non-critical hint; intentionally swallows NotSupportedException /
// PlatformNotSupportedException on older or non-.NET-Core runtimes.
}
try
{
originalGCMode = GCSettings.LatencyMode;
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
}
catch (PlatformNotSupportedException)
{
// Mono on Android does not implement GCSettings.LatencyMode.
// Latch off so subsequent sessions skip the throwing call entirely
// (the unhandled-exception allowance is finite and would burn out
// after a few gameplay entries, killing the process).
gcLatencyModeSupported = false;
Logger.Log("GCSettings.LatencyMode unsupported on this runtime; high-performance GC tuning disabled.");
}
// Suppress all GC generations for the duration of the session.
// GC.TryStartNoGCRegion(budget, disallowFullBlockingGC:false) blocks Gen0+Gen1+Gen2
// collection until the budget is exhausted; if allocation exceeds the budget
// the runtime silently reverts to normal GC — the failure mode is "old behaviour",
// not a crash. A 64 MB budget covers typical per-map allocation rates.
// This eliminates the residual Gen0/Gen1 pauses that SustainedLowLatency
// (which only suppresses Gen2) leaves intact.
//
// GC.TryStartNoGCRegion is a .NET Core / .NET 5+ API. Mono for Android
// older runtimes throw NotImplementedException; we disable it permanently
// on the first failure to avoid repeated catching overhead.
if (noGCRegionSupported && !noGCRegionActive)
{
try
{
// disallowFullBlockingGC: false — if allocation exceeds the budget,
// the runtime silently reverts to normal GC instead of throwing.
noGCRegionActive = GC.TryStartNoGCRegion(NO_GC_REGION_BUDGET_BYTES, disallowFullBlockingGC: false);
if (noGCRegionActive)
Logger.Log("High performance session: no-GC region started (64 MB budget)");
}
catch
{
noGCRegionSupported = false;
Logger.Log("GC.TryStartNoGCRegion unsupported on this runtime; skipping no-GC region.");
}
}
}
private void exitSession()
{
if (Interlocked.Decrement(ref activeSessions) > 0)
{
Logger.Log($"High performance session finished ({activeSessions} others remain)");
return;
}
Logger.Log("Ending high performance session (Android)");
if (!gcLatencyModeSupported)
return;
try
{
if (GCSettings.LatencyMode == GCLatencyMode.SustainedLowLatency)
GCSettings.LatencyMode = originalGCMode;
}
catch (PlatformNotSupportedException)
{
gcLatencyModeSupported = false;
}
if (noGCRegionActive)
{
try
{
GC.EndNoGCRegion();
}
catch
{
// EndNoGCRegion can throw InvalidOperationException if we are not actually
// inside a no-GC region (e.g. budget was exhausted and the runtime exited
// it automatically). Swallow: the goal was to reduce pauses and the
// runtime has already managed the transition gracefully.
}
noGCRegionActive = false;
}
}
}
}