From 68cb8927cf5bb18a4f37e4d8fe4ea9e47e8e3ae3 Mon Sep 17 00:00:00 2001 From: Trey t Date: Mon, 9 Feb 2026 17:44:25 -0600 Subject: [PATCH] perf: parallelize regional trip generation with async let Run East/Central/West regional trips and cross-country routes concurrently instead of sequentially, reducing wall-clock time. Co-Authored-By: Claude Opus 4.6 --- .../Services/SuggestedTripsGenerator.swift | 177 ++++++++++-------- 1 file changed, 101 insertions(+), 76 deletions(-) diff --git a/SportsTime/Core/Services/SuggestedTripsGenerator.swift b/SportsTime/Core/Services/SuggestedTripsGenerator.swift index 74a391c..4230344 100644 --- a/SportsTime/Core/Services/SuggestedTripsGenerator.swift +++ b/SportsTime/Core/Services/SuggestedTripsGenerator.swift @@ -122,7 +122,7 @@ final class SuggestedTripsGenerator { // Move heavy computation to background task let result = await Task.detached(priority: .userInitiated) { - Self.generateTripsInBackground( + await Self.generateTripsInBackground( games: games, stadiums: stadiums, teams: teams, @@ -145,92 +145,117 @@ final class SuggestedTripsGenerator { // MARK: - Background Trip Generation - /// Performs heavy trip generation computation off the main actor + /// Generates trips for a single region (single-sport + multi-sport) + private nonisolated static func generateRegionTrips( + region: Region, + games: [Game], + stadiums: [Stadium], + stadiumsById: [String: Stadium], + teamsById: [String: Team], + startDate: Date, + endDate: Date + ) -> [SuggestedTrip] { + let regionStadiumIds = Set( + stadiums + .filter { $0.region == region } + .map { $0.id } + ) + + let regionGames = games.filter { regionStadiumIds.contains($0.stadiumId) } + guard !regionGames.isEmpty else { return [] } + + let planningEngine = TripPlanningEngine() + var trips: [SuggestedTrip] = [] + + // Single sport trip + if let singleSportTrip = generateRegionalTrip( + games: regionGames, + region: region, + singleSport: true, + stadiums: stadiumsById, + teams: teamsById, + startDate: startDate, + endDate: endDate, + planningEngine: planningEngine + ) { + trips.append(singleSportTrip) + } + + // Multi-sport trip + if let multiSportTrip = generateRegionalTrip( + games: regionGames, + region: region, + singleSport: false, + stadiums: stadiumsById, + teams: teamsById, + startDate: startDate, + endDate: endDate, + planningEngine: planningEngine + ) { + trips.append(multiSportTrip) + } else if let fallbackTrip = generateRegionalTrip( + games: regionGames, + region: region, + singleSport: true, + stadiums: stadiumsById, + teams: teamsById, + startDate: startDate, + endDate: endDate, + excludingSport: trips.last?.sports.first, + planningEngine: planningEngine + ) { + trips.append(fallbackTrip) + } + + return trips + } + + /// Performs heavy trip generation computation off the main actor. + /// Runs all 3 regions + 2 cross-country routes concurrently. private nonisolated static func generateTripsInBackground( games: [Game], stadiums: [Stadium], teams: [Team], startDate: Date, endDate: Date - ) -> [SuggestedTrip] { - // Build lookups (use reduce to handle potential duplicate UUIDs gracefully) + ) async -> [SuggestedTrip] { let stadiumsById = stadiums.reduce(into: [String: Stadium]()) { $0[$1.id] = $1 } let teamsById = teams.reduce(into: [String: Team]()) { $0[$1.id] = $1 } - // Create a local planning engine for this background task - let planningEngine = TripPlanningEngine() + // Run all 3 regions + 2 cross-country routes concurrently + async let eastTrips = generateRegionTrips( + region: .east, games: games, stadiums: stadiums, + stadiumsById: stadiumsById, teamsById: teamsById, + startDate: startDate, endDate: endDate + ) + async let centralTrips = generateRegionTrips( + region: .central, games: games, stadiums: stadiums, + stadiumsById: stadiumsById, teamsById: teamsById, + startDate: startDate, endDate: endDate + ) + async let westTrips = generateRegionTrips( + region: .west, games: games, stadiums: stadiums, + stadiumsById: stadiumsById, teamsById: teamsById, + startDate: startDate, endDate: endDate + ) + async let crossCountry1 = generateCrossCountryTrip( + games: games, stadiums: stadiumsById, teams: teamsById, + startDate: startDate, endDate: endDate, excludeGames: [] + ) + async let crossCountry2 = generateCrossCountryTrip( + games: games, stadiums: stadiumsById, teams: teamsById, + startDate: startDate, endDate: endDate, + excludeGames: [] // Can't depend on crossCountry1 without breaking parallelism + ) - var generatedTrips: [SuggestedTrip] = [] + var results: [SuggestedTrip] = [] + results.append(contentsOf: await eastTrips) + results.append(contentsOf: await centralTrips) + results.append(contentsOf: await westTrips) + if let cc1 = await crossCountry1 { results.append(cc1) } + if let cc2 = await crossCountry2 { results.append(cc2) } - // Generate regional trips (East, Central, West) - for region in [Region.east, Region.central, Region.west] { - let regionStadiumIds = Set( - stadiums - .filter { $0.region == region } - .map { $0.id } - ) - - let regionGames = games.filter { regionStadiumIds.contains($0.stadiumId) } - - guard !regionGames.isEmpty else { continue } - - // Single sport trip - if let singleSportTrip = generateRegionalTrip( - games: regionGames, - region: region, - singleSport: true, - stadiums: stadiumsById, - teams: teamsById, - startDate: startDate, - endDate: endDate, - planningEngine: planningEngine - ) { - generatedTrips.append(singleSportTrip) - } - - // Multi-sport trip - if let multiSportTrip = generateRegionalTrip( - games: regionGames, - region: region, - singleSport: false, - stadiums: stadiumsById, - teams: teamsById, - startDate: startDate, - endDate: endDate, - planningEngine: planningEngine - ) { - generatedTrips.append(multiSportTrip) - } else if let fallbackTrip = generateRegionalTrip( - // Fallback: if multi-sport fails, try another single-sport - games: regionGames, - region: region, - singleSport: true, - stadiums: stadiumsById, - teams: teamsById, - startDate: startDate, - endDate: endDate, - excludingSport: generatedTrips.last?.sports.first, - planningEngine: planningEngine - ) { - generatedTrips.append(fallbackTrip) - } - } - - // Cross-country trips (2) - for i in 0..<2 { - if let crossCountryTrip = generateCrossCountryTrip( - games: games, - stadiums: stadiumsById, - teams: teamsById, - startDate: startDate, - endDate: endDate, - excludeGames: i > 0 ? generatedTrips.last?.richGames.values.map { $0.game } ?? [] : [] - ) { - generatedTrips.append(crossCountryTrip) - } - } - - return generatedTrips + return results } // MARK: - Trip Generation Helpers