Skip to content

SP0.13a (#749): gate findNextReservationTarget on reservation-eligibility (~9% fast-sim win)#754

Merged
bedaHovorka merged 2 commits into
goal-10from
sp0-13/fast-sim-perf
Jul 11, 2026
Merged

SP0.13a (#749): gate findNextReservationTarget on reservation-eligibility (~9% fast-sim win)#754
bedaHovorka merged 2 commits into
goal-10from
sp0-13/fast-sim-perf

Conversation

@bedaHovorka

Copy link
Copy Markdown
Owner

Closes #749.

Split out of #738, which is now closed as not-reproducible — the claimed 0.2 s → 0.6 s regression does
not exist in any release build (see the close-out
for the full bisect). What the investigation did find is this: a real, measurable ~9% of fast-sim wall
time spent on graph searches whose results are thrown away.

The problem

ShuntingLoop.toBlockInputObservation resolved BlockInputObservation.toSeparatorName by calling
PathReservationService.findNextReservationTarget(to) for every block input on every tick — including
inputs that provably cannot take a forward reservation.

That call is a BFS (findNextSemaphoresVia) plus a full topological-path enumeration per candidate
(isPathAvailablefindAllTopologicalPaths). Measured on vyhybna, example shuntingLoop 300:

findNextReservationTarget calls 906
...discarded unread by the dispatcher 892 (98.5%)
cost 30.3 ms of 342 ms (~9%)

RuleBasedDispatcher.checkInput already returns null for exactly those inputs — FREE -> null,
!isApproachingThisInput -> null, pathAlreadyExtendedBeyond -> null. The value was computed and then
dropped on the floor.

The fix

Gate the search on eligibility. The three facts it needs are already computed a few lines above, so the gate
costs nothing:

val canReserveForward =
    !pathAlreadyExtendedBeyond && (isApproachingThisInput || pathSetUpTowardThisInput)
val toSeparatorName =
    if (canReserveForward) pathReservationService.findNextReservationTarget(to)?.let(::nameOf) else null

Sensor-port contract change (deliberate, documented)

This narrows the BlockInputObservation contract: toSeparatorName is populated only where a forward
reservation is possible, and is null elsewhere without the search having run.

For RuleBasedDispatcher this is behaviour-preserving by construction. For a future LLM dispatcher the
distinction matters, so it is now spelled out in BlockInputObservation's KDoc: a null means "no
forward-reservation target applies"
, not "the search found nothing" — a dispatcher must not read it as
evidence that the track ahead is occupied. Both cases call for the same response anyway (no reservation this
tick).

Results

Native linuxX64 release, example shuntingLoop 300, median of 7:

build wall
tip before fix 341 ms
this PR 318 ms
SP0.4 c3eb0f66 (pre-seam baseline) 307–329 ms

The seam's per-tick overhead is now back inside baseline noise.

Golden output is byte-for-byte identical — 90/90 event lines, 5 trains, 211.1 s sim time.

Tests

New ShuntingLoopReservationTargetLazinessTest (:core, @Tag("integration-test")) locks the contract:
no non-eligible input may carry a toSeparatorName, and a FREE input never carries one. Verified to
fail 2/2 without the gate (k1->doA1=A resolves), and pass with it.

gate result
:core:jvmTest 2194 ✅
:core:integrationTest 725 ✅
:core:heavyTest (required — touches sim/) 1001 ✅
:dispatcher-agent:test 51 ✅
:desktop-ui:test 650 ✅
:desktop-ui:integrationTest 247 ✅
checkCoreCommonMainPurity / detekt / ktlintCheck

0 failures across all suites.

Not in this PR

The investigation found that ~60–70% of fast-sim wall time is kDisco RKF45 continuous integration
(1.85M Motor.derivatives() calls/run, caused by dtMax = 1e-3 acting as the event-detection resolution).
That is a much larger and much riskier change — filed as #750 (model side) and
kdisco#67 (engine side). Also spawned: #751, #752, #753.

🤖 Generated with Claude Code

ShuntingLoop.toBlockInputObservation resolved BlockInputObservation.toSeparatorName
by calling PathReservationService.findNextReservationTarget for every block input on
every tick, including inputs that provably cannot take a forward reservation.

The call is a BFS (findNextSemaphoresVia) plus a full topological-path enumeration per
candidate (isPathAvailable -> findAllTopologicalPaths). Measured on vyhybna: 906 calls
per `example shuntingLoop 300`, of which 892 (98.5%) are discarded unread by
RuleBasedDispatcher.checkInput, which already returns null for FREE, not-approaching
and already-extended inputs. Cost: 30.3 ms of 342 ms wall (~9%).

Gate the search on eligibility. The three facts it needs (isApproachingThisInput,
pathSetUpTowardThisInput, pathAlreadyExtendedBeyond) are already computed in the same
function, so the gate is free.

This narrows the sensor-port contract: toSeparatorName is now populated only where a
forward reservation is possible, and is null elsewhere without the search having run.
RuleBasedDispatcher behaviour is unchanged by construction. Documented on
BlockInputObservation so a future LLM dispatcher does not read null as "track ahead is
occupied".

Native linuxX64 release, example shuntingLoop 300, median of 7:
  before  341 ms
  after   318 ms
Golden output byte-for-byte identical (90/90 event lines, 5 trains, 211.1 s sim time).

ShuntingLoopReservationTargetLazinessTest locks the contract and fails 2/2 without the
gate (a FREE input resolves k1->doA1=A).

Gates: core:jvmTest 2194, core:integrationTest 725, core:heavyTest 1001,
dispatcher-agent:test 51, desktop-ui:test 650, desktop-ui:integrationTest 247 — all
green. purity/detekt/ktlint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@bedaHovorka bedaHovorka left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@copilot : some tips for clean code, please fix it

Copilot AI commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

@copilot : some tips for clean code, please fix it

Fixed in 2fcb2f5: I extracted the reservation-target eligibility/lookup into a small helper in ShuntingLoop, so toBlockInputObservation() stays cleaner without changing behavior.

@bedaHovorka bedaHovorka left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

oK

@bedaHovorka bedaHovorka merged commit dba2c6e into goal-10 Jul 11, 2026
3 checks passed
@bedaHovorka bedaHovorka deleted the sp0-13/fast-sim-perf branch July 11, 2026 19:26
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.

SP0.13a: gate findNextReservationTarget on reservation-eligibility (~9% fast-sim win)

2 participants