An adversarial performance review found our own instruments cannot measure the project's own performance gates: - FrameProfiler aggregated CPU/GPU/alloc/stage samples into ~5-second windows and reset the ring buffers after each report, so route-wide p50/p95/p99 distributions across a whole soak could not be reconstructed after the fact. ACDREAM_FRAME_HISTORY=<path> now opts into a separate per-frame history (one record per frame, ~72 bytes/record, accumulated in memory with zero frame-thread I/O) that a shutdown-only Dispose() writes as CSV. The aggregated [frame-prof] report format and its existing metrics are unchanged. - The canonical checkpoint JSON tracked cache residency (entry/byte counts) but never LOH size/fragmentation, process-wide allocated bytes, or cache hit/miss/eviction traffic — a committed audit JSON showed 65% LOH fragmentation that no tracked instrument recorded, and "does a revisit portal hit or miss the caches" was unanswerable from an artifact alone. WorldLifecycleResourceSnapshot now carries loh_size_bytes/loh_fragmentation_bytes (GCMemoryInfo.GenerationInfo index 3), process_total_allocated_bytes (GC.GetTotalAllocatedBytes), and Interlocked hit/miss/eviction counters for the CPU mesh cache, decoded-texture cache, and the four bounded DAT-object caches (portal/cell/highRes/language, aggregated). - run-connected-r6-soak.ps1 unconditionally forced ACDREAM_UNCAPPED_RENDER=1 with no capped mode, while its sibling lifecycle-gate script correctly gated it behind a switch. Added -Uncapped (default capped, matching the sibling script's pattern), fixed the stationary dwell (12s -> 26s, past the 25s LiveEntityLivenessController deadline the adjacent comment already cited), and now write an env-disclosure.json into the automation artifact directory before every launch listing every ACDREAM_* var the script sets plus -Uncapped, since the prior audit could only see ACDREAM_DUMP_MOVE_TRUTH and nothing else was ever recorded anywhere. Cache counters are wired via the existing composition path (ObjectMeshManager already owns the CPU mesh cache and the mesh extractor directly; content.Dats is threaded into WorldLifecycleResourceSnapshotSource the same way every other composition consumer receives it). The DAT-object cache lives behind IDatReaderWriter, a third-party interface from the DatReaderWriter package that cannot be extended; RuntimeDatCollection (the one production implementation) exposes the aggregate stats directly and a pattern match reads them, degrading to zero for any test double — no new static registry was introduced (GpuMemoryTracker remains the one precedented process-wide static). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 1da2c33c875b41fa383dd79694ee2765f0e21896)
105 lines
3.9 KiB
C#
105 lines
3.9 KiB
C#
using DatReaderWriter;
|
|
using DatReaderWriter.Options;
|
|
using AcDream.Content;
|
|
using DatReaderWriter.Lib.IO;
|
|
using System.Collections.ObjectModel;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
namespace AcDream.App;
|
|
|
|
/// <summary>
|
|
/// Opens the read-only DAT collection owned for the lifetime of an App process.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// DatReaderWriter's default <see cref="FileCachingStrategy.OnDemand"/> retains
|
|
/// every unpacked file entry for the lifetime of the collection. The runtime has
|
|
/// bounded decoded-content caches of its own, so retaining that second,
|
|
/// unbounded copy makes memory depend on every landblock visited. Keep the small
|
|
/// B-tree lookup cache, but read file payloads on demand and let the owning
|
|
/// runtime caches decide their residency.
|
|
/// </remarks>
|
|
internal static class RuntimeDatCollectionFactory
|
|
{
|
|
internal static IDatReaderWriter OpenReadOnly(string datDirectory) =>
|
|
new RuntimeDatCollection(new DatCollection(CreateReadOnlyOptions(datDirectory)));
|
|
|
|
internal static DatCollectionOptions CreateReadOnlyOptions(string datDirectory)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(datDirectory);
|
|
|
|
return new DatCollectionOptions
|
|
{
|
|
DatDirectory = datDirectory,
|
|
AccessType = DatAccessType.Read,
|
|
IndexCachingStrategy = IndexCachingStrategy.OnDemand,
|
|
FileCachingStrategy = FileCachingStrategy.Never,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Owns the runtime's one raw DAT handle set and its one shared bounded typed
|
|
/// object facade. Disposing this owner closes both layers exactly once.
|
|
/// </summary>
|
|
internal sealed class RuntimeDatCollection : IDatReaderWriter
|
|
{
|
|
private readonly DatCollection _raw;
|
|
private readonly DatCollectionAdapter _bounded;
|
|
private bool _disposed;
|
|
|
|
internal RuntimeDatCollection(DatCollection raw)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(raw);
|
|
_raw = raw;
|
|
_bounded = new DatCollectionAdapter(raw);
|
|
}
|
|
|
|
public string SourceDirectory => _bounded.SourceDirectory;
|
|
|
|
/// <summary>Aggregate DAT-object cache hit/miss/eviction counts (2026-07-24 measurement-tooling review).</summary>
|
|
internal CacheStats ObjectCacheStats => _bounded.ObjectCacheStats;
|
|
|
|
public IDatDatabase Portal => _bounded.Portal;
|
|
public IDatDatabase Cell => _bounded.Cell;
|
|
public ReadOnlyDictionary<uint, IDatDatabase> CellRegions => _bounded.CellRegions;
|
|
public IDatDatabase HighRes => _bounded.HighRes;
|
|
public IDatDatabase Language => _bounded.Language;
|
|
public IDatDatabase Local => _bounded.Local;
|
|
public ReadOnlyDictionary<uint, uint> RegionFileMap => _bounded.RegionFileMap;
|
|
public int PortalIteration => _bounded.PortalIteration;
|
|
public int CellIteration => _bounded.CellIteration;
|
|
public int HighResIteration => _bounded.HighResIteration;
|
|
public int LanguageIteration => _bounded.LanguageIteration;
|
|
|
|
[return: MaybeNull]
|
|
public T Get<T>(uint fileId) where T : IDBObj => _bounded.Get<T>(fileId);
|
|
|
|
public bool TryGet<T>(uint fileId, [MaybeNullWhen(false)] out T value)
|
|
where T : IDBObj =>
|
|
_bounded.TryGet(fileId, out value);
|
|
|
|
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
|
|
_bounded.GetAllIdsOfType<T>();
|
|
|
|
public bool TryGetFileBytes(uint regionId, uint fileId, ref byte[] bytes, out int bytesRead) =>
|
|
_bounded.TryGetFileBytes(regionId, fileId, ref bytes, out bytesRead);
|
|
|
|
public IEnumerable<IDatReaderWriter.IdResolution> ResolveId(uint id) =>
|
|
_bounded.ResolveId(id);
|
|
|
|
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
|
_bounded.TrySave(obj, iteration);
|
|
|
|
public bool TrySave<T>(uint regionId, T obj, int iteration = 0) where T : IDBObj =>
|
|
_bounded.TrySave(regionId, obj, iteration);
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
_disposed = true;
|
|
_bounded.Dispose();
|
|
_raw.Dispose();
|
|
}
|
|
}
|