From e999d57c19abd0953762249532778926c3959ba1 Mon Sep 17 00:00:00 2001 From: ecgang Date: Sun, 2 Aug 2026 10:22:45 -0700 Subject: [PATCH] Count the simulated-mesh storm bound per packet type `publicMessageRelaysAcrossLineTopologyWithinTTLBudget` bounded every frame the mesh delivered, announces included. Announce *admission* is the one decision the manual scheduler does not own: `sendAnnounceNow` asks `announceThrottle.shouldSend(force:now: Date())` (BLEService.swift:2760), so announce volume tracks real elapsed seconds rather than `advanceTime`, and the forced-announce window is only 0.15s. #1651 has since made that harmless here, by draining the mesh to quiet before the baseline is taken: the announce traffic now lands in the baseline instead of in the measured delta. Re-measured on top of it, burning 0.2s of wall clock and changing nothing else: burn baseline total/announce delta total/announce/message none 66 / 22 4 / 0 / 4 0.2s early 80 / 36 4 / 0 / 4 0.2s late 66 / 22 4 / 0 / 4 The burn moves the baseline and leaves the delta alone. So the flake is fixed, and this is not the fix. What is left is legibility. The assertion now names the traffic it bounds, so a later change on the announce path cannot spend the message budget's headroom unnoticed. The message-only cost is 4 and is invariant under the perturbation above, so the budget is 8. Full SimulatedMeshTests suite green (macOS scheme, 8/8). Co-Authored-By: Claude Opus 5 (1M context) --- bitchatTests/Simulation/SimulatedMesh.swift | 24 +++++++++++++++++++ .../Simulation/SimulatedMeshTests.swift | 20 ++++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/bitchatTests/Simulation/SimulatedMesh.swift b/bitchatTests/Simulation/SimulatedMesh.swift index 6caf379d..c4bf6312 100644 --- a/bitchatTests/Simulation/SimulatedMesh.swift +++ b/bitchatTests/Simulation/SimulatedMesh.swift @@ -29,6 +29,29 @@ final class SimulatedMesh { private var pendingDeliveries: [(from: Int, packet: BitchatPacket)] = [] /// Total (packet, receiving-node) deliveries pumped — the storm bound. private(set) var deliveredFrameCount = 0 + /// The same deliveries split by packet type. A whole-mesh frame budget + /// silently includes announce traffic, and announce *admission* is the one + /// decision the manual scheduler does not own: `sendAnnounceNow` asks + /// `announceThrottle.shouldSend(force:now: Date())` (BLEService), so + /// whether a scheduled re-announce reaches the wire depends on real + /// elapsed seconds, not on `advanceTime`. Burn 0.2s of wall clock in the + /// middle of a test — the forced-announce window is 0.15s — and announce + /// deliveries rise (measured: 22 → 36) while every other type is + /// unchanged. A test that drains the mesh to quiet before it measures + /// absorbs that into its baseline; one that cannot has a budget moving + /// with runner load. Counting per type lets a test bound the traffic it + /// is actually about either way. + private var deliveredFrameCountsByType: [UInt8: Int] = [:] + + /// Deliveries of one packet type — the storm bound for a specific kind of + /// traffic, immune to unrelated announce frames. + /// Unlocked deliberately, matching `deliveredFrameCount` above: both + /// counters are written only by `pump` on the test thread (the outbound + /// tap touches `pendingDeliveries`/`emitted`, never these). Taking the + /// lock here would advertise a protection the writer does not hold. + func deliveredFrameCount(ofType type: MessageType) -> Int { + deliveredFrameCountsByType[type.rawValue] ?? 0 + } private(set) var nodes: [Node] = [] private var neighbors: [Set] = [] @@ -168,6 +191,7 @@ final class SimulatedMesh { for receiver in neighbors[from] { for link in links(from: from, at: receiver) { deliveredFrameCount += 1 + deliveredFrameCountsByType[packet.type, default: 0] += 1 nodes[receiver].service._test_ingestFrame(packet, link: link) } } diff --git a/bitchatTests/Simulation/SimulatedMeshTests.swift b/bitchatTests/Simulation/SimulatedMeshTests.swift index b4630076..512165d8 100644 --- a/bitchatTests/Simulation/SimulatedMeshTests.swift +++ b/bitchatTests/Simulation/SimulatedMeshTests.swift @@ -63,7 +63,8 @@ struct SimulatedMeshTests { // baseline snapshot depends on the draw — the budget assertion below // flaked on CI at 14 and 18 frames for exactly that reason. Advance // until the mesh goes a full window with no new frames, so the - // baseline only ever measures the message under test. + // baseline only ever measures the message under test. Quiescence is + // judged on every frame; the baseline itself is taken per type. var settled = mesh.deliveredFrameCount for _ in 0..<20 { mesh.advanceTime(by: 2) @@ -71,7 +72,7 @@ struct SimulatedMeshTests { if now == settled { break } settled = now } - let baseline = mesh.deliveredFrameCount + let baseline = mesh.deliveredFrameCount(ofType: .message) let capture = TransportEventCapture() c.service.eventDelegate = capture @@ -84,9 +85,18 @@ struct SimulatedMeshTests { let arrived = await capture.drainedPublicMessageCount(content: "hello line") == 1 #expect(arrived) - // Storm bound: a single public message across one relay hop must - // not multiply into more than a handful of frames. - #expect(mesh.deliveredFrameCount - baseline <= 12) + // Storm bound: a single public message across one relay hop must not + // multiply into more than a handful of frames. Counted per type so + // the budget measures relay fan-out and nothing else. The whole-mesh + // total also carries announces, and announce admission is the one + // decision the manual scheduler does not own — `sendAnnounceNow` asks + // `announceThrottle.shouldSend(force:now: Date())` — so announce + // volume tracks real elapsed seconds. The settle loop above keeps + // that traffic in the baseline rather than in the delta: burning 0.2s + // of wall clock before it moves the baseline 66 → 80 frames and + // leaves this delta at 4. Per-type counting is what makes the + // assertion say which traffic it bounds. + #expect(mesh.deliveredFrameCount(ofType: .message) - baseline <= 8) } @Test