feat(diagnostics): ITraceSink seam + TraceEvent + Null/Text sinks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
12
engine/Age.Engine/Diagnostics/ITraceSink.cs
Normal file
12
engine/Age.Engine/Diagnostics/ITraceSink.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Age.Engine.Diagnostics;
|
||||
|
||||
/// <summary>The engine's diagnostics seam. The VM emits typed <see cref="TraceEvent"/>s here; any
|
||||
/// consumer (CLI, Godot, tests) supplies a sink instead of reimplementing IHost. Observe-only:
|
||||
/// a sink never reads/writes VM state or influences control flow (that guarantees trace parity).</summary>
|
||||
public interface ITraceSink
|
||||
{
|
||||
/// <summary>Cheap gate: when false the VM skips constructing per-instruction Step events, keeping
|
||||
/// the hot path (a corpus sweep is ~1.46M instructions) free. Rare events emit regardless.</summary>
|
||||
bool TracingSteps { get; }
|
||||
void Emit(in TraceEvent e);
|
||||
}
|
||||
11
engine/Age.Engine/Diagnostics/NullTraceSink.cs
Normal file
11
engine/Age.Engine/Diagnostics/NullTraceSink.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Age.Engine.Diagnostics;
|
||||
|
||||
/// <summary>The inert default: no step tracing, empty Emit. Supplying this (or null) to the VM
|
||||
/// guarantees byte-identical behavior.</summary>
|
||||
public sealed class NullTraceSink : ITraceSink
|
||||
{
|
||||
public static readonly NullTraceSink Instance = new();
|
||||
private NullTraceSink() { }
|
||||
public bool TracingSteps => false;
|
||||
public void Emit(in TraceEvent e) { }
|
||||
}
|
||||
42
engine/Age.Engine/Diagnostics/TextTraceSink.cs
Normal file
42
engine/Age.Engine/Diagnostics/TextTraceSink.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Age.Engine.Model;
|
||||
namespace Age.Engine.Diagnostics;
|
||||
|
||||
/// <summary>The one built-in formatter: writes each event as a deterministic text line to a
|
||||
/// TextWriter (Console.Out or a file). Indents by frame depth. If an OpcodeTable is supplied, Step
|
||||
/// lines show the mnemonic; otherwise the raw opcode. Step lines only appear when includeSteps is set.</summary>
|
||||
public sealed class TextTraceSink : ITraceSink
|
||||
{
|
||||
private readonly TextWriter _w;
|
||||
private readonly OpcodeTable? _table;
|
||||
private readonly bool _steps;
|
||||
|
||||
public TextTraceSink(TextWriter writer, OpcodeTable? table = null, bool includeSteps = false)
|
||||
{ _w = writer; _table = table; _steps = includeSteps; }
|
||||
|
||||
public bool TracingSteps => _steps;
|
||||
|
||||
public void Emit(in TraceEvent e)
|
||||
{
|
||||
string indent = new string(' ', Math.Max(0, e.Depth - 1) * 2);
|
||||
switch (e.Kind)
|
||||
{
|
||||
case TraceEventKind.FrameEnter:
|
||||
_w.WriteLine($"{indent}» {e.Name} (enter, {e.Cause})"); break;
|
||||
case TraceEventKind.FrameExit:
|
||||
_w.WriteLine($"{indent}« {e.Name} ({e.Text})"); break;
|
||||
case TraceEventKind.Step:
|
||||
_w.WriteLine($"{indent} {e.Pc:x4} {Mnemonic(e.Opcode)} {Args(e.Ins)}"); break;
|
||||
case TraceEventKind.CallScript:
|
||||
_w.WriteLine($"{indent} call-script 0x{e.Id:x} ={e.Name ?? "?"} " +
|
||||
$"({(e.Name != null ? "resolved" : "stub/unresolved")})"); break;
|
||||
case TraceEventKind.Stub:
|
||||
_w.WriteLine($"{indent} {e.Pc:x4} STUB op=0x{e.Opcode:x}"); break;
|
||||
case TraceEventKind.Halt:
|
||||
_w.WriteLine($"halt: {e.Text} @ {e.Steps} steps"); break;
|
||||
}
|
||||
}
|
||||
|
||||
private string Mnemonic(int op) => _table?.Label(op) is { Length: > 0 } l ? l : $"0x{op:x}";
|
||||
private static string Args(Instruction? ins) =>
|
||||
ins == null ? "" : string.Join(" ", ins.Args.Select(o => $"{o.Type}:{o.Value}"));
|
||||
}
|
||||
35
engine/Age.Engine/Diagnostics/TraceEvent.cs
Normal file
35
engine/Age.Engine/Diagnostics/TraceEvent.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Age.Engine.Model;
|
||||
namespace Age.Engine.Diagnostics;
|
||||
|
||||
public enum TraceEventKind { Step, FrameEnter, FrameExit, CallScript, Stub, Halt }
|
||||
public enum FrameCause { TopScene, CallScript }
|
||||
|
||||
/// <summary>An engine diagnostic fact. A <c>readonly struct</c> with a Kind discriminator and a shared
|
||||
/// field set — no per-event heap allocation. Only the fields relevant to a Kind are populated; the
|
||||
/// static factories are the only intended constructors.</summary>
|
||||
public readonly struct TraceEvent
|
||||
{
|
||||
public TraceEventKind Kind { get; private init; }
|
||||
public int Pc { get; private init; }
|
||||
public int Opcode { get; private init; }
|
||||
public int Depth { get; private init; }
|
||||
public long Id { get; private init; } // call-script id
|
||||
public long Steps { get; private init; } // total steps at Halt
|
||||
public FrameCause Cause { get; private init; }
|
||||
public string? Name { get; private init; } // script/scene name; resolved call-script name (null => unresolved/stub)
|
||||
public string? Text { get; private init; } // halt reason; frame outcome
|
||||
public Instruction? Ins { get; private init; } // Step: the instruction (args) by ref, never copied
|
||||
|
||||
public static TraceEvent Step(int pc, Instruction ins, int depth) => new()
|
||||
{ Kind = TraceEventKind.Step, Pc = pc, Opcode = ins.Opcode, Ins = ins, Depth = depth };
|
||||
public static TraceEvent FrameEnter(string name, int depth, FrameCause cause, long id = 0) => new()
|
||||
{ Kind = TraceEventKind.FrameEnter, Name = name, Depth = depth, Cause = cause, Id = id };
|
||||
public static TraceEvent FrameExit(string name, int depth, string outcome) => new()
|
||||
{ Kind = TraceEventKind.FrameExit, Name = name, Depth = depth, Text = outcome };
|
||||
public static TraceEvent CallScript(long id, string? name) => new()
|
||||
{ Kind = TraceEventKind.CallScript, Id = id, Name = name };
|
||||
public static TraceEvent Stub(int opcode, int pc) => new()
|
||||
{ Kind = TraceEventKind.Stub, Opcode = opcode, Pc = pc };
|
||||
public static TraceEvent Halt(string reason, long steps) => new()
|
||||
{ Kind = TraceEventKind.Halt, Text = reason, Steps = steps };
|
||||
}
|
||||
Reference in New Issue
Block a user