perf(content): close prepared asset cutover

This commit is contained in:
Erik 2026-07-24 15:49:24 +02:00
parent b1ad4b7c0a
commit a564c4b782
11 changed files with 428 additions and 66 deletions

View file

@ -5,14 +5,15 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
namespace AcDream.Content;
/// <summary>
/// THE <see cref="DatCollection"/> → <see cref="IDatReaderWriter"/> adapter,
/// shared by all three consumers: ObjectMeshManager (AcDream.App, via
/// WbMeshAdapter), acdream-bake (AcDream.Bake.BakeRunner), and the pak
/// equivalence suite (AcDream.Content.Tests). MP1b's review found the
/// shared by runtime DAT-backed gameplay/content access,
/// acdream-bake (AcDream.Bake.BakeRunner), and the pak equivalence suite
/// (AcDream.Content.Tests). MP1b's review found the
/// original App-internal copy had silently forked into three near-identical
/// versions — drift between them is exactly what the live-vs-pak equivalence
/// suite CANNOT detect (both sides would drift together only if they share
@ -218,6 +219,7 @@ public sealed class DatDatabaseWrapper : IDatDatabase {
// cache itself documents why estimated bytes are not a hard heap bound.
private readonly BoundedDatObjectCache _cache = new();
private readonly object _databaseLock = new();
private int _malformedRecordDiagnosticEmitted;
public DatDatabaseWrapper(DatDatabase db) {
ArgumentNullException.ThrowIfNull(db);
@ -241,6 +243,7 @@ public sealed class DatDatabaseWrapper : IDatDatabase {
return true;
}
bool existingRecordFailed;
lock (_databaseLock) {
// A different reader may have populated the cache while this
// caller waited for the serialized DatDatabase read path.
@ -253,17 +256,25 @@ public sealed class DatDatabaseWrapper : IDatDatabase {
return true;
}
// TEMP diagnostic (dat-race investigation 2026-06-09, strip with fix):
// a miss for an id whose BTree entry EXISTS is always an anomaly —
// either Unpack returned false or the lookup flickered transiently.
// Legit not-found probes (e.g. Portal→HighRes fallback) stay silent.
// Kept through the MP1b unification — the tripwire now covers the
// bake tool and the equivalence suite too, not just the client.
if (_db.Tree.TryGetFile(fileId, out _)) {
Console.WriteLine(
$"[dat-miss] {typeof(T).Name} 0x{fileId:X8} entry EXISTS but TryGet failed " +
$"(thread={Environment.CurrentManagedThreadId})");
}
// A miss for an id whose BTree entry exists is a typed-access
// anomaly (wrong requested type, malformed content, or an unpacker
// failure). Capture only the fact while serialized DAT access is
// held; diagnostics must never perform console I/O under this
// shared lock.
existingRecordFailed = _db.Tree.TryGetFile(fileId, out _);
}
// One actionable report per database keeps a repeatedly requested
// malformed record from becoming another exception/logging-style hot
// path. Missing keys stay silent.
if (existingRecordFailed
&& Interlocked.Exchange(
ref _malformedRecordDiagnosticEmitted,
1) == 0)
{
Console.Error.WriteLine(
$"[dat-miss] {typeof(T).Name} 0x{fileId:X8} entry EXISTS but TryGet failed " +
$"(thread={Environment.CurrentManagedThreadId}; further reports suppressed)");
}
return false;