// // PerformanceBaselineTests.swift // bitchatTests // // Performance baselines for hot paths so regressions are measured, not // guessed. Uses XCTest `measure {}` (swift-testing has no equivalent) with // NO recorded baselines — these tests record timing in CI logs but never // fail on timing, so they cannot flake. // // Each benchmark builds its fixtures OUTSIDE the measure block with fixed // seeds/content so every iteration does deterministic work: no network, // no Tor, no sleeps. // // Skippable via the BITCHAT_SKIP_PERF_BASELINES=1 environment variable; // runs by default. // import XCTest import SwiftUI import BitFoundation @testable import bitchat @MainActor final class PerformanceBaselineTests: XCTestCase { override func setUpWithError() throws { try XCTSkipIf( ProcessInfo.processInfo.environment["BITCHAT_SKIP_PERF_BASELINES"] == "1", "Performance baselines skipped via BITCHAT_SKIP_PERF_BASELINES" ) } override func tearDown() { // Drain main-queue tasks spawned by coordinators (handlePublicMessage // hops etc.) so they don't bleed into the next test's measure block. RunLoop.current.run(until: Date().addingTimeInterval(0.05)) super.tearDown() } /// Reports one human-readable throughput line per benchmark so CI logs /// are readable without parsing XCTest's measure output. private func reportThroughput(_ name: String, samples: [TimeInterval], operations: Int, unit: String) { guard !samples.isEmpty else { return } let avg = samples.reduce(0, +) / Double(samples.count) let opsPerSec = avg > 0 ? Double(operations) / avg : .infinity print(String( format: "PERF[%@]: %.0f %@/sec (avg %.3f ms per pass of %d, %d passes)", name, opsPerSec, unit, avg * 1000, operations, samples.count )) } // MARK: - 1a. Nostr inbound event handling (fresh events) /// `NostrInboundPipeline.handleNostrEvent` for never-seen geo events /// (kind 20000): signature verification, dedup record, presence/nickname /// bookkeeping, and public-message ingest scheduling. func testNostrInboundEventHandling_freshEvents() throws { let events = try Self.makeSignedGeohashEvents(count: 500) // A fresh context per measure pass so every event takes the // first-seen path on every iteration. Kept alive so the weakly // captured Task hops stay valid. var keepAlive: [(PerfNostrContext, ChatNostrCoordinator)] = [] var samples: [TimeInterval] = [] measure { let context = PerfNostrContext() let coordinator = ChatNostrCoordinator(context: context) keepAlive.append((context, coordinator)) let start = Date() for event in events { coordinator.inbound.handleNostrEvent(event) } samples.append(Date().timeIntervalSince(start)) XCTAssertEqual(context.processedEventCount, events.count) } reportThroughput("nostrInbound.fresh", samples: samples, operations: events.count, unit: "events") } // MARK: - 1b. Nostr inbound event handling (duplicate events) /// The dedup-hit path: identical events replayed. Duplicates dominate /// real relay traffic (the same event arrives from several relays), so /// this path runs hundreds of times a minute in busy geohashes. Note it /// still pays full Schnorr signature verification before the dedup check. func testNostrInboundEventHandling_duplicateEvents() throws { let events = try Self.makeSignedGeohashEvents(count: 500) let context = PerfNostrContext() let coordinator = ChatNostrCoordinator(context: context) // Pre-warm: every event is now recorded as processed. for event in events { coordinator.inbound.handleNostrEvent(event) } XCTAssertEqual(context.processedEventCount, events.count) var samples: [TimeInterval] = [] measure { let start = Date() for event in events { coordinator.inbound.handleNostrEvent(event) } samples.append(Date().timeIntervalSince(start)) } // Dedup held: nothing was re-processed. XCTAssertEqual(context.processedEventCount, events.count) reportThroughput("nostrInbound.duplicate", samples: samples, operations: events.count, unit: "events") } // MARK: - 2. BLE inbound packet pipeline (decode + dedup) /// Binary encode/decode round trip plus `MessageDeduplicator` at /// realistic mesh sizes: 1000 packets with 100-300 byte payloads, each /// dedup-checked twice (first-seen insert, then duplicate hit) the way /// relayed packets arrive on multiple links. func testBLEInboundPacketPipeline() throws { var rng = SeededGenerator(seed: 0xB17C4A7) let baseTimestamp = UInt64(1_700_000_000_000) var packets: [BitchatPacket] = [] var dedupIDs: [String] = [] packets.reserveCapacity(1000) dedupIDs.reserveCapacity(1000) for index in 0..<1000 { let senderID = Data((0..<8).map { _ in UInt8.random(in: 0...255, using: &rng) }) let payloadSize = Int.random(in: 100...300, using: &rng) let payload = Data((0.. delivered so every call /// performs a real update (never the skip path). func testDeliveryStatusIncrementalUpdates() { let context = PerfDeliveryContext.makeCorpus(publicCount: 2000, peerCount: 50, messagesPerPeer: 40) let coordinator = ChatDeliveryCoordinator(context: context) // Warm the index so the measure block exercises the incremental path. XCTAssertTrue(coordinator.updateMessageDeliveryStatus(context.messages[0].id, status: .sent)) // 250 public + 250 private IDs spread across the corpus. var targetIDs: [String] = [] for i in stride(from: 0, to: 2000, by: 8) { targetIDs.append(context.messages[i].id) } let peerIDs = context.privateChats.keys.sorted { $0.id < $1.id } for (offset, peer) in peerIDs.enumerated() where offset < 25 { guard let chat = context.privateChats[peer] else { continue } for i in stride(from: 0, to: chat.count, by: 4) { targetIDs.append(chat[i].id) } } XCTAssertEqual(targetIDs.count, 500) let fixedDate = Date(timeIntervalSince1970: 1_700_000_000) var toggle = false var samples: [TimeInterval] = [] measure { toggle.toggle() let status: DeliveryStatus = toggle ? .delivered(to: "peer", at: fixedDate) : .sent let start = Date() var updated = 0 for id in targetIDs where coordinator.updateMessageDeliveryStatus(id, status: status) { updated += 1 } samples.append(Date().timeIntervalSince(start)) XCTAssertEqual(updated, targetIDs.count) } reportThroughput("delivery.incrementalUpdate", samples: samples, operations: targetIDs.count, unit: "updates") } // MARK: - 4b. Delivery location index (rebuild path) /// The expensive path: a non-append insertion (out-of-order arrival at /// the head of the timeline) invalidates the index, so the next status /// update triggers a full rebuild over ~4000 message locations. func testDeliveryStatusIndexRebuild() { let context = PerfDeliveryContext.makeCorpus(publicCount: 2000, peerCount: 50, messagesPerPeer: 40) let coordinator = ChatDeliveryCoordinator(context: context) // Warm the index before the first insertion invalidates it. XCTAssertTrue(coordinator.updateMessageDeliveryStatus(context.messages[0].id, status: .sent)) // Pre-built head insertions, one consumed per measure pass. let insertions: [BitchatMessage] = (0..<32).map { i in BitchatMessage( id: "insert-\(i)", sender: "alice", content: "out-of-order arrival \(i)", timestamp: Date(timeIntervalSince1970: 1_690_000_000), isRelay: false ) } let probeID = context.messages[1000].id let fixedDate = Date(timeIntervalSince1970: 1_700_000_000) var pass = 0 var toggle = false var samples: [TimeInterval] = [] measure { precondition(pass < insertions.count, "add more pre-built insertions") context.messages.insert(insertions[pass], at: 0) pass += 1 toggle.toggle() let status: DeliveryStatus = toggle ? .delivered(to: "peer", at: fixedDate) : .sent let start = Date() let updated = coordinator.updateMessageDeliveryStatus(probeID, status: status) samples.append(Date().timeIntervalSince(start)) XCTAssertTrue(updated) } reportThroughput("delivery.indexRebuild", samples: samples, operations: 1, unit: "rebuilds") } // MARK: - 5. Message formatting /// `MessageFormattingEngine.formatMessage` over 200 messages with /// mentions, hashtags, and URLs. Formatting caches per message, so each /// measure pass consumes a fresh pre-built batch (cache-miss path, which /// is the cost paid when messages first render). func testMessageFormatting() { let context = PerfFormattingContext(nickname: "carol") let batchCount = 16 // > XCTest's default 10 measure iterations let batchSize = 200 let contents: [(String, [String]?)] = [ ("hello mesh, anyone around tonight?", nil), ("@carol#a1b2 did you see this? https://example.com/threads/42", ["carol"]), ("checking in from the harbor #bitchat #mesh", nil), ("@bob#0042 ping me when you get this", ["bob#0042"]), ("long form update with a link https://news.example.org/articles/2026/06/mesh-networks and a tag #geohash", nil), ] let batches: [[BitchatMessage]] = (0.. [NostrEvent] { let senders = try (0..<8).map { _ in try NostrIdentity.generate() } let lines = [ "hello from the geohash", "anyone near the station?", "@bob#0042 are you on mesh too?", "check this out https://example.com/p/123 #bitchat", "teleport check, who's local?", ] return try (0.. UInt64 { state &+= 0x9E37_79B9_7F4A_7C15 var z = state z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9 z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB return z ^ (z >> 31) } } // MARK: - Mock ChatNostrContext /// Minimal `ChatNostrContext` for benchmarking the `ChatNostrCoordinator` /// stack (`NostrInboundPipeline` in particular) without a `ChatViewModel` /// (mirrors `ChatNostrCoordinatorContextTests`). /// Callbacks are cheap dictionary/array operations so the measured cost is /// the coordinator's own pipeline. @MainActor private final class PerfNostrContext: ChatNostrContext { var activeChannel: ChannelID = .location(GeohashChannel(level: .neighborhood, geohash: "u4pruyd")) var currentGeohash: String? = "u4pruyd" var geoSubscriptionID: String? var geoDmSubscriptionID: String? var geoSamplingSubs: [String: String] = [:] var lastGeoNotificationAt: [String: Date] = [:] var nostrRelayManager: NostrRelayManager? { nil } func setGeoChatSubscriptionID(_ id: String?) { geoSubscriptionID = id } func setGeoDmSubscriptionID(_ id: String?) { geoDmSubscriptionID = id } func addGeoSamplingSub(_ subID: String, forGeohash geohash: String) { geoSamplingSubs[subID] = geohash } func removeGeoSamplingSub(_ subID: String) { geoSamplingSubs.removeValue(forKey: subID) } func clearGeoSamplingSubs() -> [String] { defer { geoSamplingSubs.removeAll() } return Array(geoSamplingSubs.keys) } var messages: [BitchatMessage] = [] func resetPublicMessagePipeline() {} func updatePublicMessagePipelineChannel(_ channel: ChannelID) {} func refreshVisibleMessages(from channel: ChannelID?) {} func addPublicSystemMessage(_ content: String) {} func drainPendingGeohashSystemMessages() -> [String] { [] } func appendGeohashMessageIfAbsent(_ message: BitchatMessage, toGeohash geohash: String) -> Bool { true } func synchronizePublicConversationStore(forGeohash geohash: String) {} private(set) var handledPublicMessageCount = 0 func handlePublicMessage(_ message: BitchatMessage) { handledPublicMessageCount += 1 } func checkForMentions(_ message: BitchatMessage) {} func sendHapticFeedback(for message: BitchatMessage) {} func parseMentions(from content: String) -> [String] { MessageFormattingEngine.extractMentions(from: content) } var selectedPrivateChatPeer: PeerID? var nostrKeyMapping: [PeerID: String] = [:] func registerNostrKeyMapping(_ pubkey: String, for peerID: PeerID) { nostrKeyMapping[peerID] = pubkey } func handlePrivateMessage(_ payload: NoisePayload, senderPubkey: String, convKey: PeerID, id: NostrIdentity, messageTimestamp: Date) {} func handleDelivered(_ payload: NoisePayload, senderPubkey: String, convKey: PeerID) {} func handleReadReceipt(_ payload: NoisePayload, senderPubkey: String, convKey: PeerID) {} func startPrivateChat(with peerID: PeerID) {} private struct NoIdentity: Error {} var geohashIdentities: [String: NostrIdentity] = [:] func deriveNostrIdentity(forGeohash geohash: String) throws -> NostrIdentity { guard let identity = geohashIdentities[geohash] else { throw NoIdentity() } return identity } func currentNostrIdentity() -> NostrIdentity? { nil } func isNostrBlocked(pubkeyHexLowercased: String) -> Bool { false } func displayNameForNostrPubkey(_ pubkeyHex: String) -> String { "anon#\(pubkeyHex.prefix(4))" } private var processedNostrEventIDs: Set = [] var processedEventCount: Int { processedNostrEventIDs.count } func hasProcessedNostrEvent(_ eventID: String) -> Bool { processedNostrEventIDs.contains(eventID) } func recordProcessedNostrEvent(_ eventID: String) { processedNostrEventIDs.insert(eventID) } func clearProcessedNostrEvents() { processedNostrEventIDs.removeAll() } var geoNicknames: [String: String] = [:] private var teleportedKeys: Set = [] var teleportedGeoCount: Int { teleportedKeys.count } func startGeoParticipantRefreshTimer() {} func stopGeoParticipantRefreshTimer() {} func setActiveParticipantGeohash(_ geohash: String?) {} private(set) var participantRecords = 0 func recordGeoParticipant(pubkeyHex: String) { participantRecords += 1 } func recordGeoParticipant(pubkeyHex: String, geohash: String) { participantRecords += 1 } func geoParticipantCount(for geohash: String) -> Int { 0 } func setGeoNickname(_ nickname: String, forPubkey pubkeyHex: String) { geoNicknames[pubkeyHex.lowercased()] = nickname } func markGeoTeleported(_ pubkeyHexLowercased: String) { teleportedKeys.insert(pubkeyHexLowercased) } func clearGeoTeleported(_ pubkeyHexLowercased: String) { teleportedKeys.remove(pubkeyHexLowercased) } func clearTeleportedGeo() { teleportedKeys.removeAll() } func clearGeoNicknames() { geoNicknames.removeAll() } func visibleGeohashPeople() -> [GeoPerson] { [] } var isTeleported = false func isGeohashOutsideRegionalChannels(_ geohash: String) -> Bool { false } func routeFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {} func sendGeohashDeliveryAck(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {} func sendGeohashReadReceipt(_ messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {} } // MARK: - Mock ChatDeliveryContext @MainActor private final class PerfDeliveryContext: ChatDeliveryContext { var messages: [BitchatMessage] = [] var privateChats: [PeerID: [BitchatMessage]] = [:] var sentReadReceipts: Set = [] var isStartupPhase: Bool { false } func notifyUIChanged() {} func markMessageDelivered(_ messageID: String) {} func pruneSentReadReceipts(keeping validMessageIDs: Set) -> Int { let oldCount = sentReadReceipts.count sentReadReceipts = sentReadReceipts.intersection(validMessageIDs) return oldCount - sentReadReceipts.count } /// 2000 public + `peerCount` x `messagesPerPeer` private messages with /// deterministic IDs and timestamps. static func makeCorpus(publicCount: Int, peerCount: Int, messagesPerPeer: Int) -> PerfDeliveryContext { let context = PerfDeliveryContext() let base = Date(timeIntervalSince1970: 1_700_000_000) context.messages = (0.. Bool { false } func senderColor(for message: BitchatMessage, isDark: Bool) -> Color { .blue } func peerURL(for peerID: PeerID) -> URL? { URL(string: "bitchat://peer/\(peerID.id)") } }