Skip to content

Perf investigation: GTFS-RT matching CPU harness + findings#472

Merged
aaronbrethorst merged 12 commits into
mainfrom
perf/gtfsrt-matching-investigation
Jul 11, 2026
Merged

Perf investigation: GTFS-RT matching CPU harness + findings#472
aaronbrethorst merged 12 commits into
mainfrom
perf/gtfsrt-matching-investigation

Conversation

@aaronbrethorst

@aaronbrethorst aaronbrethorst commented Jul 11, 2026

Copy link
Copy Markdown
Member

What & why

Investigates a production report: a 2 GB / 1 vCPU OBA box with one GTFS-RT feed (~840 trips, ~30 s poll) pegs CPU near 85% of a core. This branch builds a repeatable perf harness, profiles the poll path at King County Metro scale, and produces an evidence-backed findings report + ranked fix list. No matching-engine code is changed — the deliverable is evidence and a plan.

Headline finding

BlockConfigurationEntryImpl$BlockStopTimeList.get() constructs a new BlockStopTimeEntryImpl on every .get() call (no cache — BlockConfigurationEntryImpl.java:375):

  • 19.78% of CPU self-time (top frame by 3×)
  • 69% of all heap allocation — 48.8 GB over a 100-refresh run (~489 MB/refresh)

It's hammered by repeated stop-time iteration on both the trip-update matching path (applyTripUpdatesToRecord) and the block-location write path (getBlockLocation) — the latter is also the API read path, so a fix likely helps arrivals queries too. Two O(stops²) inner loops in GtfsRealtimeTripLibrary and the KCM stop-count histogram (33k trips, median 31 / max 97 stops) corroborate it.

Ranked fix #1 (low cost/risk, highest reward): cache/memoize BlockStopTimeList.get(). Full list of 4 fixes with cost/risk/reward + covering tests in the report.

Honest caveat: on a desktop core one 30 s poll is ~200 ms ≈ 0.7% of a core — this repro proves the waste but does not by itself reproduce the production 85% peg (which likely also involves a slower cloud vCPU, higher poll cadence, or API read-traffic). The report recommends confirming production poll cadence + read load before attributing the exact figure.

What's in the branch

  • onebusaway-gtfsrt-perf — a new isolated module (all main() classes, no tests in the default build): PerfBundleHarness, GtfsRtRefreshBenchmark (reset()+refresh() timed loop), Profiling (measure-phase async-profiler + JFR fallback), BuildKcmBundle, StopCountHistogram. Re-run GtfsRtRefreshBenchmark to validate any fix's before/after.
  • Golden regression testGtfsRealtimeTripLibraryCharacterizationTest locks createVehicleLocationRecordForUpdate output so future optimizations are provably behavior-preserving (runs in CI).
  • docs/perf/2026-07-10-gtfsrt-matching-findings.md — the full report.
  • Design spec + implementation plan under docs/superpowers/.

KCM GTFS/feed data, the built bundle, and flamegraphs are git-ignored (not committed).

Testing

  • GtfsRealtimeTripLibraryCharacterizationTest: 1/1 passing (BUILD SUCCESS).
  • Harness validated end-to-end against a live KCM bundle: 100-refresh baseline (mean 203 ms), records=551 every iteration (both cost stages fire), CPU + allocation flamegraphs captured.

Summary by CodeRabbit

  • New Features

    • Added a repeatable GTFS-realtime performance benchmark for measuring refresh timing, throughput, and processed records.
    • Added utilities for building test bundles, profiling CPU and allocations, and analyzing trip stop-count distributions.
  • Documentation

    • Added investigation findings, design specifications, methodology, performance evidence, and prioritized optimization recommendations.
  • Tests

    • Added a golden-output characterization test to help verify GTFS-realtime matching behavior remains consistent.

Also wires the two GtfsRealtimeSource dependencies PerfBundleHarness was
missing (VehicleOccupancyListener, ServiceAlertsService). Without them
refresh() NPEs on the first vehicle occupancy record and again on the
first alert, since those setters are normally @Autowired by Spring but
this harness builds GtfsRealtimeSource by hand. VehicleOccupancyListener
reuses the existing VehicleStatusServiceImpl bean (it implements both
listener interfaces); ServiceAlertsService gets a minimal in-memory
implementation since the DB-backed one isn't wired into this harness's
Spring context.
…date

Locks the observable output of GtfsRealtimeTripLibrary's matching hot
path (blockId, tripId, serviceDate, scheduleDeviation, timepoint
predictions) against a committed golden fixture, so upcoming
allocation-reduction changes to BlockStopTimeList.get() and friends can
be verified as behavior-preserving. Fixture construction is ported
from GtfsRealtimeTripLibraryTest#testTprInterpolation_0(), the
existing case that produces multiple timepoint predictions (one
interpolated, one from the feed). Regenerate with -Dupdate.golden=true.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a standalone GTFS-RT performance module with bundle setup, refresh benchmarking, CPU/allocation profiling, stop-count analysis, a golden characterization test, and investigation documentation. Matching engine implementation is unchanged.

Changes

GTFS-RT performance investigation

Layer / File(s) Summary
Investigation design and execution plan
docs/superpowers/specs/..., docs/superpowers/plans/...
Defines the harness architecture, measurement phases, profiling modes, evidence collection, validation strategy, and implementation plan.
Performance module and bundle bootstrap
pom.xml, onebusaway-gtfsrt-perf/pom.xml, onebusaway-gtfsrt-perf/src/main/resources/*, onebusaway-gtfsrt-perf/src/main/java/.../BuildKcmBundle.java
Registers a standalone Maven module, configures federation Spring beans, and adds a CLI for building and indexing a GTFS bundle.
Refresh harness wiring and lifecycle
onebusaway-gtfsrt-perf/src/main/java/.../PerfBundleHarness.java
Creates the Spring context, wires GtfsRealtimeSource, disables background refresh, counts produced records, supplies in-memory alerts, and runs reset-plus-refresh iterations.
Benchmark, profiling, and distribution tools
onebusaway-gtfsrt-perf/src/main/java/.../GtfsRtRefreshBenchmark.java, Profiling.java, StopCountHistogram.java
Adds warmup and timed refresh measurements, async-profiler/JFR selection, latency reporting, and stop-count distribution statistics.
Behavior characterization and findings report
onebusaway-transit-data-federation/src/test/..., onebusaway-transit-data-federation/src/test/resources/..., docs/perf/*
Adds deterministic golden coverage for vehicle-location record creation and reports refresh timings, hotspots, allocation evidence, complexity analysis, and ranked optimization candidates.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Benchmark
  participant Harness
  participant GtfsRealtimeSource
  participant Listener
  Benchmark->>Harness: refreshOnce()
  Harness->>GtfsRealtimeSource: reset()
  Harness->>GtfsRealtimeSource: refresh()
  GtfsRealtimeSource->>Listener: vehicle-location callbacks
  Listener-->>Harness: record count
  Benchmark-->>Benchmark: aggregate timed samples
Loading
🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #1's requested OBA-app-mods patches are not addressed; this PR is a separate performance investigation. Either add the service-alert, cache, block, and search-behavior changes from issue #1, or unlink that issue from this PR.
Out of Scope Changes check ⚠️ Warning The perf harness, profiling docs, and findings are unrelated to issue #1's requested functional patch set. Split the performance investigation into a separate PR and keep this one limited to the OBA-app-mods patch scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a GTFS-RT matching CPU investigation with a harness and findings report.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/gtfsrt-matching-investigation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
onebusaway-transit-data-federation/src/test/java/org/onebusaway/transit_data_federation/impl/realtime/gtfs_realtime/GtfsRealtimeTripLibraryCharacterizationTest.java (1)

91-106: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove duplicate setBlockCalendarService call on mock.

Lines 101 and 105 are identical no-op calls to setBlockCalendarService on a Mockito mock. The mock is already properly configured via when().thenReturn() on line 99, so both setter calls are unnecessary — and line 105 is a straight duplicate of line 101.

♻️ Proposed cleanup
     Mockito.when(_serviceSource.getBlockCalendarService()).thenReturn(_blockCalendarService);
     Mockito.when(_serviceSource.getBlockFinder()).thenReturn(new BlockFinder(_serviceSource));
-    _serviceSource.setBlockCalendarService(_blockCalendarService);
     _library.setEntitySource(_entitySource);
     _library.setServiceSource(_serviceSource);
-
-    _serviceSource.setBlockCalendarService(_blockCalendarService);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@onebusaway-transit-data-federation/src/test/java/org/onebusaway/transit_data_federation/impl/realtime/gtfs_realtime/GtfsRealtimeTripLibraryCharacterizationTest.java`
around lines 91 - 106, Remove the redundant duplicate invocation of
setBlockCalendarService in the before() setup method; retain only the necessary
mock configuration and single setter call, or remove both if the setter is not
required for test behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@onebusaway-gtfsrt-perf/src/main/java/org/onebusaway/perf/gtfsrt/PerfBundleHarness.java`:
- Around line 159-160: Delegate handleRawPosition in the listener adapter to the
wrapped real listener, passing through vehicleId, lat, lon, and timestamp just
like resetVehicleLocation delegates to _delegate, so VehicleStatusServiceImpl
processes every raw position update during performance runs.

---

Nitpick comments:
In
`@onebusaway-transit-data-federation/src/test/java/org/onebusaway/transit_data_federation/impl/realtime/gtfs_realtime/GtfsRealtimeTripLibraryCharacterizationTest.java`:
- Around line 91-106: Remove the redundant duplicate invocation of
setBlockCalendarService in the before() setup method; retain only the necessary
mock configuration and single setter call, or remove both if the setter is not
required for test behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e6a5857c-de7e-40c3-9c5d-8567a922c22c

📥 Commits

Reviewing files that changed from the base of the PR and between c967410 and 8708372.

📒 Files selected for processing (13)
  • docs/perf/2026-07-10-gtfsrt-matching-findings.md
  • docs/superpowers/plans/2026-07-10-gtfsrt-matching-perf-harness.md
  • docs/superpowers/specs/2026-07-10-gtfsrt-matching-perf-investigation-design.md
  • onebusaway-gtfsrt-perf/pom.xml
  • onebusaway-gtfsrt-perf/src/main/java/org/onebusaway/perf/gtfsrt/BuildKcmBundle.java
  • onebusaway-gtfsrt-perf/src/main/java/org/onebusaway/perf/gtfsrt/GtfsRtRefreshBenchmark.java
  • onebusaway-gtfsrt-perf/src/main/java/org/onebusaway/perf/gtfsrt/PerfBundleHarness.java
  • onebusaway-gtfsrt-perf/src/main/java/org/onebusaway/perf/gtfsrt/Profiling.java
  • onebusaway-gtfsrt-perf/src/main/java/org/onebusaway/perf/gtfsrt/StopCountHistogram.java
  • onebusaway-gtfsrt-perf/src/main/resources/perf-data-sources.xml
  • onebusaway-transit-data-federation/src/test/java/org/onebusaway/transit_data_federation/impl/realtime/gtfs_realtime/GtfsRealtimeTripLibraryCharacterizationTest.java
  • onebusaway-transit-data-federation/src/test/resources/org/onebusaway/transit_data_federation/impl/realtime/gtfs_realtime/characterization-golden.txt
  • pom.xml

Comment on lines +159 to +160
@Override public void resetVehicleLocation(AgencyAndId vehicleId) { _delegate.resetVehicleLocation(vehicleId); }
@Override public void handleRawPosition(AgencyAndId vehicleId, double lat, double lon, long timestamp) { }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find VehicleStatusServiceImpl.handleRawPosition implementation to determine
# whether it does non-trivial work that the harness should be measuring.
rg -n -A 15 'void handleRawPosition' --type java \
  onebusaway-transit-data-federation/src/main/java/org/onebusaway/transit_data_federation/impl/realtime/VehicleStatusServiceImpl.java

Repository: OneBusAway/onebusaway-application-modules

Length of output: 984


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the harness and surrounding references to understand whether raw-position
# handling is part of the intended benchmark path.
printf '\n== PerfBundleHarness excerpt ==\n'
sed -n '1,240p' onebusaway-gtfsrt-perf/src/main/java/org/onebusaway/perf/gtfsrt/PerfBundleHarness.java

printf '\n== References to handleRawPosition ==\n'
rg -n -A 3 -B 3 'handleRawPosition|setVehiclePositionsUrl|replicate the production poll path|poll path at scale' \
  onebusaway-gtfsrt-perf onebusaway-transit-data-federation onebusaway-gtfs-realtime \
  --type java --type md

Repository: OneBusAway/onebusaway-application-modules

Length of output: 17558


Delegate handleRawPosition to the real listener PerfBundleHarness.java:160GtfsRealtimeSource calls this for every vehicle position update, and VehicleStatusServiceImpl stores each raw position in its cache. Leaving it empty makes the perf harness measure less work than the real poll path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@onebusaway-gtfsrt-perf/src/main/java/org/onebusaway/perf/gtfsrt/PerfBundleHarness.java`
around lines 159 - 160, Delegate handleRawPosition in the listener adapter to
the wrapped real listener, passing through vehicleId, lat, lon, and timestamp
just like resetVehicleLocation delegates to _delegate, so
VehicleStatusServiceImpl processes every raw position update during performance
runs.

@aaronbrethorst aaronbrethorst merged commit 07fd254 into main Jul 11, 2026
3 checks passed
@aaronbrethorst aaronbrethorst deleted the perf/gtfsrt-matching-investigation branch July 11, 2026 03:55
@Ahmedhossamdev

Copy link
Copy Markdown
Member

@aaronbrethorst 🔥🔥

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants