test(app): add canonical connected soak snapshots
Make scripted lifecycle checkpoints acknowledged post-diagnostics render barriers, capture the exact frame outcome beside canonical resource ownership, and harden the nine-stop route with ordered same-location cache and lifetime gates without weakening process residency thresholds. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
2862622ba2
commit
bca4148739
17 changed files with 995 additions and 52 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.UI.Testing;
|
||||
|
||||
|
|
@ -71,14 +72,73 @@ internal sealed record WorldLifecycleCheckpoint(
|
|||
DateTime TimestampUtc,
|
||||
int ProcessId,
|
||||
WorldRevealLifecycleSnapshot Reveal,
|
||||
RenderFrameOutcome Render,
|
||||
WorldLifecycleResourceSnapshot Resources);
|
||||
|
||||
internal sealed class WorldLifecycleCheckpointRequest :
|
||||
IRetailUiAutomationCheckpoint
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
private RetailUiAutomationCheckpointStatus _status =
|
||||
RetailUiAutomationCheckpointStatus.Pending;
|
||||
private string? _error;
|
||||
|
||||
public WorldLifecycleCheckpointRequest(object owner, int sequence, string name)
|
||||
{
|
||||
Owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
Sequence = sequence;
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
internal object Owner { get; }
|
||||
public int Sequence { get; }
|
||||
public string Name { get; }
|
||||
|
||||
public RetailUiAutomationCheckpointStatus Status
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _status;
|
||||
}
|
||||
}
|
||||
|
||||
public string? Error
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
return _error;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TrySetTerminal(
|
||||
RetailUiAutomationCheckpointStatus status,
|
||||
string? error)
|
||||
{
|
||||
if (status == RetailUiAutomationCheckpointStatus.Pending)
|
||||
throw new ArgumentOutOfRangeException(nameof(status));
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_status != RetailUiAutomationCheckpointStatus.Pending)
|
||||
return false;
|
||||
_status = status;
|
||||
_error = error;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic-only runtime seam used by production retained-UI scripts. It
|
||||
/// writes one structured checkpoint at explicit script edges and delegates GL
|
||||
/// capture to the render-thread screenshot owner.
|
||||
/// </summary>
|
||||
internal sealed class WorldLifecycleAutomationController : IRetailUiAutomationRuntime
|
||||
internal sealed class WorldLifecycleAutomationController :
|
||||
IRetailUiAutomationRuntime,
|
||||
IRenderFramePostDiagnosticsPhase,
|
||||
IDisposable
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
|
|
@ -87,16 +147,21 @@ internal sealed class WorldLifecycleAutomationController : IRetailUiAutomationRu
|
|||
|
||||
private readonly Func<WorldRevealLifecycleSnapshot> _getReveal;
|
||||
private readonly Func<int> _getPortalMaterializationCount;
|
||||
private readonly Func<WorldLifecycleResourceSnapshot> _captureResources;
|
||||
private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot>
|
||||
_captureResources;
|
||||
private readonly FrameScreenshotController _screenshots;
|
||||
private readonly string _artifactDirectory;
|
||||
private readonly Action<string> _log;
|
||||
private readonly object _requestOwner = new();
|
||||
private readonly object _sync = new();
|
||||
private readonly Queue<WorldLifecycleCheckpointRequest> _requests = [];
|
||||
private int _sequence;
|
||||
private bool _disposed;
|
||||
|
||||
public WorldLifecycleAutomationController(
|
||||
Func<WorldRevealLifecycleSnapshot> getReveal,
|
||||
Func<int> getPortalMaterializationCount,
|
||||
Func<WorldLifecycleResourceSnapshot> captureResources,
|
||||
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources,
|
||||
FrameScreenshotController screenshots,
|
||||
string artifactDirectory,
|
||||
Action<string>? log = null)
|
||||
|
|
@ -116,35 +181,127 @@ internal sealed class WorldLifecycleAutomationController : IRetailUiAutomationRu
|
|||
public bool IsWorldViewportVisible => _getReveal().WorldViewportObserved;
|
||||
public int PortalMaterializationCount => _getPortalMaterializationCount();
|
||||
|
||||
public bool TryWriteCheckpoint(string name, out string error)
|
||||
public bool TryRequestCheckpoint(
|
||||
string name,
|
||||
out IRetailUiAutomationCheckpoint? checkpoint,
|
||||
out string error)
|
||||
{
|
||||
checkpoint = null;
|
||||
if (!AutomationArtifactName.TryValidate(name, out error))
|
||||
return false;
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
error = $"checkpoint '{name}' was rejected because world lifecycle automation is shutting down";
|
||||
return false;
|
||||
}
|
||||
|
||||
var request = new WorldLifecycleCheckpointRequest(
|
||||
_requestOwner,
|
||||
checked(++_sequence),
|
||||
name);
|
||||
_requests.Enqueue(request);
|
||||
checkpoint = request;
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelCheckpoint(IRetailUiAutomationCheckpoint checkpoint)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(checkpoint);
|
||||
if (checkpoint is not WorldLifecycleCheckpointRequest request
|
||||
|| !ReferenceEquals(request.Owner, _requestOwner))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The checkpoint acknowledgement is not owned by this automation runtime.",
|
||||
nameof(checkpoint));
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
request.TrySetTerminal(
|
||||
RetailUiAutomationCheckpointStatus.Cancelled,
|
||||
$"checkpoint '{request.Name}' was cancelled before render-frame capture");
|
||||
}
|
||||
}
|
||||
|
||||
public void Process(RenderFrameInput input, RenderFrameOutcome outcome)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
while (_requests.Count != 0)
|
||||
{
|
||||
WorldLifecycleCheckpointRequest request = _requests.Dequeue();
|
||||
if (request.Status != RetailUiAutomationCheckpointStatus.Pending)
|
||||
continue;
|
||||
WriteCheckpoint(request, outcome);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCheckpoint(
|
||||
WorldLifecycleCheckpointRequest request,
|
||||
RenderFrameOutcome outcome)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(_artifactDirectory);
|
||||
var checkpoint = new WorldLifecycleCheckpoint(
|
||||
Sequence: checked(++_sequence),
|
||||
Name: name,
|
||||
Sequence: request.Sequence,
|
||||
Name: request.Name,
|
||||
TimestampUtc: DateTime.UtcNow,
|
||||
ProcessId: Environment.ProcessId,
|
||||
Reveal: _getReveal(),
|
||||
Resources: _captureResources());
|
||||
Render: outcome,
|
||||
Resources: _captureResources(outcome));
|
||||
string json = JsonSerializer.Serialize(checkpoint, JsonOptions);
|
||||
string timelinePath = Path.Combine(_artifactDirectory, "world-lifecycle.checkpoints.jsonl");
|
||||
File.AppendAllText(timelinePath, json + Environment.NewLine);
|
||||
string timelinePath = Path.Combine(
|
||||
_artifactDirectory,
|
||||
"world-lifecycle.checkpoints.jsonl");
|
||||
File.WriteAllText(
|
||||
Path.Combine(_artifactDirectory, $"checkpoint-{name}.json"),
|
||||
Path.Combine(
|
||||
_artifactDirectory,
|
||||
$"checkpoint-{request.Name}.json"),
|
||||
json + Environment.NewLine);
|
||||
_log($"[world-gate] checkpoint name={name} sequence={checkpoint.Sequence} path={timelinePath}");
|
||||
error = string.Empty;
|
||||
return true;
|
||||
// Timeline publication is the commit edge: a visible row always
|
||||
// has its complete named checkpoint artifact beside it.
|
||||
File.AppendAllText(timelinePath, json + Environment.NewLine);
|
||||
request.TrySetTerminal(
|
||||
RetailUiAutomationCheckpointStatus.Succeeded,
|
||||
error: null);
|
||||
_log(
|
||||
$"[world-gate] checkpoint name={request.Name} "
|
||||
+ $"sequence={request.Sequence} path={timelinePath}");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
error = $"checkpoint '{name}' failed: {exception.Message}";
|
||||
return false;
|
||||
request.TrySetTerminal(
|
||||
RetailUiAutomationCheckpointStatus.Failed,
|
||||
$"checkpoint '{request.Name}' sequence {request.Sequence} failed: "
|
||||
+ exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
while (_requests.Count != 0)
|
||||
{
|
||||
WorldLifecycleCheckpointRequest request = _requests.Dequeue();
|
||||
request.TrySetTerminal(
|
||||
RetailUiAutomationCheckpointStatus.Cancelled,
|
||||
$"checkpoint '{request.Name}' was cancelled during world lifecycle automation shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
?? throw new ArgumentNullException(nameof(frameProfiler));
|
||||
}
|
||||
|
||||
public WorldLifecycleResourceSnapshot Capture()
|
||||
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
||||
{
|
||||
ObjectMeshManager meshManager = _meshes.MeshManager
|
||||
?? throw new InvalidOperationException(
|
||||
|
|
@ -82,8 +82,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
LoadedLandblocks: _world.LoadedLandblockIds.Count,
|
||||
WorldEntities: _world.Entities.Count,
|
||||
AnimatedEntities: _animations.Count,
|
||||
VisibleLandblocks: render.VisibleLandblocks,
|
||||
TotalLandblocks: render.TotalLandblocks,
|
||||
VisibleLandblocks: outcome.World.VisibleLandblocks,
|
||||
TotalLandblocks: outcome.World.TotalLandblocks,
|
||||
LiveEntities: _liveEntities.Count,
|
||||
MaterializedLiveEntities: _liveEntities.MaterializedCount,
|
||||
PendingLiveTeardowns: _liveEntities.PendingTeardownCount,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue