diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/M3BatchOutputTests.fs | 7 | ||||
| -rw-r--r-- | src/LivingVillage.Headless/BatchOutput.fs | 17 | ||||
| -rw-r--r-- | src/LivingVillage.Headless/Program.fs | 64 |
3 files changed, 87 insertions, 1 deletions
diff --git a/src/LivingVillage.Desktop.Tests/M3BatchOutputTests.fs b/src/LivingVillage.Desktop.Tests/M3BatchOutputTests.fs index b1822c7..963d571 100644 --- a/src/LivingVillage.Desktop.Tests/M3BatchOutputTests.fs +++ b/src/LivingVillage.Desktop.Tests/M3BatchOutputTests.fs @@ -32,6 +32,13 @@ type M3BatchOutputTests () = Assert.AreEqual<string>("[world 0] progress day=15/15 elapsed=260.0s", BatchOutput.progressLine 0 15L 15L 260.0) [<TestMethod>] + member _.MultiworldLineReportsCountsGcRssAndElapsed () = + let line = BatchOutput.multiworldLine 8 4L 20L 1600L 32000L 64L 5120L (1048576L * 96L) (1048576L * 240L) 31.5 + Assert.AreEqual<string>( + "multiworld_sample worlds=8 day=4/20 events=1600 rumors=32000 annals=64 memory_entries=5120 gc_live_mb=96.00 working_set_rss_mb=240.00 elapsed_s=31.5", + line) + + [<TestMethod>] member _.SummaryLineKeepsOldFieldsAndAppendsWallClock () = let line = BatchOutput.summaryLine 4 3 1 4 1.100 9.900 0.010 0.400 4 610.5 // 监督器正则只读前三个字段,前缀必须逐字保持。 diff --git a/src/LivingVillage.Headless/BatchOutput.fs b/src/LivingVillage.Headless/BatchOutput.fs index 971da97..aa79391 100644 --- a/src/LivingVillage.Headless/BatchOutput.fs +++ b/src/LivingVillage.Headless/BatchOutput.fs @@ -42,6 +42,23 @@ module BatchOutput = "batch_summary worlds=%d passed=%d failed=%d old_passed=%d/%d ratio_min=%.3f ratio_max=%.3f gini_min=%.3f gini_max=%.3f elapsed_s=%.1f workers=%d wall_s=%.1f" worlds passed failed oldPassed worlds ratioMin ratioMax giniMin giniMax elapsedSeconds workers elapsedSeconds + /// 多世界并存压测采样行:worlds/day 与各历史集合计数 + GC live set + OS RSS + 墙钟。 + let multiworldLine + (worlds: int) + (day: int64) + (totalDays: int64) + (events: int64) + (rumors: int64) + (annals: int64) + (memoryEntries: int64) + (gcLiveBytes: int64) + (rssBytes: int64) + (elapsedSeconds: float) : string = + let mb (bytes: int64) = float bytes / (1024.0 * 1024.0) + sprintf + "multiworld_sample worlds=%d day=%d/%d events=%d rumors=%d annals=%d memory_entries=%d gc_live_mb=%.2f working_set_rss_mb=%.2f elapsed_s=%.1f" + worlds day totalDays events rumors annals memoryEntries (mb gcLiveBytes) (mb rssBytes) elapsedSeconds + /// --cost-probe 单段输出:ticks/s、墙钟、分配与 GC 全部来自真实测量。 let costLine (days: int64) diff --git a/src/LivingVillage.Headless/Program.fs b/src/LivingVillage.Headless/Program.fs index 4bd6424..c9dd442 100644 --- a/src/LivingVillage.Headless/Program.fs +++ b/src/LivingVillage.Headless/Program.fs @@ -612,6 +612,66 @@ let runProfileLongRun (argv: string[]) : int = Console.Out.Flush() if world.Tick = days * ticksPerDay then 0 else 1 +/// `--profile-multiworld [worlds] [days] [npcs] [sampleEveryDays] [seedBase]`: +/// N 个 Sim 世界并存、按采样轮并行推进 D 天(只读 profile,不改 Kernel/Sim 语义)。 +/// 每 `sampleEveryDays` 天打印一行流式采样(worlds/count、events、rumors、annals、 +/// memory_entries、gc_live_mb、working_set_rss_mb、elapsed_s)并 flush;行的聚合口径为 +/// 全部并存世界之和,ground truth 是 GC live set 与 OS WorkingSet。 +let runProfileMultiworld (argv: string[]) : int = + let argInt64 index (dflt: int64) = + if argv.Length > index then + match Int64.TryParse argv.[index] with + | true, v when v > 0L -> v + | _ -> dflt + else dflt + let worlds = int (argInt64 1 10L) + let days = argInt64 2 20L + let npcs = int (argInt64 3 (int64 Sim.npcCount)) + let sampleEvery = argInt64 4 2L + let seedBase = if argv.Length > 5 then (match UInt64.TryParse argv.[5] with | true, v -> v | _ -> 42UL) else 42UL + let proc = System.Diagnostics.Process.GetCurrentProcess() + printfn "multiworld_start worlds=%d days=%d npcs=%d sample_every_days=%d seed_base=%d ticks_per_day=%d" + worlds days npcs sampleEvery seedBase ticksPerDay + Console.Out.Flush() + let ws = Array.init worlds (fun i -> Sim.initialWorldN (seedBase + uint64 i) npcs) + let drivers = Array.init worlds (fun i -> { Rng = Rng.ofSeed (seedBase + uint64 i); Until = segmentTicks; Input = zeroInput }) + let sw = System.Diagnostics.Stopwatch.StartNew() + let advanceOne (i: int) (targetDay: int64) = + let mutable world = ws.[i] + let mutable driver = drivers.[i] + let dayEnd = targetDay * ticksPerDay + while world.Tick < dayEnd do + let d, input = + if world.Tick < driver.Until then driver, driver.Input + else + let d = nextSegment driver.Rng driver.Until + d, d.Input + driver <- d + world <- Sim.step { Input = input } world + ws.[i] <- world + drivers.[i] <- driver + let sample (day: int64) = + let gcLive = System.GC.GetTotalMemory true + proc.Refresh() + let rss = proc.WorkingSet64 + let events = ws |> Array.sumBy (fun w -> int64 w.Events.Length) + let rumors = ws |> Array.sumBy (fun w -> int64 w.Rumors.Length) + let annals = ws |> Array.sumBy (fun w -> int64 w.Annals.Length) + let memEntries = ws |> Array.sumBy (fun w -> int64 (w.Npcs |> Array.sumBy (fun n -> n.Mind.Memory.Length))) + printfn "%s" (LivingVillage.Headless.BatchOutput.multiworldLine worlds day days events rumors annals memEntries gcLive rss sw.Elapsed.TotalSeconds) + Console.Out.Flush() + sample 0L + let mutable dayNo = 0L + while dayNo < days do + let target = min days (dayNo + sampleEvery) + System.Threading.Tasks.Parallel.For(0, worlds, fun i -> advanceOne i target) |> ignore + dayNo <- target + sample dayNo + let digests = ws |> Array.mapi (fun i w -> sprintf "%d:%s" i (LivingVillage.Headless.PerformanceProbe.worldDigest w)) |> String.concat "," + printfn "multiworld_done worlds=%d day=%d elapsed_s=%.1f world_digests=%s" worlds dayNo sw.Elapsed.TotalSeconds digests + Console.Out.Flush() + if ws |> Array.forall (fun w -> w.Tick = days * ticksPerDay) then 0 else 1 + let runExisting (argv: string[]) = let rec parse (i: int) @@ -675,7 +735,7 @@ let runExisting (argv: string[]) = if stats.NonFinite > 0L || stats.OutOfBounds > 0L || not replayOk then 1 else 0 | Error msg -> eprintfn $"headless: {msg}" - eprintfn "usage: dotnet run -c Release --project src/LivingVillage.Headless -- --days N --seed S [--npc N] [--dump-relations] [--dump-rumors] [--replay-rumors] | --m5-smoke | --m6a-smoke | --batch K D | --performance-baseline | --cost-probe [D1,D2,...] | --rumor-bench [days npcs chats_per_day]" + eprintfn "usage: dotnet run -c Release --project src/LivingVillage.Headless -- --days N --seed S [--npc N] [--dump-relations] [--dump-rumors] [--replay-rumors] | --m5-smoke | --m6a-smoke | --batch K D | --performance-baseline | --cost-probe [D1,D2,...] | --rumor-bench [days npcs chats_per_day] | --profile-long-run [days sampleEveryDays npcs seed] | --profile-multiworld [worlds days npcs sampleEveryDays seedBase]" 2 let private parseCostLadder (argv: string[]) : int64 list = @@ -706,5 +766,7 @@ let main argv = runRumorBench argv elif argv.Length >= 1 && argv.[0] = "--profile-long-run" then runProfileLongRun argv + elif argv.Length >= 1 && argv.[0] = "--profile-multiworld" then + runProfileMultiworld argv else runExisting argv |
