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 = 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 只压缩输出长度, /// 不参与模拟和性能计时;若存档格式改变,摘要也应随之改变并触发回归。 /// Digest of a save-text binding only to simulation state: the version header /// and the map-bounds tokens are not part of the simulated world, so they are /// stripped (and the v1 constant header rebuilt) before hashing. V1/V2 saves of /// the same world therefore share one digest, and the legacy baseline digest /// (hash over "LV_WORLD_SAVE_V1|") is preserved byte-for-byte. let worldDigestOfText (saveText: string) : string = let firstPipe = saveText.IndexOf('|') if firstPipe <= 0 then use sha = SHA256.Create() (saveText: string) |> Encoding.UTF8.GetBytes |> sha.ComputeHash |> Convert.ToHexString else let version = saveText.Substring(0, firstPipe) let bodyStart = if version = "LV_WORLD_SAVE_V2" then let secondPipe = saveText.IndexOf('|', firstPipe + 1) if secondPipe < 0 then saveText.Length else let thirdPipe = saveText.IndexOf('|', secondPipe + 1) if thirdPipe < 0 then saveText.Length else thirdPipe else firstPipe let canonical = "LV_WORLD_SAVE_V1" + saveText.Substring(bodyStart) use sha = SHA256.Create() canonical |> Encoding.UTF8.GetBytes |> sha.ComputeHash |> Convert.ToHexString let WorldDigestOfText (saveText: string) : string = worldDigestOfText saveText let worldDigest (world: World) : string = worldDigestOfText (WorldSave.save world) /// 对一个固定配置执行预热和单次测量。 /// /// 计时区间只包含从预热后快照开始的 `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