diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-20 19:32:52 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-20 19:32:52 +0800 |
| commit | 6f904d6f8528bd13941894af3db16d0bc3b0aeb3 (patch) | |
| tree | edb64713930b99d9e3e9daf2c7c9dcba411f60b9 /src/LivingVillage.Headless/PerformanceProbe.fs | |
| parent | 7a7f14a1a036801b6d27953f6edc4e4a2f7b2efb (diff) | |
| download | living-village-6f904d6f8528bd13941894af3db16d0bc3b0aeb3.tar.gz | |
perf: add simulation baseline probe
Diffstat (limited to 'src/LivingVillage.Headless/PerformanceProbe.fs')
| -rw-r--r-- | src/LivingVillage.Headless/PerformanceProbe.fs | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/src/LivingVillage.Headless/PerformanceProbe.fs b/src/LivingVillage.Headless/PerformanceProbe.fs new file mode 100644 index 0000000..4641fb4 --- /dev/null +++ b/src/LivingVillage.Headless/PerformanceProbe.fs @@ -0,0 +1,214 @@ +namespace LivingVillage.Headless + +open System +open System.Diagnostics +open System.Runtime.InteropServices +open System.Security.Cryptography +open System.Text +open LivingVillage.Kernel +open LivingVillage.Kernel.Sim + +/// 性能基线的固定输入配置。 +/// +/// 所有 tick 数均为模拟 tick;输入序列由本模块按固定八步模式生成,不依赖墙上时钟、 +/// 全局随机数或线程局部状态。`WarmupTicks` 不计入样本,`MeasureTicks` 只计量 +/// `Sim.step` 调用循环,`Repetitions` 由命令入口控制重复次数。 +type PerformanceConfiguration = + { Seed: uint64 + NpcCount: int + WarmupTicks: int + MeasureTicks: int + Repetitions: int } + +/// 一次性能样本及其完整测量口径。 +/// +/// `ElapsedStopwatchTicks` 使用 `Stopwatch.Frequency` 解释;分配量是当前测量线程的 +/// `GC.GetAllocatedBytesForCurrentThread` 增量;GC 次数是测量前后进程计数器的差值。 +/// `FinalDigest` 只用于证明重复运行的最终快照一致,不是安全校验和。 +type PerformanceSample = + { Seed: uint64 + NpcCount: int + WarmupTicks: int + MeasureTicks: int + ElapsedStopwatchTicks: int64 + StopwatchFrequency: int64 + AllocatedBytes: int64 + Gen0Collections: int64 + Gen1Collections: int64 + Gen2Collections: int64 + FinalTick: int64 + FinalDigest: string + RuntimeDescription: string + RuntimeVersion: string } + +module PerformanceProbe = + + /// 固定的基线配置;本次 Task 0 的原始证据必须使用此配置或明确记录覆盖值。 + let defaultConfiguration : PerformanceConfiguration = + { Seed = 42UL + NpcCount = 4 + WarmupTicks = 120 + MeasureTicks = 6000 + Repetitions = 3 } + + /// 验证探针配置。失败返回中文/英文混合的稳定字段名,便于命令行和测试定位。 + let validateConfiguration (configuration: PerformanceConfiguration) : Result<unit, string> = + if configuration.NpcCount <= 0 then + Error "NpcCount must be greater than zero" + elif configuration.NpcCount > 1000 then + Error "NpcCount must be at most 1000" + elif configuration.WarmupTicks < 0 then + Error "WarmupTicks must not be negative" + elif configuration.MeasureTicks <= 0 then + Error "MeasureTicks must be greater than zero" + elif configuration.Repetitions <= 0 then + Error "Repetitions must be greater than zero" + else + Ok () + + let private requireValid (configuration: PerformanceConfiguration) : unit = + match validateConfiguration configuration with + | Ok () -> () + | Error message -> invalidArg (nameof configuration) message + + let private inputAt (index: int) : Input = + let diagonal = 0.70710678f + match index % 8 with + | 0 -> { MoveX = 1.0f; MoveY = 0.0f } + | 1 -> { MoveX = diagonal; MoveY = diagonal } + | 2 -> { MoveX = 0.0f; MoveY = 1.0f } + | 3 -> { MoveX = -diagonal; MoveY = diagonal } + | 4 -> { MoveX = -1.0f; MoveY = 0.0f } + | 5 -> { MoveX = -diagonal; MoveY = -diagonal } + | 6 -> { MoveX = 0.0f; MoveY = -1.0f } + | _ -> { MoveX = diagonal; MoveY = -diagonal } + + let private inputSequence (configuration: PerformanceConfiguration) : Input[] = + let count = max 1 (configuration.WarmupTicks + configuration.MeasureTicks) + Array.init count inputAt + + /// 运行固定输入序列并返回最终 World 快照。 + /// + /// 输入:合法的 `PerformanceConfiguration`;单位为模拟 tick,NPC 数为实体数量。 + /// 输出:包含预热和测量 tick 的最终快照。函数只使用局部 `world` 和输入数组, + /// 不修改调用方快照,也不写入正式 headless 批验状态。`World.Npcs` 只读;每次 + /// `Sim.step` 返回的新数组视为逻辑快照,step 内部的 `Array.copy`、嵌套 struct + /// 复制和 `ResizeArray` 暂存只在该 tick 生命周期内有效。非法配置抛出 + /// `ArgumentException`;调用方可先用 `validateConfiguration` 获取 `Result`。 + let runSteps (configuration: PerformanceConfiguration) : World = + requireValid configuration + let inputs = inputSequence configuration + let totalTicks = configuration.WarmupTicks + configuration.MeasureTicks + let mutable world = Sim.initialWorldN configuration.Seed configuration.NpcCount + for tick in 0 .. totalTicks - 1 do + world <- Sim.step { Input = inputs.[tick % inputs.Length] } world + world + + let private runStepsFrom + (inputs: Input[]) + (startIndex: int) + (steps: int) + (world: World) + : World = + let mutable next = world + if steps > 0 then + for offset in 0 .. steps - 1 do + next <- Sim.step { Input = inputs.[(startIndex + offset) % inputs.Length] } next + next + + /// 为最终快照生成稳定摘要;摘要计算发生在计时区间之外。 + /// + /// `WorldSave.save` 提供完整且有序的快照序列化,SHA-256 只压缩输出长度, + /// 不参与模拟和性能计时;若存档格式改变,摘要也应随之改变并触发回归。 + let worldDigest (world: World) : string = + let encoded = WorldSave.save world + use sha = SHA256.Create() + encoded + |> Encoding.UTF8.GetBytes + |> sha.ComputeHash + |> Convert.ToHexString + + /// 对一个固定配置执行预热和单次测量。 + /// + /// 计时区间只包含从预热后快照开始的 `MeasureTicks` 次 `Sim.step`;输入数组、 + /// 初始世界、预热和 GC 归零动作均在计时区间外。由于 GC 计数器是进程级计数器, + /// 该探针应以单进程、单测量线程运行;输出必须连同运行时和配置保存。 + let measure (configuration: PerformanceConfiguration) : PerformanceSample = + requireValid configuration + let inputs = inputSequence configuration + let initial = Sim.initialWorldN configuration.Seed configuration.NpcCount + let warmed = runStepsFrom inputs 0 configuration.WarmupTicks initial + + GC.Collect(2, GCCollectionMode.Forced, true, true) + GC.WaitForPendingFinalizers() + GC.Collect(2, GCCollectionMode.Forced, true, true) + + let allocatedBefore = GC.GetAllocatedBytesForCurrentThread() + let gen0Before = GC.CollectionCount(0) + let gen1Before = GC.CollectionCount(1) + let gen2Before = GC.CollectionCount(2) + let stopwatch = Stopwatch.StartNew() + let finalWorld = runStepsFrom inputs configuration.WarmupTicks configuration.MeasureTicks warmed + stopwatch.Stop() + let allocatedAfter = GC.GetAllocatedBytesForCurrentThread() + + { Seed = configuration.Seed + NpcCount = configuration.NpcCount + WarmupTicks = configuration.WarmupTicks + MeasureTicks = configuration.MeasureTicks + ElapsedStopwatchTicks = stopwatch.ElapsedTicks + StopwatchFrequency = Stopwatch.Frequency + AllocatedBytes = allocatedAfter - allocatedBefore + Gen0Collections = int64 (GC.CollectionCount(0) - gen0Before) + Gen1Collections = int64 (GC.CollectionCount(1) - gen1Before) + Gen2Collections = int64 (GC.CollectionCount(2) - gen2Before) + FinalTick = finalWorld.Tick + FinalDigest = worldDigest finalWorld + RuntimeDescription = RuntimeInformation.FrameworkDescription + RuntimeVersion = Environment.Version.ToString() } + + /// 将单次样本格式化为可直接保存的完整 key/value 行;不隐藏实际测量数字。 + let formatSample (sample: PerformanceSample) : string = + let elapsedSeconds = + if sample.StopwatchFrequency > 0L then + float sample.ElapsedStopwatchTicks / float sample.StopwatchFrequency + else + 0.0 + let throughput = + if elapsedSeconds > 0.0 then float sample.MeasureTicks / elapsedSeconds else 0.0 + sprintf + "seed=%d npc_count=%d warmup_ticks=%d measure_ticks=%d elapsed_stopwatch_ticks=%d stopwatch_frequency=%d elapsed_seconds=%.9f ticks_per_second=%.3f allocated_bytes=%d gen0=%d gen1=%d gen2=%d final_tick=%d final_digest=%s runtime=%s runtime_version=%s" + sample.Seed sample.NpcCount sample.WarmupTicks sample.MeasureTicks + sample.ElapsedStopwatchTicks sample.StopwatchFrequency elapsedSeconds throughput + sample.AllocatedBytes sample.Gen0Collections sample.Gen1Collections sample.Gen2Collections + sample.FinalTick sample.FinalDigest sample.RuntimeDescription sample.RuntimeVersion + + /// 执行默认三次重复测量并打印配置、样本和确定性结论。 + /// + /// 返回 0 表示所有重复样本的最终摘要一致;返回 1 表示摘要不一致;非法配置 + /// 不会静默继续。该入口只由显式 `--performance-baseline` 调用,不改变 `--batch`。 + let runDefault () : int = + let configuration = defaultConfiguration + match validateConfiguration configuration with + | Error message -> + eprintfn "performance_baseline=INVALID configuration=%s" message + 2 + | Ok () -> + printfn + "performance_config seed=%d npc_count=%d warmup_ticks=%d measure_ticks=%d repetitions=%d input_pattern=8-direction-cyclic" + configuration.Seed configuration.NpcCount configuration.WarmupTicks configuration.MeasureTicks + configuration.Repetitions + printfn "performance_runtime framework=%s version=%s os=%s architecture=%s stopwatch_frequency=%d" + RuntimeInformation.FrameworkDescription (Environment.Version.ToString()) + RuntimeInformation.OSDescription (RuntimeInformation.ProcessArchitecture.ToString()) Stopwatch.Frequency + let mutable firstDigest = None + let mutable deterministic = true + for repetition in 1 .. configuration.Repetitions do + let sample = measure configuration + printfn "performance_sample repetition=%d %s" repetition (formatSample sample) + match firstDigest with + | None -> firstDigest <- Some sample.FinalDigest + | Some digest when digest <> sample.FinalDigest -> deterministic <- false + | Some _ -> () + printfn "performance_determinism=%s" (if deterministic then "PASS" else "FAIL") + if deterministic then 0 else 1 |
