summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel/RumorBench.fs
blob: 145f92c916b9fa0a8798a655734c657ca0e998ac (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
namespace LivingVillage.Kernel

open System
open System.Diagnostics
open LivingVillage.Kernel.Sim

/// Kernel 层 Rumor 长跑基准:把「聊天 → 生成/追溯传闻」这条路径单独拎出来,按
/// 模拟日推进 tick 并注入固定节奏的聊天,长期测量 Rumor 工作集随世界年龄的
/// 分配与耗时量级(与 `Sim.step` 的 NPC 快照分配解耦,便于隔离谣言子系统)。
///
/// 该基准只读、不改变任何模拟语义;所有输入由 `Config` 决定,无墙钟/`System.Random`,
/// 同一 `Config` 两次运行产生相同的工作集校验和。
module RumorBench =

    type Config =
        { Seed: uint64
          NpcCount: int
          Days: int64
          /// 每个模拟日注入的聊天次数(决定谣言产生速率)。
          ChatsPerDay: int }

    type Result =
        { Seed: uint64
          NpcCount: int
          Days: int64
          ChatsPerDay: int
          ChatCalls: int64
          RumorsCreated: int64
          RumorsRetained: int
          /// 保留工作集内各条目 Id 之和,用于证明同一 Config 的确定性。
          RetainedIdChecksum: int64
          AllocatedBytes: int64
          ElapsedMs: float }

    /// 任务默认口径:30 NPC × 60 模拟日 × 每日 2000 次聊天。
    let defaultConfig : Config =
        { Seed = 42UL
          NpcCount = 30
          Days = 60L
          ChatsPerDay = 2000 }

    /// 测试/快速核验口径(同样是 30 NPC,但只跑 3 天)。
    let quickConfig : Config =
        { defaultConfig with Days = 3L }

    let private resetParticipants (ids: NpcId list) (world: World) : World =
        let npcs = Array.copy world.Npcs
        for id in ids do
            let (NpcId index) = id
            if index >= 0 && index < npcs.Length then
                npcs.[index] <-
                    { npcs.[index] with
                        Mind = { npcs.[index].Mind with Action = Wander } }
        { world with Npcs = npcs }

    let private retainedChecksum (rumors: RumorEvent list) : int64 =
        rumors |> List.sumBy (fun rumor -> let (RumorId id) = rumor.Id in id)

    let run (config: Config) : Result =
        if config.NpcCount < 2 then invalidArg (nameof config) "NpcCount must be at least 2"
        if config.Days <= 0L then invalidArg (nameof config) "Days must be positive"
        if config.ChatsPerDay <= 0 then invalidArg (nameof config) "ChatsPerDay must be positive"
        if config.ChatsPerDay > 1000000 then invalidArg (nameof config) "ChatsPerDay is too large"
        let callsPerDay = int64 config.ChatsPerDay
        let ticksPerCall = max 1L (ticksPerDay / callsPerDay)
        let totalCalls = config.Days * callsPerDay
        let mutable world = Sim.initialWorldN config.Seed config.NpcCount
        let mutable created = 0L
        let mutable calls = 0L
        GC.Collect(2, GCCollectionMode.Forced, true, true)
        GC.WaitForPendingFinalizers()
        GC.Collect(2, GCCollectionMode.Forced, true, true)
        let allocatedBefore = GC.GetAllocatedBytesForCurrentThread()
        let stopwatch = Stopwatch.StartNew()
        for i in 0L .. totalCalls - 1L do
            let dayIndex = i / callsPerDay
            let within = i % callsPerDay
            let tick = dayIndex * ticksPerDay + within * ticksPerCall
            let narratorIndex = int (i % int64 config.NpcCount)
            let receiverIndex =
                int ((i + 1L + (i / int64 config.NpcCount) % int64 (config.NpcCount - 1)) % int64 config.NpcCount)
            let narrator = NpcId narratorIndex
            let receiver = NpcId receiverIndex
            world <-
                resetParticipants [ narrator; receiver ] { world with Tick = tick }
            match Sim.chat { Narrator = narrator; Receiver = receiver } world with
            | ChatSucceeded (rumor, next) ->
                world <- next
                calls <- calls + 1L
                if rumor.IsSome then created <- created + 1L
            | ChatRejected _ -> ()
        stopwatch.Stop()
        let allocatedAfter = GC.GetAllocatedBytesForCurrentThread()
        { Seed = config.Seed
          NpcCount = config.NpcCount
          Days = config.Days
          ChatsPerDay = config.ChatsPerDay
          ChatCalls = calls
          RumorsCreated = created
          RumorsRetained = world.Rumors.Length
          RetainedIdChecksum = retainedChecksum world.Rumors
          AllocatedBytes = allocatedAfter - allocatedBefore
          ElapsedMs = stopwatch.Elapsed.TotalMilliseconds }

    let format (result: Result) : string =
        sprintf
            "rumor_bench seed=%d npcs=%d days=%d chats_per_day=%d chat_calls=%d rumors_created=%d rumors_retained=%d retained_id_checksum=%d allocated_bytes=%d elapsed_ms=%.3f"
            result.Seed result.NpcCount result.Days result.ChatsPerDay result.ChatCalls result.RumorsCreated
            result.RumorsRetained result.RetainedIdChecksum result.AllocatedBytes result.ElapsedMs