summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel/RumorBench.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Kernel/RumorBench.fs')
-rw-r--r--src/LivingVillage.Kernel/RumorBench.fs109
1 files changed, 109 insertions, 0 deletions
diff --git a/src/LivingVillage.Kernel/RumorBench.fs b/src/LivingVillage.Kernel/RumorBench.fs
new file mode 100644
index 0000000..145f92c
--- /dev/null
+++ b/src/LivingVillage.Kernel/RumorBench.fs
@@ -0,0 +1,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