// // GameDAGRouterScaleTests.swift // SportsTimeTests // // Phase 3: GameDAGRouter Scale & Performance Tests // Stress tests for large datasets. May run for extended periods. // import Testing import Foundation @testable import SportsTime @Suite("GameDAGRouter Scale & Performance Tests") struct GameDAGRouterScaleTests { // MARK: - 3A: Scale Tests @Test("3.1 - 5 games completes within 5 minutes") func test_findRoutes_5Games_CompletesWithin5Minutes() async throws { let config = FixtureGenerator.Configuration( seed: 31, gameCount: 5, stadiumCount: 5, teamCount: 5, geographicSpread: .regional ) let data = FixtureGenerator.generate(with: config) let startTime = Date() let routes = GameDAGRouter.findRoutes( games: data.games, stadiums: data.stadiumsById, constraints: .default ) let elapsed = Date().timeIntervalSince(startTime) #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes") #expect(!routes.isEmpty, "Should produce at least one route") // Verify route validity for route in routes { #expect(!route.isEmpty, "Routes should not be empty") for i in 0..<(route.count - 1) { #expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered") } } print("3.1 - 5 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s") } @Test("3.2 - 50 games completes within 5 minutes") func test_findRoutes_50Games_CompletesWithin5Minutes() async throws { let config = FixtureGenerator.Configuration( seed: 32, gameCount: 50, stadiumCount: 15, teamCount: 15, geographicSpread: .regional ) let data = FixtureGenerator.generate(with: config) let startTime = Date() let routes = GameDAGRouter.findRoutes( games: data.games, stadiums: data.stadiumsById, constraints: .default ) let elapsed = Date().timeIntervalSince(startTime) #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes") #expect(!routes.isEmpty, "Should produce routes") // Verify route validity for route in routes { for i in 0..<(route.count - 1) { #expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered") } } print("3.2 - 50 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s") } @Test("3.3 - 500 games completes within 5 minutes") func test_findRoutes_500Games_CompletesWithin5Minutes() async throws { let config = FixtureGenerator.Configuration.medium // 500 games let data = FixtureGenerator.generate(with: config) let startTime = Date() let routes = GameDAGRouter.findRoutes( games: data.games, stadiums: data.stadiumsById, constraints: .default ) let elapsed = Date().timeIntervalSince(startTime) #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes") #expect(!routes.isEmpty, "Should produce routes") // Verify route validity for route in routes { for i in 0..<(route.count - 1) { #expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered") } } print("3.3 - 500 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s") // Record baseline if not set if TestConstants.baseline500Games == 0 { print("BASELINE 500 games: \(elapsed)s (record this in TestConstants)") } } @Test("3.4 - 2000 games completes within 5 minutes") func test_findRoutes_2000Games_CompletesWithin5Minutes() async throws { let config = FixtureGenerator.Configuration.large // 2000 games let data = FixtureGenerator.generate(with: config) let startTime = Date() let routes = GameDAGRouter.findRoutes( games: data.games, stadiums: data.stadiumsById, constraints: .default ) let elapsed = Date().timeIntervalSince(startTime) #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes") #expect(!routes.isEmpty, "Should produce routes") // Verify route validity for route in routes { for i in 0..<(route.count - 1) { #expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered") } } print("3.4 - 2000 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s") // Record baseline if not set if TestConstants.baseline2000Games == 0 { print("BASELINE 2000 games: \(elapsed)s (record this in TestConstants)") } } @Test("3.5 - 10000 games completes within 5 minutes") func test_findRoutes_10000Games_CompletesWithin5Minutes() async throws { let config = FixtureGenerator.Configuration.stress // 10000 games let data = FixtureGenerator.generate(with: config) let startTime = Date() let routes = GameDAGRouter.findRoutes( games: data.games, stadiums: data.stadiumsById, constraints: .default ) let elapsed = Date().timeIntervalSince(startTime) #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes") #expect(!routes.isEmpty, "Should produce routes") // Verify route validity for route in routes { for i in 0..<(route.count - 1) { #expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered") } } print("3.5 - 10000 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s") // Record baseline if not set if TestConstants.baseline10000Games == 0 { print("BASELINE 10000 games: \(elapsed)s (record this in TestConstants)") } } @Test("3.6 - 50000 nodes completes within 5 minutes") func test_findRoutes_50000Nodes_CompletesWithin5Minutes() async throws { // Extreme stress test - 50000 games let config = FixtureGenerator.Configuration( seed: 36, gameCount: 50000, stadiumCount: 30, teamCount: 60, geographicSpread: .nationwide ) let data = FixtureGenerator.generate(with: config) let startTime = Date() let routes = GameDAGRouter.findRoutes( games: data.games, stadiums: data.stadiumsById, constraints: .default, beamWidth: 25 // Reduced beam width for extreme scale ) let elapsed = Date().timeIntervalSince(startTime) #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes (may need timeout adjustment)") // Routes may be empty for extreme stress test - that's acceptable if it completes print("3.6 - 50000 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s") } // MARK: - 3B: Performance Baselines @Test("3.7 - Record baseline times for 500/2000/10000 games") func test_recordBaselineTimes() async throws { // Run each size and record times for baseline establishment var baselines: [(size: Int, time: TimeInterval)] = [] // 500 games let config500 = FixtureGenerator.Configuration.medium let data500 = FixtureGenerator.generate(with: config500) let start500 = Date() _ = GameDAGRouter.findRoutes( games: data500.games, stadiums: data500.stadiumsById, constraints: .default ) let elapsed500 = Date().timeIntervalSince(start500) baselines.append((500, elapsed500)) // 2000 games let config2000 = FixtureGenerator.Configuration.large let data2000 = FixtureGenerator.generate(with: config2000) let start2000 = Date() _ = GameDAGRouter.findRoutes( games: data2000.games, stadiums: data2000.stadiumsById, constraints: .default ) let elapsed2000 = Date().timeIntervalSince(start2000) baselines.append((2000, elapsed2000)) // 10000 games let config10000 = FixtureGenerator.Configuration.stress let data10000 = FixtureGenerator.generate(with: config10000) let start10000 = Date() _ = GameDAGRouter.findRoutes( games: data10000.games, stadiums: data10000.stadiumsById, constraints: .default ) let elapsed10000 = Date().timeIntervalSince(start10000) baselines.append((10000, elapsed10000)) // Print baselines for recording print("\n=== PERFORMANCE BASELINES ===") for baseline in baselines { print("\(baseline.size) games: \(String(format: "%.3f", baseline.time))s") } print("==============================\n") // All should complete within timeout for baseline in baselines { #expect(baseline.time < TestConstants.performanceTimeout, "\(baseline.size) games should complete within timeout") } } @Test("3.8 - Performance regression assertions") func test_performanceRegressionAssertions() async throws { // Skip if baselines not yet established guard TestConstants.baseline500Games > 0 else { print("Skipping regression test - baselines not yet recorded") return } // 500 games - compare to baseline with 50% tolerance let config500 = FixtureGenerator.Configuration.medium let data500 = FixtureGenerator.generate(with: config500) let start500 = Date() _ = GameDAGRouter.findRoutes( games: data500.games, stadiums: data500.stadiumsById, constraints: .default ) let elapsed500 = Date().timeIntervalSince(start500) let tolerance500 = TestConstants.baseline500Games * 1.5 #expect(elapsed500 <= tolerance500, "500 games should not regress more than 50% from baseline (\(TestConstants.baseline500Games)s)") // 2000 games if TestConstants.baseline2000Games > 0 { let config2000 = FixtureGenerator.Configuration.large let data2000 = FixtureGenerator.generate(with: config2000) let start2000 = Date() _ = GameDAGRouter.findRoutes( games: data2000.games, stadiums: data2000.stadiumsById, constraints: .default ) let elapsed2000 = Date().timeIntervalSince(start2000) let tolerance2000 = TestConstants.baseline2000Games * 1.5 #expect(elapsed2000 <= tolerance2000, "2000 games should not regress more than 50% from baseline") } // 10000 games if TestConstants.baseline10000Games > 0 { let config10000 = FixtureGenerator.Configuration.stress let data10000 = FixtureGenerator.generate(with: config10000) let start10000 = Date() _ = GameDAGRouter.findRoutes( games: data10000.games, stadiums: data10000.stadiumsById, constraints: .default ) let elapsed10000 = Date().timeIntervalSince(start10000) let tolerance10000 = TestConstants.baseline10000Games * 1.5 #expect(elapsed10000 <= tolerance10000, "10000 games should not regress more than 50% from baseline") } } // MARK: - 3C: Memory Tests @Test("3.9 - Repeated calls show no memory leak") func test_findRoutes_RepeatedCalls_NoMemoryLeak() async throws { // Run 100 iterations with medium dataset and verify no memory growth let config = FixtureGenerator.Configuration( seed: 39, gameCount: 100, stadiumCount: 15, teamCount: 15, geographicSpread: .regional ) let data = FixtureGenerator.generate(with: config) // Get initial memory footprint (rough approximation) let initialMemory = getMemoryUsageMB() // Run 100 iterations for iteration in 0..<100 { autoreleasepool { _ = GameDAGRouter.findRoutes( games: data.games, stadiums: data.stadiumsById, constraints: .default ) } // Check memory every 20 iterations if iteration > 0 && iteration % 20 == 0 { let currentMemory = getMemoryUsageMB() print("Iteration \(iteration): Memory usage \(String(format: "%.1f", currentMemory)) MB") } } let finalMemory = getMemoryUsageMB() let memoryGrowth = finalMemory - initialMemory print("Memory test: Initial=\(String(format: "%.1f", initialMemory))MB, Final=\(String(format: "%.1f", finalMemory))MB, Growth=\(String(format: "%.1f", memoryGrowth))MB") // Allow up to 50MB growth (reasonable for 100 iterations with route caching) #expect(memoryGrowth < 50.0, "Memory should not grow excessively over 100 iterations (grew \(memoryGrowth)MB)") } @Test("3.10 - Large dataset memory bounded") func test_findRoutes_LargeDataset_MemoryBounded() async throws { // 10K games should not exceed reasonable memory let config = FixtureGenerator.Configuration.stress // 10000 games let data = FixtureGenerator.generate(with: config) let beforeMemory = getMemoryUsageMB() _ = GameDAGRouter.findRoutes( games: data.games, stadiums: data.stadiumsById, constraints: .default ) let afterMemory = getMemoryUsageMB() let memoryUsed = afterMemory - beforeMemory print("10K games memory: Before=\(String(format: "%.1f", beforeMemory))MB, After=\(String(format: "%.1f", afterMemory))MB, Used=\(String(format: "%.1f", memoryUsed))MB") // 10K games with 30 stadiums should not use more than 500MB #expect(memoryUsed < 500.0, "10K games should not use more than 500MB (used \(memoryUsed)MB)") } // MARK: - Helper Functions /// Returns current memory usage in MB (approximate) private func getMemoryUsageMB() -> Double { var info = mach_task_basic_info() var count = mach_msg_type_number_t(MemoryLayout.size) / 4 let result = withUnsafeMutablePointer(to: &info) { $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count) } } guard result == KERN_SUCCESS else { return 0 } return Double(info.resident_size) / 1024.0 / 1024.0 } }