fix(pathfinding): stop opening every .swb twice at boot (#2548)
## Problem
Every map's `.swb` step cache was opened, indexed and logged **twice** on boot.
`MovementPath.Configure()` explicitly called `PathCacheCommands.Configure()` and `CacheEvictionTimer.Configure()`. Both are types exposing a public static parameterless `Configure()`, which `AssemblyHandler.Invoke("Configure")` already discovers and calls once each (`AssemblyHandler.cs:157`). So `PathCacheCommands.Configure()` ran twice, and `AutoLoadAtStartup()` with it. `PathCacheCommands.Configure()` called `PathfindRecorder.Configure()` the same way.
`TryOpenLazyReader` disposes the prior reader before replacing it, so there was no handle leak — but the header and full chunk index of each `.swb` were read twice (~48 MB of files across six facets). The expensive `.mul` hashing was already memoized, so it was not doubled.
## Fix
Consolidate the cache lifecycle into `Initialize`:
- `Configure()` keeps only settings and command registration.
- `Initialize()` opens the readers once, then prebakes only maps that still lack one.
- The post-bake reopen is per-map instead of a blanket `AutoLoadAtStartup()` — on a partial bake (some valid `.swb`, one stale) that would close and reopen the readers already open, a second double-open on a different path.
`Initialize` is the correct phase. `Configure` runs before `TileMatrixLoader.LoadTileMatrix()` and `World.Load()` (`Main.cs:458/460/463/465`), so opening a `.swb` there forced the lazy `Map.Tiles` property — the fingerprint hashes the map files — and built every `TileMatrix` ahead of the loader that owns it, possibly before `TileMatrix.Configure()` settled `Pre6000ClientSupport`. Both sit at the default call priority and the phase sort is unstable. Moving pathfinding out leaves nothing in `Configure` that touches `Map.Tiles`, closing that hazard; the other 22 `.Tiles` users in UOContent are all runtime paths.
Multis stay out of the bake by design — houses and boats are player data that moves, handled by the multi-aware path at query time.
## Logging
The per-map `StepCache: opened ... chunks indexed` line drops to `Debug`. Opening is the expected case; `BakeMap` already logs a rebuild at `Information`, and `Initialize` still emits `PathBake: pre-bake complete (N map(s) written)`.
## Verification
- `dotnet build Projects/UOContent` — 0 errors, 0 warnings.
- `dotnet test --filter FullyQualifiedName~Pathfinding` — **123 passed, 0 failed**.
Boot logs should now show one `opened` line per map at `Debug`, none at `Information`.
This commit is contained in:
parent
c39454137e
commit
9c11ccdb80
4 changed files with 28 additions and 21 deletions
|
|
@ -285,7 +285,8 @@ public sealed class StepCache
|
||||||
}
|
}
|
||||||
_lazyReaders[mapId] = reader;
|
_lazyReaders[mapId] = reader;
|
||||||
|
|
||||||
logger.Information(
|
// Debug: opening is the expected case. A rebuild is the interesting one, and BakeMap logs it.
|
||||||
|
logger.Debug(
|
||||||
"StepCache: opened {Path} ({ChunkCount} chunks indexed) for map {MapId}",
|
"StepCache: opened {Path} ({ChunkCount} chunks indexed) for map {MapId}",
|
||||||
path, reader.IndexedChunkCount, mapId
|
path, reader.IndexedChunkCount, mapId
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,6 @@ public sealed class MovementPath
|
||||||
public static void Configure()
|
public static void Configure()
|
||||||
{
|
{
|
||||||
CommandSystem.Register("Path", AccessLevel.GameMaster, Path_OnCommand);
|
CommandSystem.Register("Path", AccessLevel.GameMaster, Path_OnCommand);
|
||||||
CacheEvictionTimer.Configure();
|
|
||||||
PathCacheCommands.Configure();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Usage("Path")]
|
[Usage("Path")]
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,12 @@ public static class PathCacheCommands
|
||||||
8192
|
8192
|
||||||
);
|
);
|
||||||
|
|
||||||
PathfindRecorder.Configure();
|
|
||||||
|
|
||||||
CommandSystem.Register("PathCacheStats", AccessLevel.Administrator, OnPathCacheStats);
|
CommandSystem.Register("PathCacheStats", AccessLevel.Administrator, OnPathCacheStats);
|
||||||
CommandSystem.Register("PathCacheClear", AccessLevel.Administrator, OnPathCacheClear);
|
CommandSystem.Register("PathCacheClear", AccessLevel.Administrator, OnPathCacheClear);
|
||||||
CommandSystem.Register("PathBake", AccessLevel.Administrator, OnPathBake);
|
CommandSystem.Register("PathBake", AccessLevel.Administrator, OnPathBake);
|
||||||
CommandSystem.Register("PathCacheSave", AccessLevel.Administrator, OnPathCacheSave);
|
CommandSystem.Register("PathCacheSave", AccessLevel.Administrator, OnPathCacheSave);
|
||||||
CommandSystem.Register("PathCacheLoad", AccessLevel.Administrator, OnPathCacheLoad);
|
CommandSystem.Register("PathCacheLoad", AccessLevel.Administrator, OnPathCacheLoad);
|
||||||
CommandSystem.Register("PathRecord", AccessLevel.Administrator, OnPathRecord);
|
CommandSystem.Register("PathRecord", AccessLevel.Administrator, OnPathRecord);
|
||||||
AutoLoadAtStartup();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -80,18 +77,23 @@ public static class PathCacheCommands
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bakes any map whose <c>.swb</c> is missing or stale, when <see cref="PrebakeSetting"/> is
|
/// Opens the existing <c>.swb</c> files, then — when <see cref="PrebakeSetting"/> is set —
|
||||||
/// set. Runs in the Initialize phase, once the tile matrix and world are loaded. An up-to-date
|
/// bakes any that are missing or stale. An up-to-date cache makes the bake a no-op, so the cost
|
||||||
/// cache makes it a no-op, so the cost lands only on a first boot or after a client or map
|
/// lands only on a first boot or after a client or map update moves the fingerprint.
|
||||||
/// update moves the fingerprint.
|
|
||||||
///
|
///
|
||||||
/// A map is judged up-to-date by whether it has an open reader. <see cref="AutoLoadAtStartup"/>
|
/// Both halves run here rather than in Configure: the fingerprint hashes the map files, so
|
||||||
/// already ran in the earlier Configure phase and only opens a reader for a .swb whose
|
/// opening a .swb forces the lazy <see cref="Map.Tiles"/> property. In Configure that would
|
||||||
/// fingerprint validates, so an open reader is proof of a good bake — no need to fingerprint
|
/// build every TileMatrix ahead of <c>TileMatrixLoader</c>, possibly before
|
||||||
/// the map a second time here.
|
/// <c>TileMatrix.Configure()</c> settles <c>Pre6000ClientSupport</c> — both sit at the default
|
||||||
|
/// call priority and the phase sort is unstable.
|
||||||
|
///
|
||||||
|
/// A reader only opens once its fingerprint validates, so an open reader is proof of a good
|
||||||
|
/// bake and the map is skipped without fingerprinting it again.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Initialize()
|
public static void Initialize()
|
||||||
{
|
{
|
||||||
|
AutoLoadAtStartup();
|
||||||
|
|
||||||
if (!ServerConfiguration.GetSetting(PrebakeSetting, false))
|
if (!ServerConfiguration.GetSetting(PrebakeSetting, false))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
@ -119,13 +121,15 @@ public static class PathCacheCommands
|
||||||
);
|
);
|
||||||
StepCache.Instance.BakeMap(map.MapID, path);
|
StepCache.Instance.BakeMap(map.MapID, path);
|
||||||
StepCache.Instance.ClearResidentChunks();
|
StepCache.Instance.ClearResidentChunks();
|
||||||
|
|
||||||
|
// Just this map: a blanket AutoLoadAtStartup() would reopen every reader already open.
|
||||||
|
StepCache.Instance.TryOpenLazyReader(path, map.MapID);
|
||||||
baked++;
|
baked++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baked > 0)
|
if (baked > 0)
|
||||||
{
|
{
|
||||||
logger.Information("PathBake: pre-bake complete ({Count} map(s) written).", baked);
|
logger.Information("PathBake: pre-bake complete ({Count} map(s) written).", baked);
|
||||||
AutoLoadAtStartup(); // reopen what we just wrote
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,13 +139,17 @@ several-minutes cost. Wiring:
|
||||||
after assemblies load (so content can register prompts) but **before Serilog starts**, so the
|
after assemblies load (so content can register prompts) but **before Serilog starts**, so the
|
||||||
console prompt is not interleaved with the async console sink. Any class can participate by
|
console prompt is not interleaved with the async console sink. Any class can participate by
|
||||||
defining `public static void ConfigurePrompts()` and self-gating on first-boot state.
|
defining `public static void ConfigurePrompts()` and self-gating on first-boot state.
|
||||||
- The bake runs in the later `Invoke("Initialize")` phase (after the tile matrix + world load,
|
- Both the reader open and the bake run in `Invoke("Initialize")`, after the tile matrix and world
|
||||||
which the bake walks).
|
load. Neither belongs in `Configure`, which runs *before* both: the fingerprint hashes the map
|
||||||
|
files, so opening a `.swb` there would force the lazy `Map.Tiles` property and build every
|
||||||
|
`TileMatrix` ahead of `TileMatrixLoader` — possibly before `TileMatrix.Configure()` settles
|
||||||
|
`Pre6000ClientSupport`, since both sit at the default call priority and the phase sort is
|
||||||
|
unstable. `PathCacheCommands.Configure` is limited to settings and command registration.
|
||||||
- Staleness is decided by the `.swb` fingerprint, which `StepCacheFile.OpenForLazy` validates at
|
- Staleness is decided by the `.swb` fingerprint, which `StepCacheFile.OpenForLazy` validates at
|
||||||
open time (hash of `tiledata.mul` + the per-map `.mul`/`.uop` files — never the in-memory
|
open time (hash of `tiledata.mul` + the per-map `.mul`/`.uop` files — never the in-memory
|
||||||
`TileData` tables, which the server patches at runtime). `Configure` opens a reader for every
|
`TileData` tables, which the server patches at runtime). `Initialize` opens a reader for every
|
||||||
up-to-date file; the bake in `Initialize` then skips any map where `StepCache.HasLazyReader` is
|
up-to-date file, then skips any map where `StepCache.HasLazyReader` is already true, so the
|
||||||
already true, so the fingerprint is computed once per boot, not twice.
|
fingerprint is computed once per boot. Each newly baked map reopens only itself.
|
||||||
|
|
||||||
## Configuration levers
|
## Configuration levers
|
||||||
|
|
||||||
|
|
@ -154,7 +158,7 @@ several-minutes cost. Wiring:
|
||||||
| `pathfinding.enable` | `PathFollower.Configure` | `true` | Master switch for `PathFollower` pathfinding. Off → greedy/auto-turn only, no A* at all. |
|
| `pathfinding.enable` | `PathFollower.Configure` | `true` | Master switch for `PathFollower` pathfinding. Off → greedy/auto-turn only, no A* at all. |
|
||||||
| `bitmap_pathfinding_cache` feature flag (`ContentFeatureFlags.BitmapPathfindingCache`, `Server.Systems.FeatureFlags`) | `FeatureFlagManager` | `true` | Off → `BitmapAStar` routes straight to the slow path with **no cache probe and no warming memory**. ≈ old FastAStar at ~1×. |
|
| `bitmap_pathfinding_cache` feature flag (`ContentFeatureFlags.BitmapPathfindingCache`, `Server.Systems.FeatureFlags`) | `FeatureFlagManager` | `true` | Off → `BitmapAStar` routes straight to the slow path with **no cache probe and no warming memory**. ≈ old FastAStar at ~1×. |
|
||||||
| `pathfinding.maxResidentChunks` | `PathCacheCommands.Configure` | 8192 (~40 MB) | LRU cap on resident chunks = the warming-memory ceiling. Lower it (e.g. 512–1024 ≈ 2.5–5 MB) on small shards. |
|
| `pathfinding.maxResidentChunks` | `PathCacheCommands.Configure` | 8192 (~40 MB) | LRU cap on resident chunks = the warming-memory ceiling. Lower it (e.g. 512–1024 ≈ 2.5–5 MB) on small shards. |
|
||||||
| `pathfinding.maxSearchNodes` | `PathCacheCommands.Configure` → `BitmapAStarAlgorithm.MaxSearchNodes` | 1000 | A* per-Find node-expansion budget. See limits above; ~1000 is the sweet spot. |
|
| `pathfinding.maxSearchNodes` | `BitmapAStarAlgorithm.Configure` → `BitmapAStarAlgorithm.MaxSearchNodes` | 1000 | A* per-Find node-expansion budget. See limits above; ~1000 is the sweet spot. |
|
||||||
| `pathfinding.prebakeMaps` | `PathCacheCommands` (first-boot prompt + `Initialize`) | `false` | When set, bakes any missing/stale `.swb` for the selected maps at startup (fingerprint-gated, so a fresh cache is a no-op). Set interactively by the first-boot prompt. |
|
| `pathfinding.prebakeMaps` | `PathCacheCommands` (first-boot prompt + `Initialize`) | `false` | When set, bakes any missing/stale `.swb` for the selected maps at startup (fingerprint-gated, so a fresh cache is a no-op). Set interactively by the first-boot prompt. |
|
||||||
| `PathFollower` `RepathDelay` | `PathFollower.cs` (const) | 2 s | Throttle: a moving goal re-`Find`s at most ~once per 2 s; a stationary reachable goal is pathed once and reused until arrival. Not a setting (compile-time). |
|
| `PathFollower` `RepathDelay` | `PathFollower.cs` (const) | 2 s | Throttle: a moving goal re-`Find`s at most ~once per 2 s; a stationary reachable goal is pathed once and reused until arrival. Not a setting (compile-time). |
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue