Skip to content

Commit 61ba411

Browse files
committed
refactor(StartupAudit): enhance measurement and reporting of startup phases
- Updated RitsuLibStartupAudit to utilize a new MeasureScope for better tracking of inclusive and exclusive timing during phase measurements. - Improved the Record method to differentiate between inclusive and exclusive timings, enhancing the accuracy of performance reports. - Added caching mechanisms for missing asset paths in AssetPathDiagnostics to prevent redundant warnings and improve logging efficiency. - Introduced methods to manage a cache of missing paths, ensuring that warnings are only logged when necessary, thus reducing clutter in logs.
1 parent b0be0cf commit 61ba411

2 files changed

Lines changed: 124 additions & 12 deletions

File tree

Diagnostics/RitsuLibStartupAudit.cs

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@ namespace STS2RitsuLib.Diagnostics
1515
internal static class RitsuLibStartupAudit
1616
{
1717
private static readonly Lock Gate = new();
18-
private static readonly List<(string Phase, double Milliseconds)> Phases = [];
18+
private static readonly List<PhaseTiming> Phases = [];
1919
private static int _reportedCount;
2020

21+
[ThreadStatic] private static MeasureScope? _currentScope;
22+
2123
/// <summary>
2224
/// Times <paramref name="action" /> and records its duration under <paramref name="phase" />.
2325
/// 对 <paramref name="action" /> 计时,并以 <paramref name="phase" /> 记录其耗时。
2426
/// </summary>
2527
internal static void Measure(string phase, Action action)
2628
{
29+
var scope = PushScope(phase);
2730
var sw = Stopwatch.StartNew();
2831
try
2932
{
@@ -32,7 +35,7 @@ internal static void Measure(string phase, Action action)
3235
finally
3336
{
3437
sw.Stop();
35-
Record(phase, sw.Elapsed.TotalMilliseconds);
38+
PopScope(scope, sw.Elapsed.TotalMilliseconds);
3639
}
3740
}
3841

@@ -42,6 +45,7 @@ internal static void Measure(string phase, Action action)
4245
/// </summary>
4346
internal static T Measure<T>(string phase, Func<T> func)
4447
{
48+
var scope = PushScope(phase);
4549
var sw = Stopwatch.StartNew();
4650
try
4751
{
@@ -50,7 +54,7 @@ internal static T Measure<T>(string phase, Func<T> func)
5054
finally
5155
{
5256
sw.Stop();
53-
Record(phase, sw.Elapsed.TotalMilliseconds);
57+
PopScope(scope, sw.Elapsed.TotalMilliseconds);
5458
}
5559
}
5660

@@ -60,9 +64,10 @@ internal static T Measure<T>(string phase, Func<T> func)
6064
/// </summary>
6165
internal static void Record(string phase, double milliseconds)
6266
{
67+
_currentScope?.AddChild(milliseconds);
6368
lock (Gate)
6469
{
65-
Phases.Add((phase, milliseconds));
70+
Phases.Add(new(phase, milliseconds, milliseconds));
6671
}
6772
}
6873

@@ -77,20 +82,62 @@ internal static void LogReport(string title)
7782
if (Phases.Count <= _reportedCount)
7883
return;
7984

80-
var total = Phases.Sum(static entry => entry.Milliseconds);
85+
var total = Phases.Sum(static entry => entry.ExclusiveMilliseconds);
8186
var text = new StringBuilder()
8287
.AppendLine()
8388
.AppendLine($"=== RitsuLib Startup Audit: {title} ===");
8489

85-
foreach (var (phase, milliseconds) in Phases)
86-
text.AppendLine($" {phase}: {milliseconds:F1} ms");
90+
foreach (var timing in Phases)
91+
{
92+
text.Append($" {timing.Phase}: {timing.ExclusiveMilliseconds:F1} ms");
93+
if (Math.Abs(timing.InclusiveMilliseconds - timing.ExclusiveMilliseconds) >= 0.05d)
94+
text.Append($" (inclusive {timing.InclusiveMilliseconds:F1} ms)");
95+
96+
text.AppendLine();
97+
}
8798

8899
text.AppendLine(" ---")
89-
.Append($" RitsuLib self-time total: {total:F1} ms");
100+
.Append($" RitsuLib exclusive self-time total: {total:F1} ms");
90101

91102
_reportedCount = Phases.Count;
92103
RitsuLibFramework.Logger.Info(text.ToString());
93104
}
94105
}
106+
107+
private static MeasureScope PushScope(string phase)
108+
{
109+
var scope = new MeasureScope(phase, _currentScope);
110+
_currentScope = scope;
111+
return scope;
112+
}
113+
114+
private static void PopScope(MeasureScope scope, double inclusiveMilliseconds)
115+
{
116+
_currentScope = scope.Parent;
117+
scope.Parent?.AddChild(inclusiveMilliseconds);
118+
119+
var exclusiveMilliseconds = Math.Max(0d, inclusiveMilliseconds - scope.ChildMilliseconds);
120+
lock (Gate)
121+
{
122+
Phases.Add(new(scope.Phase, inclusiveMilliseconds, exclusiveMilliseconds));
123+
}
124+
}
125+
126+
private sealed class MeasureScope(string phase, MeasureScope? parent)
127+
{
128+
internal string Phase { get; } = phase;
129+
internal MeasureScope? Parent { get; } = parent;
130+
internal double ChildMilliseconds { get; private set; }
131+
132+
internal void AddChild(double milliseconds)
133+
{
134+
ChildMilliseconds += milliseconds;
135+
}
136+
}
137+
138+
private readonly record struct PhaseTiming(
139+
string Phase,
140+
double InclusiveMilliseconds,
141+
double ExclusiveMilliseconds);
95142
}
96143
}

Utils/AssetPathDiagnostics.cs

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,25 @@ namespace STS2RitsuLib.Utils
44
{
55
internal static class AssetPathDiagnostics
66
{
7+
private static readonly Lock StartupMissingPathCacheGate = new();
8+
private static readonly HashSet<string> StartupMissingPathCache = [];
9+
private static bool _startupMissingPathCacheEnabled = true;
10+
private static bool _startupMissingPathCacheShutdownRegistered;
11+
712
internal static bool Exists(string path, object owner, string memberName)
813
{
14+
var ownerLabel = DescribeOwner(owner);
15+
var cacheKey = BuildMissingPathCacheKey(ownerLabel, memberName, path);
16+
17+
if (IsCachedStartupMissingPath(cacheKey))
18+
return false;
19+
920
if (GodotResourcePath.ResourceExists(path))
1021
return true;
1122

12-
WarnMissingPath(owner, memberName, path);
23+
if (ShouldWarnMissingPath(cacheKey))
24+
WarnMissingPath(ownerLabel, memberName, path);
25+
1326
return false;
1427
}
1528

@@ -22,6 +35,10 @@ internal static bool Exists(string path, object owner, string memberName)
2235
internal static void WarnModCharacterAssetOverrideMissing(object owner, string memberName, string path)
2336
{
2437
var ownerLabel = DescribeOwner(owner);
38+
var cacheKey = BuildMissingPathCacheKey(ownerLabel, memberName, path);
39+
40+
if (!ShouldWarnMissingPath(cacheKey))
41+
return;
2542

2643
RitsuLibFramework.Logger.Warn(
2744
$"[Assets] Mod character asset override path not found for {ownerLabel}.{memberName}: '{path}'. " +
@@ -45,14 +62,62 @@ internal static string[] CollectExistingPaths(object owner,
4562
return [.. results];
4663
}
4764

48-
private static void WarnMissingPath(object owner, string memberName, string path)
65+
private static void WarnMissingPath(string ownerLabel, string memberName, string path)
4966
{
50-
var ownerLabel = DescribeOwner(owner);
51-
5267
RitsuLibFramework.Logger.Warn(
5368
$"[Assets] Missing resource path for {ownerLabel}.{memberName}: '{path}'. Falling back to the base asset.");
5469
}
5570

71+
private static bool IsCachedStartupMissingPath(string cacheKey)
72+
{
73+
EnsureStartupMissingPathCacheShutdownRegistered();
74+
75+
lock (StartupMissingPathCacheGate)
76+
{
77+
return _startupMissingPathCacheEnabled && StartupMissingPathCache.Contains(cacheKey);
78+
}
79+
}
80+
81+
private static bool ShouldWarnMissingPath(string cacheKey)
82+
{
83+
EnsureStartupMissingPathCacheShutdownRegistered();
84+
85+
lock (StartupMissingPathCacheGate)
86+
{
87+
return !_startupMissingPathCacheEnabled || StartupMissingPathCache.Add(cacheKey);
88+
}
89+
}
90+
91+
private static void EnsureStartupMissingPathCacheShutdownRegistered()
92+
{
93+
if (_startupMissingPathCacheShutdownRegistered)
94+
return;
95+
96+
lock (StartupMissingPathCacheGate)
97+
{
98+
if (_startupMissingPathCacheShutdownRegistered)
99+
return;
100+
101+
_startupMissingPathCacheShutdownRegistered = true;
102+
}
103+
104+
RitsuLibFramework.SubscribeLifecycleOnce<MainMenuReadyEvent>(_ => DisableStartupMissingPathCache());
105+
}
106+
107+
private static void DisableStartupMissingPathCache()
108+
{
109+
lock (StartupMissingPathCacheGate)
110+
{
111+
_startupMissingPathCacheEnabled = false;
112+
StartupMissingPathCache.Clear();
113+
}
114+
}
115+
116+
private static string BuildMissingPathCacheKey(string ownerLabel, string memberName, string path)
117+
{
118+
return $"{ownerLabel}\n{memberName}\n{path}";
119+
}
120+
56121
private static string DescribeOwner(object owner)
57122
{
58123
try

0 commit comments

Comments
 (0)