독립 검증 도구 추가와 앱 상태 책임 분리
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ProjectQuint.Probes;
|
||||
|
||||
public sealed record CoreBenchmarkReport(int SchemaVersion, bool Complete, DateTimeOffset MeasuredAtUtc,
|
||||
string Runtime, string OperatingSystem, string Architecture, int ProcessorCount,
|
||||
int Notes, int Events, uint Seed, int FrameRate, int WarmupIterations, int RequestedIterations,
|
||||
ReplayObservation[] Samples);
|
||||
|
||||
public static class CoreBenchmark
|
||||
{
|
||||
public static CoreBenchmarkReport Run(int notes, uint seed, int frameRate, int iterations,
|
||||
Action<CoreBenchmarkReport> checkpoint)
|
||||
{
|
||||
if (iterations < 1 || iterations > 100)
|
||||
throw new ArgumentOutOfRangeException(nameof(iterations), "Use 1 to 100 iterations.");
|
||||
var workload = SyntheticWorkload.Create(notes, seed);
|
||||
const int warmups = 3;
|
||||
JudgementSnapshot expected = default;
|
||||
for (var index = 0; index < warmups; index++)
|
||||
expected = ReplayRunner.Run(workload, frameRate).Judgement;
|
||||
var samples = new List<ReplayObservation>(iterations);
|
||||
var measuredAt = DateTimeOffset.UtcNow;
|
||||
for (var index = 0; index < iterations; index++)
|
||||
{
|
||||
var sample = ReplayRunner.Run(workload, frameRate);
|
||||
if (sample.Judgement != expected || !sample.Judgement.Finished)
|
||||
throw new InvalidOperationException("The benchmark replay diverged from its warmup result.");
|
||||
samples.Add(sample);
|
||||
checkpoint(Report(false));
|
||||
}
|
||||
return Report(true);
|
||||
|
||||
CoreBenchmarkReport Report(bool complete) => new(1, complete, measuredAt,
|
||||
RuntimeInformation.FrameworkDescription,
|
||||
System.OperatingSystem.IsMacOS() ? "macOS" :
|
||||
System.OperatingSystem.IsLinux() ? "Linux" :
|
||||
System.OperatingSystem.IsWindows() ? "Windows" : "Other",
|
||||
RuntimeInformation.ProcessArchitecture.ToString(), Environment.ProcessorCount,
|
||||
notes, workload.Events.Length, seed, frameRate, warmups, iterations, samples.ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../ProjectQuint.Core/ProjectQuint.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Diagnostics;
|
||||
using ProjectQuint.Core.Judgement;
|
||||
|
||||
namespace ProjectQuint.Probes;
|
||||
|
||||
public readonly record struct JudgementSnapshot(int Score, int Combo, int MaxCombo,
|
||||
int MissCount, int KoolCount, int CoolCount, int GoodCount, int JudgedUnits, bool Finished);
|
||||
|
||||
public sealed record ReplayObservation(JudgementSnapshot Judgement, double SetupMilliseconds,
|
||||
double ReplayMilliseconds, long SetupAllocatedBytes, long ReplayAllocatedBytes);
|
||||
|
||||
public static class ReplayRunner
|
||||
{
|
||||
public static ReplayObservation Run(SyntheticWorkload workload, int frameRate)
|
||||
{
|
||||
if (frameRate < 1 || frameRate > 10_000)
|
||||
throw new ArgumentOutOfRangeException(nameof(frameRate), "Use a frame rate from 1 to 10000.");
|
||||
var allocationStart = GC.GetAllocatedBytesForCurrentThread();
|
||||
var setupStart = Stopwatch.GetTimestamp();
|
||||
var engine = new RhythmJudgementEngine(workload.Notes, laneCount: 4);
|
||||
var setupEnd = Stopwatch.GetTimestamp();
|
||||
var replayAllocationStart = GC.GetAllocatedBytesForCurrentThread();
|
||||
|
||||
long frame = 0;
|
||||
foreach (var input in workload.Events)
|
||||
{
|
||||
// Merge two timestamped streams; no sleeps or real-time waiting affect this trace.
|
||||
while ((double)frame / frameRate <= input.Time)
|
||||
engine.Update((double)frame++ / frameRate);
|
||||
if (input.Pressed)
|
||||
engine.Press(input.Lane, input.Time);
|
||||
else
|
||||
engine.Release(input.Lane, input.Time);
|
||||
}
|
||||
engine.Update(workload.Notes[^1].EndTime + 1);
|
||||
var replayEnd = Stopwatch.GetTimestamp();
|
||||
var allocationEnd = GC.GetAllocatedBytesForCurrentThread();
|
||||
return new ReplayObservation(new JudgementSnapshot(engine.Score, engine.Combo, engine.MaxCombo,
|
||||
engine.MissCount, engine.KoolCount, engine.CoolCount, engine.GoodCount,
|
||||
engine.JudgedNoteCount, engine.IsFinished),
|
||||
Milliseconds(setupStart, setupEnd), Milliseconds(setupEnd, replayEnd),
|
||||
replayAllocationStart - allocationStart, allocationEnd - replayAllocationStart);
|
||||
}
|
||||
|
||||
private static double Milliseconds(long start, long end) =>
|
||||
(end - start) * 1000.0 / Stopwatch.Frequency;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using ProjectQuint.Core.Charts;
|
||||
|
||||
namespace ProjectQuint.Probes;
|
||||
|
||||
public readonly record struct ReplayInput(double Time, int Lane, bool Pressed);
|
||||
|
||||
public sealed record SyntheticWorkload(PlayableNote[] Notes, ReplayInput[] Events, uint Seed)
|
||||
{
|
||||
public static SyntheticWorkload Create(int noteCount, uint seed)
|
||||
{
|
||||
if (noteCount < 1 || noteCount > 100_000)
|
||||
throw new ArgumentOutOfRangeException(nameof(noteCount), "Use 1 to 100000 notes.");
|
||||
var notes = new PlayableNote[noteCount];
|
||||
var events = new List<ReplayInput>(noteCount * 2);
|
||||
var state = seed;
|
||||
for (var index = 0; index < noteCount; index++)
|
||||
{
|
||||
// Explicit integer PRNG makes the workload independent of System.Random versions.
|
||||
state = unchecked(state * 1664525u + 1013904223u);
|
||||
var time = 1.0 + index * 0.125;
|
||||
var longNote = index % 11 == 0;
|
||||
var note = new PlayableNote
|
||||
{
|
||||
Id = index + 1,
|
||||
Lane = index % 4,
|
||||
StartTime = time,
|
||||
EndTime = time + (longNote ? 0.375 : 0),
|
||||
};
|
||||
notes[index] = note;
|
||||
// Deliberate misses and early releases exercise failure as well as perfect play.
|
||||
if (index % 17 == 0)
|
||||
continue;
|
||||
var pressTime = time + ((int)(state % 7) - 3) * 0.01;
|
||||
events.Add(new ReplayInput(pressTime, note.Lane, true));
|
||||
events.Add(new ReplayInput(longNote
|
||||
? note.EndTime - (index % 23 == 0 ? 0.2 : 0)
|
||||
: pressTime + 0.02, note.Lane, false));
|
||||
}
|
||||
return new SyntheticWorkload(notes, events.OrderBy(e => e.Time)
|
||||
.ThenBy(e => e.Pressed).ThenBy(e => e.Lane).ToArray(), seed);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user