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