Skip to content

Commit 29b01c2

Browse files
bmanderclaude
andauthored
Reuse shared Polyline cache for focused shapes; drop decoded double-cache (#1908) (#1919)
A focused trip's shape was cached twice under the same shapeId with divergent policies: TripObservationRepository's ShapeCache (Polyline, LRU 100, no TTL — GTFS shapes are immutable) and FocusedTripRepository.shapeCache (decoded List<GeoPoint>, LRU 32, with an unexplained 10-minute TTL on immutable data). ensureShape() already returns the shared, deduped Polyline (coalescing the network fetch via its own SingleFlight), so the decoded cache was a redundant second copy (~2x memory, a second eviction policy, and the stray TTL). Drop it and decode on demand from the shared Polyline cache; the Location -> GeoPoint map is cheap and runs on a focus change, not the per-frame path. The fetch concurrency throttle (shapePermits) and same-shape decode coalescing (shapeFetches) are unchanged. The cross-call shape memo now lives entirely in TripObservationRepository (covered by TripObservationRepositoryTest); reframe the FocusedTripRepository test that asserted its own memo to document the delegation. Part of #1908 (finding 8, item 2). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ac3e7c2 commit 29b01c2

2 files changed

Lines changed: 18 additions & 8 deletions

File tree

onebusaway-android/src/main/java/org/onebusaway/android/map/FocusedTripRepository.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,6 @@ class DefaultFocusedTripRepository internal constructor(
120120
private val shapePermits = Semaphore(MAX_CONCURRENT_SHAPE_FETCHES)
121121
private val routeStopCache =
122122
ExpiringLruCache<String, List<RouteStopGroup>>(cacheSize, cacheTtl, now)
123-
private val shapeCache =
124-
ExpiringLruCache<String, List<GeoPoint>>(cacheSize, cacheTtl, now)
125123

126124
override suspend fun getGeometry(trips: Set<FocusedTrip>): FocusedTripGeometry = coroutineScope {
127125
val tripsWithShapes = ArrayList<FocusedTrip>()
@@ -203,15 +201,20 @@ class DefaultFocusedTripRepository internal constructor(
203201
null
204202
}
205203

206-
/** Shape-id cache avoids refetching one pattern merely because a later arrival has a new trip id. */
204+
/**
205+
* Decoded points for a shape. Retention and fetch dedup are owned by the shared [Polyline] cache
206+
* inside [TripObservationRepository.ensureShape] (keyed by shapeId, no TTL — GTFS shapes are
207+
* immutable), so this holds no shape cache of its own: it coalesces concurrent decodes for one
208+
* shape and re-decodes the (already-cached) polyline per focus. The map is cheap and runs on a
209+
* focus change, not the per-frame path; the permit caps concurrent shape fetches.
210+
*/
207211
private suspend fun fetchShape(tripId: String, shapeId: String): List<GeoPoint>? =
208-
shapeCache.get(shapeId) ?: shapeFetches.run(shapeId) {
209-
shapeCache.get(shapeId) ?: shapePermits.withPermit {
212+
shapeFetches.run(shapeId) {
213+
shapePermits.withPermit {
210214
resolveOrNull("shape $shapeId") { observations.ensureShape(tripId, shapeId) }
211215
?.let { polyline ->
212216
withContext(Dispatchers.Default) { polyline.points.map(Location::toGeoPoint) }
213217
}
214-
?.also { shapeCache.put(shapeId, it) }
215218
}
216219
}
217220
}

onebusaway-android/src/test/java/org/onebusaway/android/map/FocusedTripRepositoryTest.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,11 @@ class FocusedTripRepositoryTest {
129129
}
130130

131131
@Test
132-
fun `shape cache is keyed by shape rather than trip`() = runTest {
132+
fun `repeat focus re-delegates shape decode to the shared polyline cache`() = runTest {
133+
// FocusedTripRepository holds no shape cache of its own: retention and fetch dedup live in
134+
// the shared Polyline cache inside TripObservationRepository.ensureShape (keyed by shapeId,
135+
// no TTL). So a later focus on the same shape re-queries ensureShape — cheap in production,
136+
// where the polyline is already cached there — rather than memoizing decoded points here.
133137
val observations = Observations().apply { shapes["shared"] = Polyline(emptyList()) }
134138
val repository = DefaultFocusedTripRepository(
135139
observations, RouteStops(), backgroundScope, now = { ElapsedTime(0L) }
@@ -138,7 +142,10 @@ class FocusedTripRepositoryTest {
138142
repository.getGeometry(setOf(FocusedTrip("older-trip", "route", "shared", null)))
139143
repository.getGeometry(setOf(FocusedTrip("newer-trip", "route", "shared", null)))
140144

141-
assertEquals(listOf("older-trip" to "shared"), observations.shapeRequests)
145+
assertEquals(
146+
listOf("older-trip" to "shared", "newer-trip" to "shared"),
147+
observations.shapeRequests,
148+
)
142149
}
143150

144151
private fun stop(id: String) = ObaStopElement(id = id, lat = 47.0, lon = -122.0)

0 commit comments

Comments
 (0)