## Summary Adds two pieces of pathfinding tooling on top of PR #2448's lazy `.swb` infrastructure: - **`PathfindRecorder`** — admin-toggled JSONL telemetry capture; one record per `BitmapAStarAlgorithm.Find` call. Output format matches the BDN harness corpus, so production traffic can be captured and replayed in benchmarks without an adapter. - **Public bake helpers on `StepCache`** — `ComputeLiveTileDataHash`, `TryReadTileDataHashFromFile`, `BakeMap`, `ClearResidentChunks`. Lets the benchmark project (and any future bake utility) drive cache fill + persist without exposing internal types. The companion BDN harness update lives in [ModernUO-Benchmarks#kb/pathfinding-pr4-bench](https://github.com/modernuo/ModernUO-Benchmarks/tree/kb/pathfinding-pr4-bench): porting `Benchmarks/PathfindInGame/` from the `kb/ai_pathfinding` branch to the API shipped in #2446–#2448. ## What's in this PR ### `PathfindRecorder` (`PathfindRecorder.cs`) - Holds a single `StreamWriter` open while recording; its internal buffer absorbs per-record writes without per-call `File.AppendAllText`. - Single `bool` check on the hot path; cheap when disabled. - Disabling flushes + disposes; an IO failure during write also disables the recorder. - Server config: - `pathfinding.recorder.enable` — bool, default `false`. Read on boot via `GetOrUpdateSetting`. - `pathfinding.recorder.path` — default `<basedir>/Data/Pathfinding/recordings/pathfinds.jsonl`. - Hooked into `BitmapAStarAlgorithm.Find` — runs once per call, does nothing when disabled. - Admin command: `[PathRecord [on|off|flush|status]` (default `status`). ### Public cache helpers - `static ulong StepCache.ComputeLiveTileDataHash()` — wraps the file module's hash function for staleness checks. - `static bool StepCache.TryReadTileDataHashFromFile(string, out ulong)` — peeks at a `.swb` file's hash field (20 bytes). - `int StepCache.BakeMap(int, string)` — walks every chunk in the map, populates resident set, saves. Offline / fixture use; blocks for many seconds on a full-map walk. - `void StepCache.ClearResidentChunks()` — drops chunks + zeros counters but keeps lazy readers open. Lets benchmark loops measure "first query after boot" cost across iterations without the lazy-reader reopen overhead.
170 lines
6.3 KiB
C#
170 lines
6.3 KiB
C#
using System.IO;
|
|
using System.Text;
|
|
using Server.Logging;
|
|
using Server.Mobiles;
|
|
using Server.Text;
|
|
|
|
namespace Server.Engines.Pathing;
|
|
|
|
/// <summary>
|
|
/// Admin-toggled telemetry: appends a JSONL line per pathfind request to a file.
|
|
/// One record per BitmapAStarAlgorithm.Find call, capturing the inputs (start, goal,
|
|
/// map, capability flags) needed to replay the scenario in benchmarks. Output format
|
|
/// matches the corpus the BDN harness consumes.
|
|
///
|
|
/// Hot-toggleable at runtime via the [PathRecord admin command — no restart needed.
|
|
/// <see cref="Configure"/> only seeds the initial state from server.cfg
|
|
/// (pathfinding.recorder.enable, default false).
|
|
///
|
|
/// Holds a single StreamWriter open while recording; its internal buffer absorbs
|
|
/// per-record writes without per-call File.Open / File.Append. Each record is built
|
|
/// in a stack-allocated ValueStringBuilder (zero per-int allocation for the field
|
|
/// formatting), then handed to the writer as a ReadOnlySpan<char>.
|
|
///
|
|
/// <b>Workload note:</b> intended for short bursts of capture (turn on, walk a region
|
|
/// or trigger a scenario, turn off). On a busy server with hundreds of pathfinds
|
|
/// per second, sustained recording can saturate the StreamWriter's 4 KB buffer and
|
|
/// block the game thread on disk writes. A backpressure-aware async sink is a
|
|
/// future enhancement if 24/7 capture becomes a use case.
|
|
/// </summary>
|
|
public static class PathfindRecorder
|
|
{
|
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(PathfindRecorder));
|
|
|
|
private static bool _enabled;
|
|
private static string _outputPath;
|
|
private static StreamWriter _writer;
|
|
private static long _recordsWritten;
|
|
|
|
public static bool Enabled => _enabled;
|
|
public static string OutputPath => _outputPath;
|
|
public static long RecordsWritten => _recordsWritten;
|
|
|
|
public static void Configure()
|
|
{
|
|
_outputPath = ServerConfiguration.GetOrUpdateSetting(
|
|
"pathfinding.recorder.path",
|
|
Path.Combine(Core.BaseDirectory, "Data", "Pathfinding", "recordings", "pathfinds.jsonl")
|
|
);
|
|
|
|
var startEnabled = ServerConfiguration.GetOrUpdateSetting("pathfinding.recorder.enable", false);
|
|
if (startEnabled)
|
|
{
|
|
SetEnabled(true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toggle recording. When enabling, opens an append-mode StreamWriter; when
|
|
/// disabling, flushes + disposes it. Idempotent — calling twice with the same
|
|
/// state is a no-op.
|
|
/// </summary>
|
|
public static void SetEnabled(bool enabled)
|
|
{
|
|
if (enabled == _enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (enabled)
|
|
{
|
|
try
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(_outputPath) ?? ".");
|
|
var stream = new FileStream(_outputPath, FileMode.Append, FileAccess.Write, FileShare.Read);
|
|
_writer = new StreamWriter(stream, new UTF8Encoding(false));
|
|
_enabled = true;
|
|
logger.Information("PathfindRecorder enabled, writing to {Path}", _outputPath);
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
logger.Warning(ex, "PathfindRecorder: failed to open {Path} for write", _outputPath);
|
|
_writer = null;
|
|
_enabled = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_enabled = false;
|
|
try
|
|
{
|
|
_writer?.Flush();
|
|
_writer?.Dispose();
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
logger.Warning(ex, "PathfindRecorder: error closing {Path}", _outputPath);
|
|
}
|
|
_writer = null;
|
|
logger.Information("PathfindRecorder disabled ({Count} records this session)", _recordsWritten);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Force a flush of the writer's internal buffer to disk. Safe to call when
|
|
/// disabled (no-op). Useful after a burst of recording when an admin wants to
|
|
/// inspect the file without waiting for buffer fill or disable.
|
|
/// </summary>
|
|
public static void Flush()
|
|
{
|
|
try
|
|
{
|
|
_writer?.Flush();
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
logger.Warning(ex, "PathfindRecorder: flush failed for {Path}", _outputPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Capture one Find call. Hot path: cheap when disabled (single bool check).
|
|
/// When enabled, formats one JSONL line and writes it through the StreamWriter's
|
|
/// internal buffer — flush is amortized across many calls.
|
|
/// </summary>
|
|
public static void RecordIfEnabled(Mobile m, Map map, Point3D start, Point3D goal)
|
|
{
|
|
if (!_enabled || _writer == null || m == null || map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var canSwim = false;
|
|
var canFly = false;
|
|
var canOpenDoors = false;
|
|
var canMoveOverObstacles = false;
|
|
if (m is BaseCreature bc)
|
|
{
|
|
canSwim = bc.CanSwim;
|
|
canFly = bc.CanFly;
|
|
canOpenDoors = bc.CanOpenDoors;
|
|
canMoveOverObstacles = bc.CanMoveOverObstacles;
|
|
}
|
|
|
|
try
|
|
{
|
|
// One interpolation handles every numeric field with no per-int ToString
|
|
// allocation; bool fields use explicit literal spans because JSON wants
|
|
// lowercase "true"/"false" and bool.ToString() yields "True"/"False".
|
|
using var vsb = ValueStringBuilder.Create(192);
|
|
vsb.Append(
|
|
$"{{\"Name\":\"recorded\",\"MapId\":{map.MapID},\"StartX\":{start.X},\"StartY\":{start.Y},\"StartZ\":{start.Z},\"GoalX\":{goal.X},\"GoalY\":{goal.Y},\"GoalZ\":{goal.Z},\"CanSwim\":"
|
|
);
|
|
vsb.Append(canSwim ? "true" : "false");
|
|
vsb.Append(",\"CanFly\":");
|
|
vsb.Append(canFly ? "true" : "false");
|
|
vsb.Append(",\"CanOpenDoors\":");
|
|
vsb.Append(canOpenDoors ? "true" : "false");
|
|
vsb.Append(",\"CanMoveOverObstacles\":");
|
|
vsb.Append(canMoveOverObstacles ? "true" : "false");
|
|
vsb.Append("}\n");
|
|
_writer.Write(vsb.AsSpan());
|
|
_recordsWritten++;
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
logger.Warning(ex, "PathfindRecorder: write failed, disabling");
|
|
SetEnabled(false);
|
|
}
|
|
}
|
|
}
|