feat(diagnostics): complete residency pressure ledger

Complete Slice D4 by adding aggregate lifecycle occupancy and traffic facts, validating physical source reports, and including decoded audio under the typed startup budget. Exercise every domain under forced pressure and retain the real cache/fence convergence gates.
This commit is contained in:
Erik 2026-07-24 16:40:17 +02:00
parent f2644d42c2
commit 1853a57c12
15 changed files with 297 additions and 15 deletions

View file

@ -1,6 +1,6 @@
# Modern Runtime Slice D — Typed Asset Handles and Unified Residency # Modern Runtime Slice D — Typed Asset Handles and Unified Residency
**Status:** active — D1 through D3 complete **Status:** active — D1 through D4 complete
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md` **Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
**Baseline:** Slice C closeout commit `a564c4b7` **Baseline:** Slice C closeout commit `a564c4b7`
**Behavior contract:** no visual-quality or retail-behavior change **Behavior contract:** no visual-quality or retail-behavior change
@ -173,6 +173,7 @@ contains byte/count ceilings with the current production values as defaults:
- composite arrays: 128 MiB physical / 64 MiB unowned; - composite arrays: 128 MiB physical / 64 MiB unowned;
- standalone particle arrays: 32 MiB / 256; - standalone particle arrays: 32 MiB / 256;
- decoded animations: 64 MiB / 512; - decoded animations: 64 MiB / 512;
- decoded audio: 32 MiB;
- deferred-alpha retained CPU scratch: 16 MiB aggregate with per-owner floors. - deferred-alpha retained CPU scratch: 16 MiB aggregate with per-owner floors.
Environment overrides are diagnostic/startup inputs and reject zero, negative, Environment overrides are diagnostic/startup inputs and reject zero, negative,
@ -248,6 +249,19 @@ current source, so no replacement cache was invented.
drain fence release, and prove convergence. drain fence release, and prove convergence.
- Assert manager and physical-owner accounting return to zero at teardown. - Assert manager and physical-owner accounting return to zero at teardown.
**Complete 2026-07-24.** The lifecycle artifact now serializes validated
per-domain rows plus aggregate entry/owner, budget, allocator
capacity/usage/fragmentation, traffic, committed CPU, mapped-address-space, and
physical GPU totals. The final omitted bounded owner, decoded audio, now
receives its startup budget through `RuntimeOptions` and reports decoded bytes
and hit/miss/eviction traffic beside animations and the render caches. A
deterministic all-domain pressure matrix exceeds every reported ceiling and
proves zero-charge convergence. The focused physical-owner suites separately
exercise prepared-mesh/staging rejection, composite and standalone texture
eviction, fence-delayed release, global-arena range retirement, animation/audio
LRU pressure, and alpha-scratch convergence. Release build and the complete
8,111-test / 5-skip solution gate are green.
### D5 — Connected gate ### D5 — Connected gate
- Run capped, uncapped, and dense connected routes against Slice C. - Run capped, uncapped, and dense connected routes against Slice C.

View file

@ -113,7 +113,9 @@ internal interface IContentEffectsAudioCompositionFactory
EntityEffectPoseRegistry poses); EntityEffectPoseRegistry poses);
TranslucencyHookSink CreateTranslucencySink(TranslucencyFadeManager fades); TranslucencyHookSink CreateTranslucencySink(TranslucencyFadeManager fades);
AnimationHookRegistrationSet CreateHookRegistrations(AnimationHookRouter router); AnimationHookRegistrationSet CreateHookRegistrations(AnimationHookRouter router);
DatSoundCache CreateSoundCache(IDatReaderWriter dats); DatSoundCache CreateSoundCache(
IDatReaderWriter dats,
long maximumDecodedBytes);
OpenAlAudioEngine CreateAudioEngine(); OpenAlAudioEngine CreateAudioEngine();
DictionaryEntitySoundTable CreateEntitySoundTables(); DictionaryEntitySoundTable CreateEntitySoundTables();
AudioHookSink CreateAudioSink( AudioHookSink CreateAudioSink(
@ -204,7 +206,10 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
AnimationHookRouter router) => AnimationHookRouter router) =>
new(router); new(router);
public DatSoundCache CreateSoundCache(IDatReaderWriter dats) => new(dats); public DatSoundCache CreateSoundCache(
IDatReaderWriter dats,
long maximumDecodedBytes) =>
new(dats, maximumDecodedBytes);
public OpenAlAudioEngine CreateAudioEngine() => new(); public OpenAlAudioEngine CreateAudioEngine() => new();
@ -426,7 +431,9 @@ internal sealed class ContentEffectsAudioCompositionPhase :
engineLease; engineLease;
try try
{ {
DatSoundCache cache = _factory.CreateSoundCache(dats); DatSoundCache cache = _factory.CreateSoundCache(
dats,
_dependencies.ResidencyBudgets.AudioBytes);
Fault(ContentEffectsAudioCompositionPoint.SoundCacheCreated); Fault(ContentEffectsAudioCompositionPoint.SoundCacheCreated);
engineLease = audioScope.Acquire( engineLease = audioScope.Acquire(
"OpenAL audio engine", "OpenAL audio engine",

View file

@ -5,6 +5,7 @@ using AcDream.App.Rendering.Residency;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Content; using AcDream.Content;
using AcDream.Content.Vfx; using AcDream.Content.Vfx;
using AcDream.Core.Audio;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Core.Terrain; using AcDream.Core.Terrain;
using AcDream.UI.Abstractions.Settings; using AcDream.UI.Abstractions.Settings;
@ -119,7 +120,8 @@ internal interface IWorldRenderCompositionFactory
WbMeshAdapter meshes, WbMeshAdapter meshes,
TextureCache textures, TextureCache textures,
IPreparedAssetSource preparedAssets, IPreparedAssetSource preparedAssets,
IAnimationLoader animations); IAnimationLoader animations,
DatSoundCache? audio);
SamplerCache CreateSamplerCache(GL gl); SamplerCache CreateSamplerCache(GL gl);
void Release(IDisposable resource); void Release(IDisposable resource);
} }
@ -299,7 +301,8 @@ internal sealed class RetailWorldRenderCompositionFactory
WbMeshAdapter meshes, WbMeshAdapter meshes,
TextureCache textures, TextureCache textures,
IPreparedAssetSource preparedAssets, IPreparedAssetSource preparedAssets,
IAnimationLoader animations) IAnimationLoader animations,
DatSoundCache? audio)
{ {
meshes.RegisterResidencySources(manager); meshes.RegisterResidencySources(manager);
textures.RegisterResidencySources(manager); textures.RegisterResidencySources(manager);
@ -328,9 +331,30 @@ internal sealed class RetailWorldRenderCompositionFactory
DecodedBytes: DecodedBytes:
diagnostics.EstimatedBytes), diagnostics.EstimatedBytes),
BudgetBytes: diagnostics.BudgetBytes, BudgetBytes: diagnostics.BudgetBytes,
Hits: diagnostics.Stats.Hits,
Misses: diagnostics.Stats.Misses,
Evictions: diagnostics.Stats.Evictions); Evictions: diagnostics.Stats.Evictions);
})); }));
} }
if (audio is not null)
{
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
ResidencyDomain.Audio,
() =>
{
DatSoundCacheDiagnostics diagnostics = audio.Diagnostics;
return new ResidencyDomainSnapshot(
ResidencyDomain.Audio,
EntryCount: diagnostics.CachedWaveCount,
OwnerCount: 0,
Charges: new ResidencyCharges(
DecodedBytes: diagnostics.ResidentWaveBytes),
BudgetBytes: diagnostics.BudgetBytes,
Hits: diagnostics.Hits,
Misses: diagnostics.Misses,
Evictions: diagnostics.Evictions);
}));
}
} }
public SamplerCache CreateSamplerCache(GL gl) => new(gl); public SamplerCache CreateSamplerCache(GL gl) => new(gl);
@ -517,7 +541,8 @@ internal sealed class WorldRenderCompositionPhase
meshAdapter, meshAdapter,
textureCache, textureCache,
content.PreparedAssets, content.PreparedAssets,
content.AnimationLoader); content.AnimationLoader,
content.Audio?.SoundCache);
scope.Complete(); scope.Complete();
_dependencies.Log( _dependencies.Log(

View file

@ -164,12 +164,41 @@ internal readonly record struct ResidencyDomainSnapshot(
long CapacityBytes = 0, long CapacityBytes = 0,
long UsedBytes = 0, long UsedBytes = 0,
long LargestFreeBytes = 0, long LargestFreeBytes = 0,
long Hits = 0,
long Misses = 0,
long Evictions = 0) long Evictions = 0)
{ {
public long FreeBytes => Math.Max(0, CapacityBytes - UsedBytes); public long FreeBytes => Math.Max(0, CapacityBytes - UsedBytes);
public long FragmentedFreeBytes => public long FragmentedFreeBytes =>
Math.Max(0, FreeBytes - LargestFreeBytes); Math.Max(0, FreeBytes - LargestFreeBytes);
public void Validate()
{
if (!Enum.IsDefined(Domain))
throw new ArgumentOutOfRangeException(nameof(Domain));
ArgumentOutOfRangeException.ThrowIfNegative(EntryCount);
ArgumentOutOfRangeException.ThrowIfNegative(OwnerCount);
Charges.Validate();
ArgumentOutOfRangeException.ThrowIfNegative(BudgetBytes);
ArgumentOutOfRangeException.ThrowIfNegative(CapacityBytes);
ArgumentOutOfRangeException.ThrowIfNegative(UsedBytes);
ArgumentOutOfRangeException.ThrowIfNegative(LargestFreeBytes);
ArgumentOutOfRangeException.ThrowIfNegative(Hits);
ArgumentOutOfRangeException.ThrowIfNegative(Misses);
ArgumentOutOfRangeException.ThrowIfNegative(Evictions);
if (UsedBytes > CapacityBytes && CapacityBytes != 0)
{
throw new InvalidOperationException(
$"{Domain} residency uses {UsedBytes} bytes from "
+ $"{CapacityBytes} bytes of physical capacity.");
}
if (LargestFreeBytes > CapacityBytes)
{
throw new InvalidOperationException(
$"{Domain} largest free range exceeds physical capacity.");
}
}
} }
internal readonly record struct ResidencySnapshot( internal readonly record struct ResidencySnapshot(
@ -177,7 +206,35 @@ internal readonly record struct ResidencySnapshot(
ResidencyCharges TotalCharges) ResidencyCharges TotalCharges)
{ {
public ResidencyDomainSnapshot Get(ResidencyDomain domain) => public ResidencyDomainSnapshot Get(ResidencyDomain domain) =>
Domains.FirstOrDefault(snapshot => snapshot.Domain == domain); Domains?.FirstOrDefault(snapshot => snapshot.Domain == domain)
?? default;
public int TotalEntries =>
Domains?.Sum(static snapshot => snapshot.EntryCount) ?? 0;
public int TotalOwners =>
Domains?.Sum(static snapshot => snapshot.OwnerCount) ?? 0;
public long TotalBudgetBytes =>
Domains?.Sum(static snapshot => snapshot.BudgetBytes) ?? 0;
public long TotalCapacityBytes =>
Domains?.Sum(static snapshot => snapshot.CapacityBytes) ?? 0;
public long TotalUsedBytes =>
Domains?.Sum(static snapshot => snapshot.UsedBytes) ?? 0;
public long TotalFragmentedFreeBytes =>
Domains?.Sum(static snapshot => snapshot.FragmentedFreeBytes) ?? 0;
public long TotalHits =>
Domains?.Sum(static snapshot => snapshot.Hits) ?? 0;
public long TotalMisses =>
Domains?.Sum(static snapshot => snapshot.Misses) ?? 0;
public long TotalEvictions =>
Domains?.Sum(static snapshot => snapshot.Evictions) ?? 0;
} }
internal interface IResidencyDomainSource internal interface IResidencyDomainSource

View file

@ -17,6 +17,7 @@ internal sealed class DelegateResidencyDomainSource(
throw new InvalidOperationException( throw new InvalidOperationException(
$"Residency source {Domain} returned {snapshot.Domain}."); $"Residency source {Domain} returned {snapshot.Domain}.");
} }
snapshot.Validate();
return snapshot; return snapshot;
} }
} }

View file

@ -19,6 +19,7 @@ public sealed record ResidencyBudgetOptions(
int StandaloneUnownedEntries, int StandaloneUnownedEntries,
long AnimationBytes, long AnimationBytes,
int AnimationEntries, int AnimationEntries,
long AudioBytes,
long AlphaScratchBytes) long AlphaScratchBytes)
{ {
public const long MiB = 1024L * 1024L; public const long MiB = 1024L * 1024L;
@ -36,6 +37,7 @@ public sealed record ResidencyBudgetOptions(
StandaloneUnownedEntries: 256, StandaloneUnownedEntries: 256,
AnimationBytes: 64 * MiB, AnimationBytes: 64 * MiB,
AnimationEntries: 512, AnimationEntries: 512,
AudioBytes: 32 * MiB,
AlphaScratchBytes: 16 * MiB); AlphaScratchBytes: 16 * MiB);
internal static ResidencyBudgetOptions Parse( internal static ResidencyBudgetOptions Parse(
@ -80,6 +82,9 @@ public sealed record ResidencyBudgetOptions(
AnimationEntries: ParseCount( AnimationEntries: ParseCount(
env("ACDREAM_RESIDENCY_ANIMATION_ENTRIES"), env("ACDREAM_RESIDENCY_ANIMATION_ENTRIES"),
defaults.AnimationEntries), defaults.AnimationEntries),
AudioBytes: ParseMiB(
env("ACDREAM_RESIDENCY_AUDIO_MIB"),
defaults.AudioBytes),
AlphaScratchBytes: ParseMiB( AlphaScratchBytes: ParseMiB(
env("ACDREAM_RESIDENCY_ALPHA_SCRATCH_MIB"), env("ACDREAM_RESIDENCY_ALPHA_SCRATCH_MIB"),
defaults.AlphaScratchBytes)); defaults.AlphaScratchBytes));
@ -110,4 +115,3 @@ public sealed record ResidencyBudgetOptions(
? count ? count
: fallback; : fallback;
} }

View file

@ -638,6 +638,7 @@ internal sealed class ResidencyManager
LogicalBytes: current.LogicalBytes, LogicalBytes: current.LogicalBytes,
CpuPreparedBytes: current.CpuPreparedBytes, CpuPreparedBytes: current.CpuPreparedBytes,
DecodedBytes: current.DecodedBytes, DecodedBytes: current.DecodedBytes,
ScratchBytes: current.ScratchBytes,
PinnedBytes: current.PinnedBytes, PinnedBytes: current.PinnedBytes,
RetiringBytes: checked( RetiringBytes: checked(
current.GpuRequestedBytes current.GpuRequestedBytes

View file

@ -1316,14 +1316,20 @@ namespace AcDream.App.Rendering.Wb
return bytes; return bytes;
} }
internal ResidencyDomainSnapshot CapturePreparedMeshResidency() => internal ResidencyDomainSnapshot CapturePreparedMeshResidency()
new( {
CacheStats stats = _cpuMeshCache.Stats;
return new ResidencyDomainSnapshot(
ResidencyDomain.PreparedMeshCpu, ResidencyDomain.PreparedMeshCpu,
EntryCount: _cpuMeshCache.Count, EntryCount: _cpuMeshCache.Count,
OwnerCount: 0, OwnerCount: 0,
Charges: new ResidencyCharges( Charges: new ResidencyCharges(
CpuPreparedBytes: _cpuMeshCache.ResidentBytes), CpuPreparedBytes: _cpuMeshCache.ResidentBytes),
BudgetBytes: _cpuMeshCache.ByteCapacity); BudgetBytes: _cpuMeshCache.ByteCapacity,
Hits: stats.Hits,
Misses: stats.Misses,
Evictions: stats.Evictions);
}
internal ResidencyDomainSnapshot CaptureStagingResidency() => internal ResidencyDomainSnapshot CaptureStagingResidency() =>
new( new(

View file

@ -8,6 +8,14 @@ using AcDream.Core.Content;
namespace AcDream.Core.Audio; namespace AcDream.Core.Audio;
public readonly record struct DatSoundCacheDiagnostics(
int CachedWaveCount,
long ResidentWaveBytes,
long BudgetBytes,
long Hits,
long Misses,
long Evictions);
/// <summary> /// <summary>
/// DatCollection-backed cache of decoded waves + SoundTable lookups. /// DatCollection-backed cache of decoded waves + SoundTable lookups.
/// ///
@ -74,6 +82,9 @@ public sealed class DatSoundCache
private readonly LinkedList<WaveEntry> _waveLru = new(); private readonly LinkedList<WaveEntry> _waveLru = new();
private readonly long _maxWaveBytes; private readonly long _maxWaveBytes;
private long _residentWaveBytes; private long _residentWaveBytes;
private long _hits;
private long _misses;
private long _evictions;
private sealed record WaveEntry(uint WaveId, WaveData Wave, long Bytes); private sealed record WaveEntry(uint WaveId, WaveData Wave, long Bytes);
@ -86,7 +97,7 @@ public sealed class DatSoundCache
/// Test seam for pinning a smaller byte budget so eviction can be /// Test seam for pinning a smaller byte budget so eviction can be
/// exercised deterministically without allocating tens of megabytes. /// exercised deterministically without allocating tens of megabytes.
/// </summary> /// </summary>
internal DatSoundCache(IDatObjectSource dats, long maxWaveBytes) public DatSoundCache(IDatObjectSource dats, long maxWaveBytes)
{ {
ArgumentNullException.ThrowIfNull(dats); ArgumentNullException.ThrowIfNull(dats);
ArgumentOutOfRangeException.ThrowIfLessThan(maxWaveBytes, 1); ArgumentOutOfRangeException.ThrowIfLessThan(maxWaveBytes, 1);
@ -105,13 +116,19 @@ public sealed class DatSoundCache
{ {
if (_waveEntries.TryGetValue(waveId, out var node)) if (_waveEntries.TryGetValue(waveId, out var node))
{ {
Interlocked.Increment(ref _hits);
Touch(node); Touch(node);
return node.Value.Wave; return node.Value.Wave;
} }
} }
if (_negativeWaveIds.ContainsKey(waveId)) if (_negativeWaveIds.ContainsKey(waveId))
{
Interlocked.Increment(ref _hits);
return null; return null;
}
Interlocked.Increment(ref _misses);
Lazy<WaveData?> lazy = _inflight.GetOrAdd( Lazy<WaveData?> lazy = _inflight.GetOrAdd(
waveId, waveId,
@ -171,6 +188,23 @@ public sealed class DatSoundCache
/// </summary> /// </summary>
public int CachedSoundTableCount => _tables.Count; public int CachedSoundTableCount => _tables.Count;
public DatSoundCacheDiagnostics Diagnostics
{
get
{
lock (_gate)
{
return new DatSoundCacheDiagnostics(
_waveEntries.Count,
_residentWaveBytes,
_maxWaveBytes,
Interlocked.Read(ref _hits),
Interlocked.Read(ref _misses),
Interlocked.Read(ref _evictions));
}
}
}
private WaveData? DecodeUncached(uint waveId) private WaveData? DecodeUncached(uint waveId)
{ {
var wave = _dats.Get<Wave>(waveId); var wave = _dats.Get<Wave>(waveId);
@ -225,5 +259,6 @@ public sealed class DatSoundCache
_waveLru.Remove(node); _waveLru.Remove(node);
_waveEntries.Remove(node.Value.WaveId); _waveEntries.Remove(node.Value.WaveId);
_residentWaveBytes -= node.Value.Bytes; _residentWaveBytes -= node.Value.Bytes;
Interlocked.Increment(ref _evictions);
} }
} }

View file

@ -433,7 +433,15 @@ public sealed class ContentEffectsAudioCompositionTests
EntityEffectPoseRegistry poses) => new(lighting, poses); EntityEffectPoseRegistry poses) => new(lighting, poses);
public TranslucencyHookSink CreateTranslucencySink( public TranslucencyHookSink CreateTranslucencySink(
TranslucencyFadeManager fades) => new(fades); TranslucencyFadeManager fades) => new(fades);
public DatSoundCache CreateSoundCache(IDatReaderWriter dats) => new(dats); public DatSoundCache CreateSoundCache(
IDatReaderWriter dats,
long maximumDecodedBytes)
{
Assert.Equal(
ResidencyBudgetOptions.Default.AudioBytes,
maximumDecodedBytes);
return new DatSoundCache(dats, maximumDecodedBytes);
}
public OpenAlAudioEngine CreateAudioEngine() public OpenAlAudioEngine CreateAudioEngine()
{ {

View file

@ -308,7 +308,8 @@ public sealed class WorldRenderCompositionTests
WbMeshAdapter meshes, WbMeshAdapter meshes,
TextureCache textures, TextureCache textures,
IPreparedAssetSource preparedAssets, IPreparedAssetSource preparedAssets,
IAnimationLoader animations) IAnimationLoader animations,
AcDream.Core.Audio.DatSoundCache? audio)
{ {
RegisteredResidency = manager; RegisteredResidency = manager;
} }

View file

@ -1,6 +1,7 @@
using System.Text.Json; using System.Text.Json;
using AcDream.App.Diagnostics; using AcDream.App.Diagnostics;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Rendering.Residency;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.UI.Testing; using AcDream.App.UI.Testing;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
@ -114,6 +115,20 @@ public sealed class WorldLifecycleAutomationControllerTests
LoadedLandblocks = 1, LoadedLandblocks = 1,
WorldEntities = 42, WorldEntities = 42,
TrackedGpuBytes = 1234, TrackedGpuBytes = 1234,
Residency = new ResidencySnapshot(
[
new ResidencyDomainSnapshot(
ResidencyDomain.Animations,
EntryCount: 2,
OwnerCount: 0,
Charges: new ResidencyCharges(
DecodedBytes: 1024),
BudgetBytes: 4096,
Hits: 8,
Misses: 3,
Evictions: 1),
],
new ResidencyCharges(DecodedBytes: 1024)),
}; };
var screenshots = new FrameScreenshotController( var screenshots = new FrameScreenshotController(
(_, _) => [0, 0, 0, 255], (_, _) => [0, 0, 0, 255],
@ -151,6 +166,18 @@ public sealed class WorldLifecycleAutomationControllerTests
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation") Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
.GetProperty("screenshotCaptured").GetBoolean()); .GetProperty("screenshotCaptured").GetBoolean());
Assert.Equal(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32()); Assert.Equal(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32());
JsonElement residency = json.RootElement
.GetProperty("resources")
.GetProperty("residency");
Assert.Equal(
4096,
residency.GetProperty("totalBudgetBytes").GetInt64());
Assert.Equal(8, residency.GetProperty("totalHits").GetInt64());
Assert.Equal(
1024,
residency.GetProperty("totalCharges")
.GetProperty("decodedBytes")
.GetInt64());
Assert.True(File.Exists(Path.Combine(directory, "checkpoint-dungeon.json"))); Assert.True(File.Exists(Path.Combine(directory, "checkpoint-dungeon.json")));
} }
finally finally

View file

@ -393,6 +393,90 @@ public sealed class ResidencyManagerTests
16, 16,
snapshot.Get(ResidencyDomain.StandaloneTextures) snapshot.Get(ResidencyDomain.StandaloneTextures)
.FragmentedFreeBytes); .FragmentedFreeBytes);
Assert.Equal(256, snapshot.TotalBudgetBytes);
Assert.Equal(128, snapshot.TotalCapacityBytes);
Assert.Equal(96, snapshot.TotalUsedBytes);
Assert.Equal(16, snapshot.TotalFragmentedFreeBytes);
Assert.Equal(3, snapshot.TotalEntries);
Assert.Equal(2, snapshot.TotalOwners);
}
[Fact]
public void ForcedPressureAcrossEveryDomainConvergesToZero()
{
const long budget = 64;
var manager = new ResidencyManager();
var current = new Dictionary<
ResidencyDomain,
ResidencyDomainSnapshot>();
foreach (ResidencyDomain domain in Enum.GetValues<ResidencyDomain>())
{
current[domain] = new ResidencyDomainSnapshot(
domain,
EntryCount: 2,
OwnerCount: 1,
Charges: new ResidencyCharges(
CpuPreparedBytes: budget * 2),
BudgetBytes: budget,
Hits: 3,
Misses: 2,
Evictions: 1);
ResidencyDomain capturedDomain = domain;
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
capturedDomain,
() => current[capturedDomain]));
}
ResidencySnapshot pressured = manager.CaptureSnapshot();
Assert.Equal(
Enum.GetValues<ResidencyDomain>().Length,
pressured.Domains.Count);
Assert.All(
pressured.Domains,
domain => Assert.True(
domain.Charges.CommittedCpuBytes > domain.BudgetBytes));
Assert.Equal(
budget * 2 * pressured.Domains.Count,
pressured.TotalCharges.CommittedCpuBytes);
foreach (ResidencyDomain domain in Enum.GetValues<ResidencyDomain>())
{
current[domain] = new ResidencyDomainSnapshot(
domain,
EntryCount: 0,
OwnerCount: 0,
Charges: ResidencyCharges.Zero,
BudgetBytes: budget,
Hits: 3,
Misses: 2,
Evictions: 3);
}
ResidencySnapshot drained = manager.CaptureSnapshot();
Assert.True(drained.TotalCharges.IsZero);
Assert.Equal(0, drained.TotalEntries);
Assert.Equal(0, drained.TotalOwners);
Assert.Equal(budget * drained.Domains.Count, drained.TotalBudgetBytes);
Assert.Equal(30, drained.TotalEvictions);
}
[Fact]
public void DomainSourceRejectsImpossibleAllocatorFacts()
{
var source = new DelegateResidencyDomainSource(
ResidencyDomain.GlobalMeshArena,
() => new ResidencyDomainSnapshot(
ResidencyDomain.GlobalMeshArena,
EntryCount: 1,
OwnerCount: 0,
Charges: ResidencyCharges.Zero,
CapacityBytes: 64,
UsedBytes: 65));
Assert.Throws<InvalidOperationException>(
() => source.CaptureResidency());
} }
[Fact] [Fact]

View file

@ -31,6 +31,7 @@ public sealed class RuntimeOptionsTests
["ACDREAM_RESIDENCY_MESH_UNOWNED_ENTRIES"] = "72", ["ACDREAM_RESIDENCY_MESH_UNOWNED_ENTRIES"] = "72",
["ACDREAM_RESIDENCY_ANIMATION_MIB"] = "48", ["ACDREAM_RESIDENCY_ANIMATION_MIB"] = "48",
["ACDREAM_RESIDENCY_ANIMATION_ENTRIES"] = "300", ["ACDREAM_RESIDENCY_ANIMATION_ENTRIES"] = "300",
["ACDREAM_RESIDENCY_AUDIO_MIB"] = "24",
}; };
RuntimeOptions options = RuntimeOptions.Parse( RuntimeOptions options = RuntimeOptions.Parse(
@ -47,6 +48,9 @@ public sealed class RuntimeOptionsTests
48 * ResidencyBudgetOptions.MiB, 48 * ResidencyBudgetOptions.MiB,
options.ResidencyBudgets.AnimationBytes); options.ResidencyBudgets.AnimationBytes);
Assert.Equal(300, options.ResidencyBudgets.AnimationEntries); Assert.Equal(300, options.ResidencyBudgets.AnimationEntries);
Assert.Equal(
24 * ResidencyBudgetOptions.MiB,
options.ResidencyBudgets.AudioBytes);
} }
[Theory] [Theory]

View file

@ -110,6 +110,14 @@ public sealed class DatSoundCacheTests
Assert.NotNull(b2); Assert.NotNull(b2);
Assert.NotSame(b1, b2); // evicted entries decode fresh on next request Assert.NotSame(b1, b2); // evicted entries decode fresh on next request
Assert.Equal(2, dats.GetCallCount(2)); Assert.Equal(2, dats.GetCallCount(2));
DatSoundCacheDiagnostics diagnostics = cache.Diagnostics;
Assert.Equal(2, diagnostics.CachedWaveCount);
Assert.Equal(300, diagnostics.ResidentWaveBytes);
Assert.Equal(300, diagnostics.BudgetBytes);
Assert.True(diagnostics.Hits >= 3);
Assert.True(diagnostics.Misses >= 4);
Assert.True(diagnostics.Evictions >= 2);
} }
[Fact] [Fact]