summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 04:23:26 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 04:23:26 +0800
commitf681867692f575b078f46afaf07060faed457461 (patch)
tree4ab11638357891257703e55f5aa790868e902e3c /src
parent2d9e4d845019b6c8793633258a45b91e0358e0e9 (diff)
downloadliving-village-f681867692f575b078f46afaf07060faed457461.tar.gz
p59: Sim 多世界并存后台压测(内存/GC 增长曲线证据单,只读)
目标:量化「多世界并存 × 天数」的内存/GC 增长,收口 P49 范围外项。 口径:新增 Headless `--profile-multiworld <worlds> <days> <npcs> <sampleEveryDays> <seedBase>`, N 个 Sim 世界同时存活,每轮 Parallel.For 各推进 sampleEveryDays 天并采样一行 (worlds/events/rumors/annals/memory_entries/gc_live_mb/working_set_rss_mb/elapsed_s, 行尾 flush);未改 Kernel/Sim 任何语义(Sim.initialWorldN / Sim.step 原样调用)。 标定:1 世界 × 2 天探针 = 34.2s(与 overnight-status 的 34.6s 一致),据此定 N=10 × 20 天。 运行:独立 systemd --user transient unit living-village-multiworld.service (scripts/profile-multiworld.sh,StandardOutput=append: + exec 2>&1,state.json 记 pid/unit/log/commit/参数/起止/exit)。主运行 wall=1024.4s(17.1min < 40min),exit=0。 结论(artifacts/perf-multiworld/20260928T195839Z/README.md): - 随世界数近似线性,未超线性:N=1 vs N=10(同日 D=20)annals/memory_entries 恰 10×、 rumors 9.67×、gc_live 8.81×(每世界 1.23MB,共享运行时开销摊薄)。 - RSS 有界无泄漏:N=10 39→峰值123(day8)→稳定104~112MB;每 2 天一轮墙钟恒 101~105s。 原因是历史集合有界(rumorRetentionDays=3 / rumorCapacity=16384 / maxAnnalEntries=128 / Mind.Memory 上限),约 day4 后进入稳态。 - 两轮世界0 digest 一致(2364CB74…)→ 并存+并行不改变单世界语义。 - 数字未越界 → 明确「不修」;只读本 slice 不修 Kernel/Sim。 回归:Desktop 304/304 + Kernel 119/119;--performance-baseline ×3 digest 953775FAEB2F… 不变,performance_determinism=PASS。未 push。
Diffstat (limited to 'src')
-rw-r--r--src/LivingVillage.Desktop.Tests/M3BatchOutputTests.fs7
-rw-r--r--src/LivingVillage.Headless/BatchOutput.fs17
-rw-r--r--src/LivingVillage.Headless/Program.fs64
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