summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Headless/PerformanceProbe.fs
blob: 4641fb4e83f428728cd73e575ae93e6fe5452b1b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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