perf: optimize featured cross-country trip generation and add tests
This commit is contained in:
@@ -253,3 +253,235 @@ struct HaversineDistanceTests {
|
||||
return R * c
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cross-Country Feature Trip Tests
|
||||
|
||||
@Suite("SuggestedTripsGenerator Cross-Country")
|
||||
struct CrossCountryFeatureTripTests {
|
||||
|
||||
private struct CitySeed {
|
||||
let name: String
|
||||
let state: String
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
}
|
||||
|
||||
// 20 known US cities spanning east/central/west regions.
|
||||
private let citySeeds: [CitySeed] = [
|
||||
CitySeed(name: "Boston", state: "MA", latitude: 42.3601, longitude: -71.0589),
|
||||
CitySeed(name: "New York", state: "NY", latitude: 40.7128, longitude: -74.0060),
|
||||
CitySeed(name: "Philadelphia", state: "PA", latitude: 39.9526, longitude: -75.1652),
|
||||
CitySeed(name: "Baltimore", state: "MD", latitude: 39.2904, longitude: -76.6122),
|
||||
CitySeed(name: "Washington", state: "DC", latitude: 38.9072, longitude: -77.0369),
|
||||
CitySeed(name: "Charlotte", state: "NC", latitude: 35.2271, longitude: -80.8431),
|
||||
CitySeed(name: "Atlanta", state: "GA", latitude: 33.7490, longitude: -84.3880),
|
||||
CitySeed(name: "Nashville", state: "TN", latitude: 36.1627, longitude: -86.7816),
|
||||
CitySeed(name: "St Louis", state: "MO", latitude: 38.6270, longitude: -90.1994),
|
||||
CitySeed(name: "Chicago", state: "IL", latitude: 41.8781, longitude: -87.6298),
|
||||
CitySeed(name: "Minneapolis", state: "MN", latitude: 44.9778, longitude: -93.2650),
|
||||
CitySeed(name: "Kansas City", state: "MO", latitude: 39.0997, longitude: -94.5786),
|
||||
CitySeed(name: "Dallas", state: "TX", latitude: 32.7767, longitude: -96.7970),
|
||||
CitySeed(name: "Denver", state: "CO", latitude: 39.7392, longitude: -104.9903),
|
||||
CitySeed(name: "Albuquerque", state: "NM", latitude: 35.0844, longitude: -106.6504),
|
||||
CitySeed(name: "Phoenix", state: "AZ", latitude: 33.4484, longitude: -112.0740),
|
||||
CitySeed(name: "Las Vegas", state: "NV", latitude: 36.1699, longitude: -115.1398),
|
||||
CitySeed(name: "Los Angeles", state: "CA", latitude: 34.0522, longitude: -118.2437),
|
||||
CitySeed(name: "San Diego", state: "CA", latitude: 32.7157, longitude: -117.1611),
|
||||
CitySeed(name: "Seattle", state: "WA", latitude: 47.6062, longitude: -122.3321),
|
||||
]
|
||||
|
||||
private func canonicalToken(_ value: String) -> String {
|
||||
value
|
||||
.lowercased()
|
||||
.replacingOccurrences(of: " ", with: "_")
|
||||
.replacingOccurrences(of: ".", with: "")
|
||||
}
|
||||
|
||||
private func makeStadium(from city: CitySeed) -> Stadium {
|
||||
let token = canonicalToken(city.name)
|
||||
return Stadium(
|
||||
id: "stadium_test_\(token)",
|
||||
name: "\(city.name) Test Stadium",
|
||||
city: city.name,
|
||||
state: city.state,
|
||||
latitude: city.latitude,
|
||||
longitude: city.longitude,
|
||||
capacity: 40000,
|
||||
sport: .mlb
|
||||
)
|
||||
}
|
||||
|
||||
private func makeTeams(for stadium: Stadium) -> [Team] {
|
||||
let token = canonicalToken(stadium.city)
|
||||
let home = Team(
|
||||
id: "team_test_home_\(token)",
|
||||
name: "\(stadium.city) Home",
|
||||
abbreviation: String(token.prefix(3)).uppercased(),
|
||||
sport: .mlb,
|
||||
city: stadium.city,
|
||||
stadiumId: stadium.id
|
||||
)
|
||||
let away = Team(
|
||||
id: "team_test_away_\(token)",
|
||||
name: "\(stadium.city) Away",
|
||||
abbreviation: "A\(String(token.prefix(2)).uppercased())",
|
||||
sport: .mlb,
|
||||
city: stadium.city,
|
||||
stadiumId: stadium.id
|
||||
)
|
||||
return [home, away]
|
||||
}
|
||||
|
||||
private func makeGames(
|
||||
from stadiums: [Stadium],
|
||||
startDate: Date,
|
||||
spacingDays: Int = 1,
|
||||
idPrefix: String
|
||||
) -> [Game] {
|
||||
var games: [Game] = []
|
||||
let calendar = Calendar.current
|
||||
|
||||
for (index, stadium) in stadiums.enumerated() {
|
||||
let gameDate = calendar.date(byAdding: .day, value: index * spacingDays, to: startDate) ?? startDate
|
||||
let token = canonicalToken(stadium.city)
|
||||
games.append(
|
||||
Game(
|
||||
id: "game_test_\(idPrefix)_\(token)_\(index)",
|
||||
homeTeamId: "team_test_home_\(token)",
|
||||
awayTeamId: "team_test_away_\(token)",
|
||||
stadiumId: stadium.id,
|
||||
dateTime: gameDate,
|
||||
sport: .mlb,
|
||||
season: "2026"
|
||||
)
|
||||
)
|
||||
}
|
||||
return games
|
||||
}
|
||||
|
||||
private func makeDataset(spacingDays: Int = 1) -> (games: [Game], stadiumsById: [String: Stadium], teamsById: [String: Team]) {
|
||||
let stadiums = citySeeds.map(makeStadium)
|
||||
let teams = stadiums.flatMap(makeTeams)
|
||||
let sortedEastToWest = stadiums.sorted { $0.longitude > $1.longitude }
|
||||
let sortedWestToEast = Array(sortedEastToWest.reversed())
|
||||
let baseDate = Date(timeIntervalSince1970: 1_736_000_000) // Fixed baseline for deterministic test behavior
|
||||
|
||||
let eastToWestGames = makeGames(
|
||||
from: sortedEastToWest,
|
||||
startDate: baseDate,
|
||||
spacingDays: spacingDays,
|
||||
idPrefix: "e2w"
|
||||
)
|
||||
|
||||
let secondLegStart = Calendar.current.date(
|
||||
byAdding: .day,
|
||||
value: (sortedEastToWest.count * spacingDays) + 2,
|
||||
to: baseDate
|
||||
) ?? baseDate
|
||||
|
||||
let westToEastGames = makeGames(
|
||||
from: sortedWestToEast,
|
||||
startDate: secondLegStart,
|
||||
spacingDays: spacingDays,
|
||||
idPrefix: "w2e"
|
||||
)
|
||||
|
||||
let games = eastToWestGames + westToEastGames
|
||||
|
||||
let stadiumsById = stadiums.reduce(into: [String: Stadium]()) { partialResult, stadium in
|
||||
partialResult[stadium.id] = stadium
|
||||
}
|
||||
let teamsById = teams.reduce(into: [String: Team]()) { partialResult, team in
|
||||
partialResult[team.id] = team
|
||||
}
|
||||
|
||||
return (games: games, stadiumsById: stadiumsById, teamsById: teamsById)
|
||||
}
|
||||
|
||||
private func routeRegions(for trip: SuggestedTrip, stadiumsById: [String: Stadium]) -> Set<Region> {
|
||||
let gameIdsInTrip = Set(trip.trip.stops.flatMap(\.games))
|
||||
let tripGames = trip.richGames.values
|
||||
.map(\.game)
|
||||
.filter { gameIdsInTrip.contains($0.id) }
|
||||
|
||||
return Set(tripGames.compactMap { game in
|
||||
stadiumsById[game.stadiumId]?.region
|
||||
})
|
||||
}
|
||||
|
||||
@Test("Cross-country (20 cities): east-to-west generates a valid coast-to-coast trip")
|
||||
func crossCountry_eastToWest_fromTwentyCities() {
|
||||
let (games, stadiumsById, teamsById) = makeDataset()
|
||||
|
||||
let trip = SuggestedTripsGenerator._testGenerateCrossCountryTrip(
|
||||
games: games,
|
||||
stadiums: stadiumsById,
|
||||
teams: teamsById,
|
||||
eastToWest: true
|
||||
)
|
||||
|
||||
#expect(trip != nil)
|
||||
guard let trip else { return }
|
||||
|
||||
#expect(trip.region == .crossCountry)
|
||||
#expect(trip.trip.stops.count >= 3)
|
||||
#expect(trip.trip.totalGames >= 3)
|
||||
|
||||
let regions = routeRegions(for: trip, stadiumsById: stadiumsById)
|
||||
#expect(regions.contains(.east))
|
||||
#expect(regions.contains(.west))
|
||||
}
|
||||
|
||||
@Test("Cross-country (20 cities): west-to-east generates a valid coast-to-coast trip")
|
||||
func crossCountry_westToEast_fromTwentyCities() {
|
||||
let (games, stadiumsById, teamsById) = makeDataset()
|
||||
|
||||
let trip = SuggestedTripsGenerator._testGenerateCrossCountryTrip(
|
||||
games: games,
|
||||
stadiums: stadiumsById,
|
||||
teams: teamsById,
|
||||
eastToWest: false
|
||||
)
|
||||
|
||||
#expect(trip != nil)
|
||||
guard let trip else { return }
|
||||
|
||||
#expect(trip.region == .crossCountry)
|
||||
#expect(trip.trip.stops.count >= 3)
|
||||
#expect(trip.trip.totalGames >= 3)
|
||||
|
||||
let regions = routeRegions(for: trip, stadiumsById: stadiumsById)
|
||||
#expect(regions.contains(.east))
|
||||
#expect(regions.contains(.west))
|
||||
}
|
||||
|
||||
@Test("Cross-country performance: 20-city dataset stays under target average runtime")
|
||||
func crossCountry_generationPerformance_twentyCityDataset() {
|
||||
let (games, stadiumsById, teamsById) = makeDataset()
|
||||
let iterations = 20
|
||||
var elapsedMillis: [Double] = []
|
||||
|
||||
for _ in 0..<iterations {
|
||||
let start = DispatchTime.now().uptimeNanoseconds
|
||||
let trip = SuggestedTripsGenerator._testGenerateCrossCountryTrip(
|
||||
games: games,
|
||||
stadiums: stadiumsById,
|
||||
teams: teamsById,
|
||||
eastToWest: true
|
||||
)
|
||||
let end = DispatchTime.now().uptimeNanoseconds
|
||||
#expect(trip != nil)
|
||||
|
||||
let millis = Double(end - start) / 1_000_000
|
||||
elapsedMillis.append(millis)
|
||||
}
|
||||
|
||||
let averageMillis = elapsedMillis.reduce(0, +) / Double(elapsedMillis.count)
|
||||
let worstMillis = elapsedMillis.max() ?? 0
|
||||
|
||||
print("Cross-country benchmark (20-city): avg=\(averageMillis)ms worst=\(worstMillis)ms over \(iterations) runs")
|
||||
|
||||
#expect(averageMillis < 250.0, "Average generation time was \(averageMillis)ms")
|
||||
#expect(worstMillis < 500.0, "Worst generation time was \(worstMillis)ms")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user