Capture actionable STEP-LIMIT diagnostics
This commit is contained in:
@@ -15,6 +15,7 @@ public sealed class GodotTraceSink : ITraceSink
|
||||
private readonly Stack<string> _scripts = new();
|
||||
private readonly Queue<GodotTraceStepSnapshot> _recentSteps = new();
|
||||
private GodotTraceStepSnapshot? _latestStep;
|
||||
private GodotTraceSnapshot? _haltSnapshot;
|
||||
public GodotTraceSink(PageLocatorState locator, GodotTimelineLog? timeline = null)
|
||||
{ _locator = locator; _timeline = timeline; }
|
||||
// The page locator needs the exact script/offset even when the heavier timeline log is disabled.
|
||||
@@ -54,6 +55,8 @@ public sealed class GodotTraceSink : ITraceSink
|
||||
string[] callStack;
|
||||
lock (_snapshotLock)
|
||||
{
|
||||
if (e.Text == "Halted" && _haltSnapshot == null)
|
||||
_haltSnapshot = SnapshotLocked();
|
||||
if (_scripts.Count > 0) _scripts.Pop();
|
||||
callStack = CurrentCallStackLocked();
|
||||
}
|
||||
@@ -84,17 +87,13 @@ public sealed class GodotTraceSink : ITraceSink
|
||||
|
||||
public GodotTraceSnapshot Snapshot()
|
||||
{
|
||||
lock (_snapshotLock)
|
||||
{
|
||||
GodotTraceStepSnapshot? current = _latestStep;
|
||||
return new GodotTraceSnapshot(
|
||||
current?.Script ?? (_scripts.Count > 0 ? _scripts.Peek() : "<unknown>"),
|
||||
current?.Offset ?? -1,
|
||||
current?.Opcode ?? -1,
|
||||
current?.Depth ?? System.Math.Max(0, _scripts.Count - 1),
|
||||
CurrentCallStackLocked(),
|
||||
_recentSteps.ToArray());
|
||||
}
|
||||
lock (_snapshotLock) return SnapshotLocked();
|
||||
}
|
||||
|
||||
/// <summary>The deepest still-active script stack captured before a halted frame unwinds.</summary>
|
||||
public GodotTraceSnapshot? HaltSnapshot
|
||||
{
|
||||
get { lock (_snapshotLock) return _haltSnapshot; }
|
||||
}
|
||||
|
||||
/// <summary>Allocation-free current coordinate for once-per-frame diagnostics.</summary>
|
||||
@@ -109,6 +108,18 @@ public sealed class GodotTraceSink : ITraceSink
|
||||
System.Array.Reverse(stack);
|
||||
return stack;
|
||||
}
|
||||
|
||||
private GodotTraceSnapshot SnapshotLocked()
|
||||
{
|
||||
GodotTraceStepSnapshot? current = _latestStep;
|
||||
return new GodotTraceSnapshot(
|
||||
current?.Script ?? (_scripts.Count > 0 ? _scripts.Peek() : "<unknown>"),
|
||||
current?.Offset ?? -1,
|
||||
current?.Opcode ?? -1,
|
||||
current?.Depth ?? System.Math.Max(0, _scripts.Count - 1),
|
||||
CurrentCallStackLocked(),
|
||||
_recentSteps.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record GodotTraceStepSnapshot(string Script, int Offset, int Opcode, int Depth);
|
||||
|
||||
@@ -580,6 +580,12 @@ public partial class Main : Godot.Control
|
||||
GD.Print($"[vm] ended: {_vm!.HaltReason ?? "unknown"} after {_vm.Steps} steps");
|
||||
ReportSubroutines();
|
||||
ShowEnd();
|
||||
if (_vm.HaltReason == "STEP-LIMIT")
|
||||
{
|
||||
GodotTraceSnapshot haltTrace = _trace.HaltSnapshot ?? _trace.Snapshot();
|
||||
GD.Print(StepLimitDiagnosticFormatter.Format(haltTrace, _table!));
|
||||
CaptureStallDiagnostic(haltTrace, "step-limit");
|
||||
}
|
||||
if (_selftest) RunSelfTest();
|
||||
}
|
||||
}
|
||||
@@ -742,12 +748,13 @@ public partial class Main : Godot.Control
|
||||
private static bool IsAdvanceAction(int action) => action is 4 or 5;
|
||||
private static bool HasAdvanceAction(int mask) => (mask & ((1 << 4) | (1 << 5))) != 0;
|
||||
|
||||
private void CaptureStallDiagnostic()
|
||||
private void CaptureStallDiagnostic(GodotTraceSnapshot? traceOverride = null,
|
||||
string snapshotKind = "stall")
|
||||
{
|
||||
try
|
||||
{
|
||||
long nowMs = _clock.NowMs;
|
||||
GodotTraceSnapshot trace = _trace.Snapshot();
|
||||
GodotTraceSnapshot trace = traceOverride ?? _trace.Snapshot();
|
||||
var activeMovies = _movies
|
||||
.OrderBy(pair => pair.Key)
|
||||
.Select(pair =>
|
||||
@@ -815,7 +822,7 @@ public partial class Main : Godot.Control
|
||||
string directory = ProjectSettings.GlobalizePath("user://diagnostics");
|
||||
System.IO.Directory.CreateDirectory(directory);
|
||||
string path = System.IO.Path.Combine(directory,
|
||||
$"stall-{System.DateTimeOffset.Now:yyyyMMdd-HHmmss-fff}.json");
|
||||
$"{snapshotKind}-{System.DateTimeOffset.Now:yyyyMMdd-HHmmss-fff}.json");
|
||||
var jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
@@ -825,10 +832,10 @@ public partial class Main : Godot.Control
|
||||
string coordinate = trace.CurrentOffset >= 0
|
||||
? $"{System.IO.Path.GetFileNameWithoutExtension(trace.CurrentScript).ToUpperInvariant()}@0x{trace.CurrentOffset:x}"
|
||||
: trace.CurrentScript;
|
||||
string clipboard = $"{coordinate} · stall snapshot {path}";
|
||||
string clipboard = $"{coordinate} · {snapshotKind} snapshot {path}";
|
||||
DisplayServer.ClipboardSet(clipboard);
|
||||
_status.Text = $"Diagnostic saved: {coordinate} (path copied)";
|
||||
GD.Print($"[diagnostic] stall snapshot {coordinate} -> {path}");
|
||||
GD.Print($"[diagnostic] {snapshotKind} snapshot {coordinate} -> {path}");
|
||||
}
|
||||
catch (System.Exception exception)
|
||||
{
|
||||
|
||||
46
godot/StepLimitDiagnosticFormatter.cs
Normal file
46
godot/StepLimitDiagnosticFormatter.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Age.Engine.Model;
|
||||
|
||||
/// <summary>Formats the bounded trace retained by the Godot frontend when the VM safety cap fires.</summary>
|
||||
public static class StepLimitDiagnosticFormatter
|
||||
{
|
||||
public static string Format(GodotTraceSnapshot snapshot, OpcodeTable table,
|
||||
int topSites = 8, int tailSteps = 16)
|
||||
{
|
||||
string Location(GodotTraceStepSnapshot step)
|
||||
{
|
||||
string script = Path.GetFileNameWithoutExtension(step.Script).ToUpperInvariant();
|
||||
string mnemonic = table.Label(step.Opcode);
|
||||
if (string.IsNullOrEmpty(mnemonic)) mnemonic = "unknown";
|
||||
return $"{script}@0x{step.Offset:x} op=0x{step.Opcode:x3} {mnemonic}";
|
||||
}
|
||||
|
||||
var output = new StringBuilder();
|
||||
var current = new GodotTraceStepSnapshot(
|
||||
snapshot.CurrentScript, snapshot.CurrentOffset, snapshot.CurrentOpcode, snapshot.CurrentDepth);
|
||||
output.AppendLine($"[step-limit] last: {Location(current)} depth={snapshot.CurrentDepth}");
|
||||
output.AppendLine($"[step-limit] frames: {string.Join(" > ",
|
||||
snapshot.CallStack.Select(name => Path.GetFileNameWithoutExtension(name).ToUpperInvariant()))}");
|
||||
output.AppendLine($"[step-limit] hot sites in final {snapshot.RecentSteps.Count} steps:");
|
||||
foreach (var site in snapshot.RecentSteps
|
||||
.GroupBy(step => (step.Script, step.Offset, step.Opcode))
|
||||
.OrderByDescending(group => group.Count())
|
||||
.ThenBy(group => group.Key.Script, StringComparer.Ordinal)
|
||||
.ThenBy(group => group.Key.Offset)
|
||||
.Take(Math.Max(0, topSites)))
|
||||
{
|
||||
var sample = new GodotTraceStepSnapshot(
|
||||
site.Key.Script, site.Key.Offset, site.Key.Opcode, 0);
|
||||
output.AppendLine($"[step-limit] {site.Count(),4}x {Location(sample)}");
|
||||
}
|
||||
|
||||
int tailStart = Math.Max(0, snapshot.RecentSteps.Count - Math.Max(0, tailSteps));
|
||||
output.AppendLine($"[step-limit] final {snapshot.RecentSteps.Count - tailStart} steps:");
|
||||
for (int index = tailStart; index < snapshot.RecentSteps.Count; index++)
|
||||
output.AppendLine($"[step-limit] {Location(snapshot.RecentSteps[index])}");
|
||||
return output.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user