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:
parent
f2644d42c2
commit
1853a57c12
15 changed files with 297 additions and 15 deletions
|
|
@ -113,7 +113,9 @@ internal interface IContentEffectsAudioCompositionFactory
|
|||
EntityEffectPoseRegistry poses);
|
||||
TranslucencyHookSink CreateTranslucencySink(TranslucencyFadeManager fades);
|
||||
AnimationHookRegistrationSet CreateHookRegistrations(AnimationHookRouter router);
|
||||
DatSoundCache CreateSoundCache(IDatReaderWriter dats);
|
||||
DatSoundCache CreateSoundCache(
|
||||
IDatReaderWriter dats,
|
||||
long maximumDecodedBytes);
|
||||
OpenAlAudioEngine CreateAudioEngine();
|
||||
DictionaryEntitySoundTable CreateEntitySoundTables();
|
||||
AudioHookSink CreateAudioSink(
|
||||
|
|
@ -204,7 +206,10 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
|
|||
AnimationHookRouter router) =>
|
||||
new(router);
|
||||
|
||||
public DatSoundCache CreateSoundCache(IDatReaderWriter dats) => new(dats);
|
||||
public DatSoundCache CreateSoundCache(
|
||||
IDatReaderWriter dats,
|
||||
long maximumDecodedBytes) =>
|
||||
new(dats, maximumDecodedBytes);
|
||||
|
||||
public OpenAlAudioEngine CreateAudioEngine() => new();
|
||||
|
||||
|
|
@ -426,7 +431,9 @@ internal sealed class ContentEffectsAudioCompositionPhase :
|
|||
engineLease;
|
||||
try
|
||||
{
|
||||
DatSoundCache cache = _factory.CreateSoundCache(dats);
|
||||
DatSoundCache cache = _factory.CreateSoundCache(
|
||||
dats,
|
||||
_dependencies.ResidencyBudgets.AudioBytes);
|
||||
Fault(ContentEffectsAudioCompositionPoint.SoundCacheCreated);
|
||||
engineLease = audioScope.Acquire(
|
||||
"OpenAL audio engine",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using AcDream.App.Rendering.Residency;
|
|||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Content.Vfx;
|
||||
using AcDream.Core.Audio;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Terrain;
|
||||
using AcDream.UI.Abstractions.Settings;
|
||||
|
|
@ -119,7 +120,8 @@ internal interface IWorldRenderCompositionFactory
|
|||
WbMeshAdapter meshes,
|
||||
TextureCache textures,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
IAnimationLoader animations);
|
||||
IAnimationLoader animations,
|
||||
DatSoundCache? audio);
|
||||
SamplerCache CreateSamplerCache(GL gl);
|
||||
void Release(IDisposable resource);
|
||||
}
|
||||
|
|
@ -299,7 +301,8 @@ internal sealed class RetailWorldRenderCompositionFactory
|
|||
WbMeshAdapter meshes,
|
||||
TextureCache textures,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
IAnimationLoader animations)
|
||||
IAnimationLoader animations,
|
||||
DatSoundCache? audio)
|
||||
{
|
||||
meshes.RegisterResidencySources(manager);
|
||||
textures.RegisterResidencySources(manager);
|
||||
|
|
@ -328,9 +331,30 @@ internal sealed class RetailWorldRenderCompositionFactory
|
|||
DecodedBytes:
|
||||
diagnostics.EstimatedBytes),
|
||||
BudgetBytes: diagnostics.BudgetBytes,
|
||||
Hits: diagnostics.Stats.Hits,
|
||||
Misses: diagnostics.Stats.Misses,
|
||||
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);
|
||||
|
|
@ -517,7 +541,8 @@ internal sealed class WorldRenderCompositionPhase
|
|||
meshAdapter,
|
||||
textureCache,
|
||||
content.PreparedAssets,
|
||||
content.AnimationLoader);
|
||||
content.AnimationLoader,
|
||||
content.Audio?.SoundCache);
|
||||
|
||||
scope.Complete();
|
||||
_dependencies.Log(
|
||||
|
|
|
|||
|
|
@ -164,12 +164,41 @@ internal readonly record struct ResidencyDomainSnapshot(
|
|||
long CapacityBytes = 0,
|
||||
long UsedBytes = 0,
|
||||
long LargestFreeBytes = 0,
|
||||
long Hits = 0,
|
||||
long Misses = 0,
|
||||
long Evictions = 0)
|
||||
{
|
||||
public long FreeBytes => Math.Max(0, CapacityBytes - UsedBytes);
|
||||
|
||||
public long FragmentedFreeBytes =>
|
||||
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(
|
||||
|
|
@ -177,7 +206,35 @@ internal readonly record struct ResidencySnapshot(
|
|||
ResidencyCharges TotalCharges)
|
||||
{
|
||||
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
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ internal sealed class DelegateResidencyDomainSource(
|
|||
throw new InvalidOperationException(
|
||||
$"Residency source {Domain} returned {snapshot.Domain}.");
|
||||
}
|
||||
snapshot.Validate();
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public sealed record ResidencyBudgetOptions(
|
|||
int StandaloneUnownedEntries,
|
||||
long AnimationBytes,
|
||||
int AnimationEntries,
|
||||
long AudioBytes,
|
||||
long AlphaScratchBytes)
|
||||
{
|
||||
public const long MiB = 1024L * 1024L;
|
||||
|
|
@ -36,6 +37,7 @@ public sealed record ResidencyBudgetOptions(
|
|||
StandaloneUnownedEntries: 256,
|
||||
AnimationBytes: 64 * MiB,
|
||||
AnimationEntries: 512,
|
||||
AudioBytes: 32 * MiB,
|
||||
AlphaScratchBytes: 16 * MiB);
|
||||
|
||||
internal static ResidencyBudgetOptions Parse(
|
||||
|
|
@ -80,6 +82,9 @@ public sealed record ResidencyBudgetOptions(
|
|||
AnimationEntries: ParseCount(
|
||||
env("ACDREAM_RESIDENCY_ANIMATION_ENTRIES"),
|
||||
defaults.AnimationEntries),
|
||||
AudioBytes: ParseMiB(
|
||||
env("ACDREAM_RESIDENCY_AUDIO_MIB"),
|
||||
defaults.AudioBytes),
|
||||
AlphaScratchBytes: ParseMiB(
|
||||
env("ACDREAM_RESIDENCY_ALPHA_SCRATCH_MIB"),
|
||||
defaults.AlphaScratchBytes));
|
||||
|
|
@ -110,4 +115,3 @@ public sealed record ResidencyBudgetOptions(
|
|||
? count
|
||||
: fallback;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -638,6 +638,7 @@ internal sealed class ResidencyManager
|
|||
LogicalBytes: current.LogicalBytes,
|
||||
CpuPreparedBytes: current.CpuPreparedBytes,
|
||||
DecodedBytes: current.DecodedBytes,
|
||||
ScratchBytes: current.ScratchBytes,
|
||||
PinnedBytes: current.PinnedBytes,
|
||||
RetiringBytes: checked(
|
||||
current.GpuRequestedBytes
|
||||
|
|
|
|||
|
|
@ -1316,14 +1316,20 @@ namespace AcDream.App.Rendering.Wb
|
|||
return bytes;
|
||||
}
|
||||
|
||||
internal ResidencyDomainSnapshot CapturePreparedMeshResidency() =>
|
||||
new(
|
||||
internal ResidencyDomainSnapshot CapturePreparedMeshResidency()
|
||||
{
|
||||
CacheStats stats = _cpuMeshCache.Stats;
|
||||
return new ResidencyDomainSnapshot(
|
||||
ResidencyDomain.PreparedMeshCpu,
|
||||
EntryCount: _cpuMeshCache.Count,
|
||||
OwnerCount: 0,
|
||||
Charges: new ResidencyCharges(
|
||||
CpuPreparedBytes: _cpuMeshCache.ResidentBytes),
|
||||
BudgetBytes: _cpuMeshCache.ByteCapacity);
|
||||
BudgetBytes: _cpuMeshCache.ByteCapacity,
|
||||
Hits: stats.Hits,
|
||||
Misses: stats.Misses,
|
||||
Evictions: stats.Evictions);
|
||||
}
|
||||
|
||||
internal ResidencyDomainSnapshot CaptureStagingResidency() =>
|
||||
new(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@ using AcDream.Core.Content;
|
|||
|
||||
namespace AcDream.Core.Audio;
|
||||
|
||||
public readonly record struct DatSoundCacheDiagnostics(
|
||||
int CachedWaveCount,
|
||||
long ResidentWaveBytes,
|
||||
long BudgetBytes,
|
||||
long Hits,
|
||||
long Misses,
|
||||
long Evictions);
|
||||
|
||||
/// <summary>
|
||||
/// DatCollection-backed cache of decoded waves + SoundTable lookups.
|
||||
///
|
||||
|
|
@ -74,6 +82,9 @@ public sealed class DatSoundCache
|
|||
private readonly LinkedList<WaveEntry> _waveLru = new();
|
||||
private readonly long _maxWaveBytes;
|
||||
private long _residentWaveBytes;
|
||||
private long _hits;
|
||||
private long _misses;
|
||||
private long _evictions;
|
||||
|
||||
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
|
||||
/// exercised deterministically without allocating tens of megabytes.
|
||||
/// </summary>
|
||||
internal DatSoundCache(IDatObjectSource dats, long maxWaveBytes)
|
||||
public DatSoundCache(IDatObjectSource dats, long maxWaveBytes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(maxWaveBytes, 1);
|
||||
|
|
@ -105,13 +116,19 @@ public sealed class DatSoundCache
|
|||
{
|
||||
if (_waveEntries.TryGetValue(waveId, out var node))
|
||||
{
|
||||
Interlocked.Increment(ref _hits);
|
||||
Touch(node);
|
||||
return node.Value.Wave;
|
||||
}
|
||||
}
|
||||
|
||||
if (_negativeWaveIds.ContainsKey(waveId))
|
||||
{
|
||||
Interlocked.Increment(ref _hits);
|
||||
return null;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _misses);
|
||||
|
||||
Lazy<WaveData?> lazy = _inflight.GetOrAdd(
|
||||
waveId,
|
||||
|
|
@ -171,6 +188,23 @@ public sealed class DatSoundCache
|
|||
/// </summary>
|
||||
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)
|
||||
{
|
||||
var wave = _dats.Get<Wave>(waveId);
|
||||
|
|
@ -225,5 +259,6 @@ public sealed class DatSoundCache
|
|||
_waveLru.Remove(node);
|
||||
_waveEntries.Remove(node.Value.WaveId);
|
||||
_residentWaveBytes -= node.Value.Bytes;
|
||||
Interlocked.Increment(ref _evictions);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue