Commit d790870
authored
fix(tsdb): stop the /api/tsdb/stats flash storm; soak evidence for execute_from_psram (#42)
* fix(server): stop /api/tsdb/stats generating a flash storm
Building that response is the only flash access anywhere on the HTTP
path, and on the S3 flash access is what crashes us: every esp_flash op
runs spi_flash_disable_interrupts_caches_and_other_cpu, which stalls the
other core. FlashLock stops httpd and the history writer being inside
flash simultaneously, but nothing stopped them ALTERNATING, and a burst
of cache-disables from both cores faults the writer inside IDF's own
cross-core stall coordination.
Reproduced on the rig, same build, 4 concurrent loops each:
4x /api/tsdb/stats -> crash in ~11s (writer faults in lfs_file_flush)
4x /api/status -> survives, at a HIGHER request rate
/api/status touches no flash. That is the entire difference, which is
why more locking would not have helped.
So stop generating the traffic: build the body at most once per minute,
serve it from PSRAM in between, and have the Diagnostics view fetch it
on open instead of on every 10s tick. Everything in the response only
moves when the writer commits.
Uses psram_string for the cache, not PSRAMString — the latter owns a raw
pointer with no copy constructor, so caching one and copying it out
would double-free.
Does NOT fix the underlying fault, which still reboots the device at
roughly its documented rate. It removes the way the UI provoked it.
Claude-Session: https://claude.ai/code/session_01RgSnMCa3JQigphGnPbazdw
* fix(server): serve /api/tsdb/stats from RAM, never from flash
The previous commit's 60s response cache was not enough. It survived the
kill-load on an idle device for 4 min, then died at 13:29:34 — the exact
instant of a writer commit — when a cache rebuild landed on top of it.
Caching made the collision rarer, not impossible.
Both sides take FlashLock and they still collide; why, is not yet
understood. So stop relying on the lock: remove one of the two parties.
TigoHistory now keeps a StatsSnapshot in RAM, refreshed by the writer
task from inside its own flash batch (after commit_journal_, on slot
assignment, and once at init). The HTTP handler copies that under a
plain std::mutex and formats it. /api/tsdb/stats now touches no flash at
any request rate, which makes it exactly as safe as /api/status — the
endpoint we proved survives unlimited hammering.
Trade: the figures are as fresh as the last commit rather than live. The
response carries snapshot_age_ms and the page renders 'sampled Nm ago'
instead of implying they're current.
Fixtures updated for the new field per CLAUDE.md; screenshots regenerated.
Still does NOT fix the underlying flash-vs-cache fault, which reboots the
device roughly daily on its own.
Claude-Session: https://claude.ai/code/session_01RgSnMCa3JQigphGnPbazdw
* perf(history): halve flash exposure — snapshot every 60 min, not 30
Every snapshot writes 4 TSDB databases (8 fsync'd writes: a data block
and a header rewrite each) plus a journal-commit marker file. Each of
those forces spi_flash_disable_interrupts_caches_and_other_cpu, which
cuts the I-cache on both cores and stalls the other one — the window the
'Fault - Unknown' crash happens in. Exposure scales with how often that
fires, so halving the cadence roughly halves the windows.
Cost: hourly rather than half-hourly panel resolution. Retention goes UP
(~225 days per panel, from ~112). period_e_kwh is energy-since-last-
snapshot so lifetime totals are unaffected by the change.
Cadence now lives in one constant, tigo_history.h:kSnapshotIntervalMin,
with the reasoning attached and the log line derived from it — the last
change to this number left five comments restating the old one, one of
which still said '5-min' after the move to 30. Those are now gone.
This is a probability reduction, not a fix, and unlike the last two
changes it cannot be verified with the 11s reproducer — the background
crash needs a multi-day soak to show any rate change, against samples
spanning 13.5-42h. Do not claim an improvement from one clean run.
Claude-Session: https://claude.ai/code/session_01RgSnMCa3JQigphGnPbazdw
* chore(history): log commit duration at INFO — it IS the crash exposure
Every mid-file write makes LittleFS copy from the write offset to EOF a
byte at a time (lfs_file_flush, lfs.c:3376), and esp_tsdb rewrites the
header at offset 0 on every record — so each commit drags most of each
file through that loop. On this rig that is ~850 KB per commit (system
262 KB + 3x198 KB panels). Both decoded crash PCs (lfs.c:3380 and
lfs.c:3598) are inside that loop, which is simply where the writer
spends nearly all its time.
The writer already measured this, at DEBUG, while the deploy config runs
at INFO — so the single most diagnostic number the component produces
was invisible. One observed commit ran 81 s (11:17:28 tick, dead at
11:18:49) while we assumed commits were milliseconds.
No behaviour change; makes the next commit measurable.
Claude-Session: https://claude.ai/code/session_01RgSnMCa3JQigphGnPbazdw
* docs(issue): retract execute_from_psram from the ruled-out list
It was excluded on the grounds that the crash 'recurs without it' — which
only tests it as a CAUSE. Nobody ever enabled it and measured it as a
CURE, and it is the only candidate found that removes the mechanism
instead of shrinking the window.
IDF 5.5.5 compiles cache_disable out entirely when
CONFIG_SPIRAM_FETCH_INSTRUCTIONS && CONFIG_SPIRAM_RODATA are set, which
is exactly what this flag sets. Verified in the built sdkconfig.h, with
CONFIG_SPI_FLASH_SHARE_SPI1_BUS undefined so no earlier branch wins;
spi1_start then takes a plain mutex. That deletes three frames from the
14:25 backtrace by construction.
'Harmful on S3' was in the notes with nothing behind it. ESPHome's own
S3 test config sets this flag.
Enabled on the rig's deploy config (not in-repo). Soak result pending —
this records the reasoning, not a result.
Claude-Session: https://claude.ai/code/session_01RgSnMCa3JQigphGnPbazdw
* fix(ui): derive the chart resolution label instead of hardcoding it
The power chart said "Power · 5-minute resolution" while the firmware was
sampling hourly — 12x wrong. The cadence has moved twice (5 -> 30 min on
2026-07-23, 30 -> 60 min on 2026-07-28) and the label was never part of
either change, because nothing connected it to kSnapshotIntervalMin.
Both history endpoints now emit interval_min, and app.html renders the
label from it. Hardcoding "60-minute" would have gone stale again the
first time we walk the dial back after the flash-crash soak.
Claude-Session: https://claude.ai/code/session_01RgSnMCa3JQigphGnPbazdw
* fix(esp32): ship execute_from_psram — the actual flash-crash fix
The soak build has run 142 h against prior MTTFs of 13.5/42.0/14.0 h, but
the flag doing the work lived only in the deploy YAML. Merging without
this would have shipped the mitigations and left the cure behind.
Three layers, deliberately redundant:
- codegen (tigo_monitor/__init__.py) sets CONFIG_SPIRAM_FETCH_INSTRUCTIONS
and CONFIG_SPIRAM_RODATA directly, so existing users get the fix without
editing anything. Gated on S3 + PSRAM, matching how ESPHome gates its own
execute_from_psram option (esp32/__init__.py:544). Verified against
test-local-tigomonitor.yaml, which sets no such flag: both symbols land in
the built sdkconfig.h, and neither SPI_FLASH_AUTO_SUSPEND nor
SPI_FLASH_SHARE_SPI1_BUS is defined, so the SPIRAM pair alone satisfies
SPI_FLASH_CACHE_NO_DISABLE.
- boards/esp32s3-atoms3r.yaml sets the flag explicitly, so the reference
config states the requirement rather than inheriting it silently.
- site/boards.js does the same for generated configs (drift test pairs the
two, so they cannot diverge).
Costs ~1.7 MiB of PSRAM, out of the heap, holding relocated instructions
and rodata.
Claude-Session: https://claude.ai/code/session_01RgSnMCa3JQigphGnPbazdw1 parent 1abf7b5 commit d790870
18 files changed
Lines changed: 492 additions & 86 deletions
File tree
- boards
- components
- tigo_monitor
- tigo_server
- web
- docs
- images
- site
- screenshots
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
22 | 26 | | |
23 | 27 | | |
24 | 28 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
20 | 33 | | |
21 | 34 | | |
22 | 35 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
| 6 | + | |
5 | 7 | | |
6 | | - | |
| 8 | + | |
7 | 9 | | |
8 | 10 | | |
9 | 11 | | |
| |||
92 | 94 | | |
93 | 95 | | |
94 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
62 | | - | |
| 62 | + | |
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
97 | | - | |
| 97 | + | |
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
| |||
131 | 131 | | |
132 | 132 | | |
133 | 133 | | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
134 | 139 | | |
135 | 140 | | |
136 | 141 | | |
| |||
171 | 176 | | |
172 | 177 | | |
173 | 178 | | |
174 | | - | |
| 179 | + | |
175 | 180 | | |
176 | 181 | | |
177 | 182 | | |
| |||
195 | 200 | | |
196 | 201 | | |
197 | 202 | | |
198 | | - | |
| 203 | + | |
199 | 204 | | |
200 | 205 | | |
201 | 206 | | |
| |||
381 | 386 | | |
382 | 387 | | |
383 | 388 | | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
384 | 394 | | |
385 | 395 | | |
386 | 396 | | |
| |||
402 | 412 | | |
403 | 413 | | |
404 | 414 | | |
405 | | - | |
| 415 | + | |
406 | 416 | | |
407 | 417 | | |
408 | 418 | | |
| |||
567 | 577 | | |
568 | 578 | | |
569 | 579 | | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
570 | 614 | | |
571 | 615 | | |
572 | 616 | | |
| |||
604 | 648 | | |
605 | 649 | | |
606 | 650 | | |
607 | | - | |
608 | | - | |
| 651 | + | |
| 652 | + | |
609 | 653 | | |
610 | 654 | | |
611 | 655 | | |
| |||
638 | 682 | | |
639 | 683 | | |
640 | 684 | | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
641 | 691 | | |
642 | 692 | | |
643 | 693 | | |
644 | 694 | | |
645 | 695 | | |
646 | 696 | | |
647 | | - | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
648 | 705 | | |
649 | 706 | | |
650 | 707 | | |
| |||
0 commit comments