Capture actionable STEP-LIMIT diagnostics

This commit is contained in:
gamer147
2026-07-29 18:51:06 -04:00
parent 32c5d1e901
commit f7751c2b93
7 changed files with 146 additions and 16 deletions

View File

@@ -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);

View File

@@ -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)
{

View 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();
}
}