test(runtime): add deterministic world gate artifacts
This commit is contained in:
parent
db45b81f75
commit
354c2adc2e
11 changed files with 703 additions and 10 deletions
|
|
@ -0,0 +1,155 @@
|
|||
using System.Text.Json;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.UI.Testing;
|
||||
|
||||
namespace AcDream.App.Diagnostics;
|
||||
|
||||
internal static class AutomationArtifactName
|
||||
{
|
||||
public static bool TryValidate(string? name, out string error)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name) || name.Length > 80)
|
||||
{
|
||||
error = "artifact name must contain 1-80 characters";
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (char c in name)
|
||||
{
|
||||
if (!char.IsAsciiLetterOrDigit(c) && c is not '-' and not '_')
|
||||
{
|
||||
error = $"artifact name '{name}' may contain only letters, digits, '-' and '_'";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record WorldLifecycleResourceSnapshot(
|
||||
int LoadedLandblocks,
|
||||
int WorldEntities,
|
||||
int AnimatedEntities,
|
||||
int VisibleLandblocks,
|
||||
int TotalLandblocks,
|
||||
int LiveEntities,
|
||||
int MaterializedLiveEntities,
|
||||
int PendingLiveTeardowns,
|
||||
int PendingLandblockRetirements,
|
||||
int ParticleEmitters,
|
||||
int Particles,
|
||||
int ParticleBindings,
|
||||
int ParticleOwners,
|
||||
int EffectOwners,
|
||||
int LightOwners,
|
||||
int ScriptOwners,
|
||||
int ActiveScripts,
|
||||
int MeshRenderData,
|
||||
int MeshAtlasArrays,
|
||||
long MeshEstimatedBytes,
|
||||
int StagedMeshUploads,
|
||||
long StagedMeshBytes,
|
||||
long TrackedGpuBytes,
|
||||
int TrackedGpuBuffers,
|
||||
int TrackedGpuTextures,
|
||||
int OwnedCompositeTextures,
|
||||
int CompositeTextureOwners,
|
||||
int ActiveParticleTextures,
|
||||
int ParticleTextureOwners,
|
||||
int CompositeWarmupPending,
|
||||
long ManagedBytes,
|
||||
long ManagedCommittedBytes,
|
||||
double Fps,
|
||||
double FrameMilliseconds,
|
||||
string? LastFrameProfile);
|
||||
|
||||
internal sealed record WorldLifecycleCheckpoint(
|
||||
int Sequence,
|
||||
string Name,
|
||||
DateTime TimestampUtc,
|
||||
int ProcessId,
|
||||
WorldRevealLifecycleSnapshot Reveal,
|
||||
WorldLifecycleResourceSnapshot Resources);
|
||||
|
||||
/// <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
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
private readonly Func<WorldRevealLifecycleSnapshot> _getReveal;
|
||||
private readonly Func<int> _getPortalMaterializationCount;
|
||||
private readonly Func<WorldLifecycleResourceSnapshot> _captureResources;
|
||||
private readonly FrameScreenshotController _screenshots;
|
||||
private readonly string _artifactDirectory;
|
||||
private readonly Action<string> _log;
|
||||
private int _sequence;
|
||||
|
||||
public WorldLifecycleAutomationController(
|
||||
Func<WorldRevealLifecycleSnapshot> getReveal,
|
||||
Func<int> getPortalMaterializationCount,
|
||||
Func<WorldLifecycleResourceSnapshot> captureResources,
|
||||
FrameScreenshotController screenshots,
|
||||
string artifactDirectory,
|
||||
Action<string>? log = null)
|
||||
{
|
||||
_getReveal = getReveal ?? throw new ArgumentNullException(nameof(getReveal));
|
||||
_getPortalMaterializationCount = getPortalMaterializationCount
|
||||
?? throw new ArgumentNullException(nameof(getPortalMaterializationCount));
|
||||
_captureResources = captureResources ?? throw new ArgumentNullException(nameof(captureResources));
|
||||
_screenshots = screenshots ?? throw new ArgumentNullException(nameof(screenshots));
|
||||
_artifactDirectory = string.IsNullOrWhiteSpace(artifactDirectory)
|
||||
? throw new ArgumentException("An automation artifact directory is required.", nameof(artifactDirectory))
|
||||
: Path.GetFullPath(artifactDirectory);
|
||||
_log = log ?? (_ => { });
|
||||
}
|
||||
|
||||
public bool IsWorldReady => _getReveal().IsReady;
|
||||
public bool IsWorldViewportVisible => _getReveal().WorldViewportObserved;
|
||||
public int PortalMaterializationCount => _getPortalMaterializationCount();
|
||||
|
||||
public bool TryWriteCheckpoint(string name, out string error)
|
||||
{
|
||||
if (!AutomationArtifactName.TryValidate(name, out error))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(_artifactDirectory);
|
||||
var checkpoint = new WorldLifecycleCheckpoint(
|
||||
Sequence: checked(++_sequence),
|
||||
Name: name,
|
||||
TimestampUtc: DateTime.UtcNow,
|
||||
ProcessId: Environment.ProcessId,
|
||||
Reveal: _getReveal(),
|
||||
Resources: _captureResources());
|
||||
string json = JsonSerializer.Serialize(checkpoint, JsonOptions);
|
||||
string timelinePath = Path.Combine(_artifactDirectory, "world-lifecycle.checkpoints.jsonl");
|
||||
File.AppendAllText(timelinePath, json + Environment.NewLine);
|
||||
File.WriteAllText(
|
||||
Path.Combine(_artifactDirectory, $"checkpoint-{name}.json"),
|
||||
json + Environment.NewLine);
|
||||
_log($"[world-gate] checkpoint name={name} sequence={checkpoint.Sequence} path={timelinePath}");
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
error = $"checkpoint '{name}' failed: {exception.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRequestScreenshot(string name, out string error) =>
|
||||
_screenshots.TryRequest(name, out error);
|
||||
|
||||
public bool IsScreenshotComplete(string name) => _screenshots.IsComplete(name);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue