Skip to content

fix(mpc): skip Core DP shadows that cannot finish in time - #1509

Merged
frahlg merged 2 commits into
masterfrom
fix/dp-shadow-skip-slow
Oct 3, 2026
Merged

frahlg merged 2 commits into
masterfrom
fix/dp-shadow-skip-slow

Conversation

@frahlg

@frahlg frahlg commented Oct 3, 2026

Copy link
Copy Markdown
Member

Problem and result

Owner request: stop the Core DP shadow from wasting CPU where it cannot finish.

After Core publishes an Energyplan plan, it runs Core DP in the background, with a 10 s limit, to compare the two plans. Stored diagnostics from the home box (Raspberry Pi 4, v0.138 and v0.139 betas, 26 Sep to 3 Oct) show:

Replans Energyplan backend Shadow result
228 with a car plugged in fleet_milp_rust 213 ran out of time after about 10.1 s. 5 finished in 9.4–9.8 s, all on horizons of 72 slots or fewer. 10 have no result: the next replan began within 12 s and cancelled them.
826 battery only value_curve_rust All finished: median 0.75 s, 90th percentile 1.6 s, max 2.0 s.

So almost every replan with a car spent 10 s of CPU, and the heat and power that go with it, on a comparison that never came. Replans seldom overlap (10 of 1 057 started within 10 s of the one before), so the cost is CPU, not a late plan.

With this PR, after a shadow runs out of time, Core skips shadows with at least as much DP work per slot for an hour, then tries one again. DP work per slot is battery SoC levels × battery power levels, times EV SoC levels × charger steps when a car is plugged in. On the home box an EV shadow has about 30 times the work per slot of a battery-only one, so battery-only shadows keep running while EV shadows wait.

Core records each skip instead of leaving a gap:

  • dp_shadow.solver.status is skipped, and fallback_reason reads skipped: a shadow no larger than this one ran out of time; next try after <UTC time>. The live plan, /api/mpc/diagnose and the stored diagnostic all carry it. A missing comparison still has no dp_shadow block.
  • The plan view already shows a block with no compared slots as "DP shadow unavailable", with fallback_reason in the tooltip, as it does for a timeout today. No UI change.
  • Logs: one WARN mpc: Core DP shadow ran out of time; skipping shadows this large per timeout, and one INFO mpc: Core DP shadow skipped per skip, both with the decision ID.

Replayed over the same records, the rule leaves 20 timeouts instead of 213. It keeps 3 of the 5 EV comparisons that finished and all 826 battery-only ones. Shadow CPU over the week drops from about 2 900 s to about 930 s; EV shadows alone drop from about 2 200 s to 230 s.

Why this rule:

  • A timeout is direct proof that a shadow of that size does not fit this box, and it adapts to the host. A fast box that never times out keeps every comparison.
  • Work per slot, not total work, decides. The horizon shrinks by one slot every 15 minutes, so total work falls a little at each replan, and a total-work rule would retry nearly every time.
  • The hourly retry finds the point where a shorter horizon fits. On the Pi, EV shadows finished only at 72 slots or fewer.
  • No new setting. The 10 s deadline becomes a named constant.

Scope and safety

  • Only the background comparison changes. The active plan, dispatch, plan validation and the Energyplan request do not.
  • Only the deadline marks a size as slow. A shadow cancelled by a newer replan or by Stop does not.
  • The mark lives in memory. After a restart, the first shadow of that size runs and may time out once more.
  • A clock stepped back after boot cannot stretch a skip: a deadline more than an hour ahead counts as expired.
  • A skip writes its diagnostic during the replan call, after publication and the main persist. A timeout wrote the same record from the background goroutine.
  • Risks:
    • A box that is busy for other reasons can time out a shadow that would normally fit. Core then skips that size for an hour.
    • On the Pi, 2 of the 5 EV comparisons that finished during the week fall inside a skip hour and would be lost.
    • The skip reason names a UTC time; the plan view shows local times elsewhere.
    • The shadow is the acceptance measure in Move the optimizer back into Core — staged, evidence-gated #1020. On the Pi, EV comparisons were already absent as timeouts; the skips record that rather than change it.

Verification

  • New tests in go/internal/mpc/core_dp_shadow_test.go:
    • TestCoreDPShadowSkipsAfterTimeout: after a timeout, the next shadow of the same size records skipped, with the reason and the next try, in the live plan, Diagnose() and the stored diagnostic.
    • TestCoreDPShadowSkipKeepsSmallerShadows: a battery-only shadow runs during an EV skip.
    • TestCoreDPShadowRetriesAfterAnHour: skipped at 59 minutes, compared at 60.
    • TestCoreDPShadowCancellationDoesNotSkip: a cancelled shadow causes no skip.
    • TestCoreDPShadowSkipIgnoresClockStepBack: after the clock steps back three hours, the shadow runs. It fails without the one-hour bound.
    • TestNativeEnergyplanSkipsEVShadowAfterTimeout: the same through real Replan calls, with the bundled Energyplan worker and a car from the loadpoint probe.
  • Without the fix, SkipsAfterTimeout and RetriesAfterAnHour fail: the shadow runs again. With the skip check disabled, those two and the native test fail. Marking on cancellation makes CancellationDoesNotSkip fail.
  • go test -race -count=20 ./internal/mpc -run 'TestCoreDPShadow|TestNativeEnergyplanSkipsEVShadowAfterTimeout|TestNativeEnergyplanDownsideAndAsyncShadow', with FTW_NATIVE_SOLVER set to the darwin-arm64 worker: pass.
  • go test -race ./internal/mpc: pass.
  • make verify on the final commit: pass (68 Go packages, native worker check, vet and build).
  • Not run on the Pi. The field numbers come from the box's stored diagnostics; the replay applies the rule to those records offline.

Checklist

  • The change follows VISION.md and one selected scope.
  • I checked overlapping PRs and coordinated shared files/contracts. No open PR touches go/internal/mpc. fix(web): pause plan, heating, settings, and card polls when hidden #1177 touches web/plan.js; this PR leaves it alone.
  • Relevant checks cover the changed behaviour and failure paths.
  • A human reviewed changed UI in a browser, or no UI changed. No UI changed.
  • A Changeset is included (patch), or the change is exempt.
  • Every commit has a DCO sign-off.

🤖 Generated with Claude Code

frahlg and others added 2 commits October 3, 2026 15:12
On the home box (Raspberry Pi 4, Energyplan primary, 26 Sep to 3 Oct),
the Core DP shadow ran out of its 10 s limit on 213 of 228 replans with
a car plugged in. Each try cost about 10 s of CPU and compared nothing.
Battery-only shadows finished all 826 times, in 2 s at most.

After a shadow runs out of time, Core now skips shadows with at least
as much DP work per slot (battery SoC and power levels, times EV SoC
levels and charger steps) for an hour, then tries one again. The
horizon shrinks through the day, so a later try may fit. Each skip is
recorded in dp_shadow with status "skipped", the reason and the time of
the next try, and logged, so the plan view and /api/mpc/diagnose tell a
skip from a missing comparison. Smaller shadows still run. A cancelled
shadow does not count as slow. The active plan, dispatch, validation
and the Energyplan request do not change.

Replayed over the same records, the rule leaves 20 timeouts instead of
213 and keeps 3 of the 5 EV comparisons that finished, plus every
battery-only one.

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Pi can boot with a wrong time and step it back later. The skip deadline
came from the old clock, so a step back stretched the skip by the size of
the step. Treat a deadline more than an hour ahead as expired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@frahlg
frahlg marked this pull request as ready for review October 3, 2026 16:17
@frahlg
frahlg merged commit 9246e4f into master Oct 3, 2026
15 checks passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-03T16:21:01.443767Z fc25ba8 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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.

1 participant