Skip to content

Commit 37c6539

Browse files
committed
revert(history): drop the esp_tsdb fork pin — it changed nothing
Measured on the rig, before and after the single-fsync patch: tsdb_write ok (sys 5135 ms, panels 15854 ms) # 2.3.0, 8 fsyncs tsdb_write ok (sys 5349 ms, panels 17269 ms) # patched, 4 fsyncs No improvement. The reasoning was wrong: removing the first fsync doesn't remove flash work, it defers it to the trailing lfs_file_sync, which then flushes the block and the header together. Same erases, same COW churn. fsync cost tracks outstanding dirty work, not call count — the LD_PRELOAD counter measured the wrong thing. So the pin bought nothing and cost fork maintenance. Back to the registry in the board YAML, the Config Builder and the deploy config. Upstream PR retracted. Also strips every claim built on that model: the "twice as fast" CHANGELOG entry (deleted), "~10.5 s" in the header comment, "~10 s to commit" in the build warning, and the docs' commit-duration figures — all back to the measured ~21 s. The 21 s is now explicitly documented as NOT understood, with both dead theories named so the next person doesn't re-derive them: it is not proportional to file size (1 MB and 192 KB DBs cost the same), and it is not fsync count (halving it changed nothing). ~5.3 s per database, per commit, cause unknown. Docs carry a caution box saying so. Claude-Session: https://claude.ai/code/session_01RgSnMCa3JQigphGnPbazdw
1 parent 011cdac commit 37c6539

6 files changed

Lines changed: 40 additions & 48 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
### Fixed
2323
- **Updating the firmware could crash the device.** The history writer is meant to stand down while an update is being written, since both are writing the same flash chip — but it only checked for an update at the moment it started a snapshot, and a snapshot takes ten to twenty seconds to write. An update beginning during one was ignored until it was too late, and the two collided. Starting an update now waits for any snapshot in progress to finish before transferring, so the collision can't happen. If you update at the wrong moment you'll see a short pause and a "waited N ms" message before the transfer begins; that's the wait working. An update is refused outright if a snapshot somehow doesn't finish within 45 seconds, rather than proceeding into a known crash.
2424

25-
- **Saving history is twice as fast.** Each snapshot writes four databases, and every write was syncing to flash twice — once for the data, once for the record header — when the second sync already covered both. On LittleFS a sync is a full journal commit costing seconds, and that cost turned out to be per sync rather than per byte: a 1 MB database and a 192 KB one took the same time, whether appending or overwriting. Dropping the redundant sync takes a snapshot from about 21 seconds to about 10. That matters beyond the writing itself, because the device holds its history lock for the whole commit — so it's also how long a chart request could be stuck waiting. Sent upstream as [zakery292/esp_tsdb#5](https://github.com/zakery292/esp_tsdb/pull/5); until it lands in a release the firmware builds against a pinned fork.
26-
- **History pages no longer fail when a snapshot is being written.** Requests gave up after 15 seconds, which was less than a snapshot took, so any chart loaded at the wrong moment returned an error. They now wait up to 30 seconds and get their data.
25+
- **History pages no longer fail when a snapshot is being written.** Requests gave up after 15 seconds, which is less than a snapshot takes, so any chart loaded at the wrong moment returned an error. They now wait up to 30 seconds and get their data.
2726

2827
- **The flash-write crash appears to be fixed, not just made rarer.** The device now executes from PSRAM (`execute_from_psram`), and firmware configures this itself — you don't have to know it exists. Every history commit takes an ESP-IDF lock that disables the instruction cache across both cores; with code running from flash, that stall raced the WiFi/BLE radio ISRs and faulted. ESP-IDF skips the cache-disable entirely when instructions and read-only data live in PSRAM instead, which removes the race rather than shrinking its window. Previous builds survived 13.5, 42.0 and 14.0 hours before faulting; the current one has run **142 hours and counting** with no movement in any memory watermark, and with the per-panel databases already at full size — so it has been running at the worst-case flash cost the whole time, not easing into it. Costs about 1.7 MB of the 8 MB PSRAM, which moves out of the heap to hold the relocated code. Applies to ESP32-S3 boards with PSRAM configured; the reference config and the Config Builder set the flag explicitly as well. Note that the 142-hour run was at the old hourly cadence, and the new default writes twice as often — so the fix now carries more load than the run that demonstrated it.
2928

boards/esp32s3-atoms3r.yaml

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,10 @@ esp32:
3333
components:
3434
# Time-series database for persistent history
3535
# (see https://rar.github.io/esphome-tigomonitor/guides/tsdb-integration/).
36-
# 2.1.0 was the first upstream release with the handle-based multi-instance
36+
# 2.1.0 is the first upstream release with the handle-based multi-instance
3737
# API and the wrapped-ring query fix this project depends on; the caret
38-
# resolved to 2.3.0.
39-
#
40-
# TEMPORARILY PINNED TO A FORK. v2.3.0 fsyncs twice per record write — once
41-
# for the data block, once for the header — and on LittleFS each fsync is a
42-
# full journal commit costing ~2.6 s regardless of file size. Four databases
43-
# per snapshot made that a ~21 s stall, long enough to block every history
44-
# query behind it. The branch drops the redundant first fsync (8 fsyncs per
45-
# snapshot -> 4). Upstream PR: zakery292/esp_tsdb#5 — revert this to
46-
# `zakery292/esp_tsdb^2.4.0` once it lands in a release.
47-
- name: zakery292/esp_tsdb
48-
source: https://github.com/RAR/esp_tsdb.git
49-
ref: perf/single-fsync-per-write
38+
# resolves to 2.3.0.
39+
- zakery292/esp_tsdb^2.1.0
5040
# LittleFS — backing filesystem for the tsdb partition
5141
- joltwallet/littlefs^1.16
5242
sdkconfig_options:

components/tigo_monitor/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ def _warn_history_wear(config):
3838
"""Flag intervals that buy resolution with flash life.
3939
4040
Each snapshot commits four databases, and every commit stalls the writer for
41-
seconds while holding the filesystem lock — measured at ~21 s on upstream
42-
esp_tsdb 2.3.0, ~10.5 s with the redundant fsync removed. That cost is per
41+
seconds while holding the filesystem lock — measured at ~21 s. That cost is per
4342
commit, so a shorter interval spends proportionally more of the device's
4443
time in it, and flash wear rises the same way. Runs as a validator rather
4544
than in to_code so `esphome config` surfaces it too.
@@ -48,7 +47,7 @@ def _warn_history_wear(config):
4847
if minutes < 15:
4948
_LOGGER.warning(
5049
"history_interval is %d min, %.1fx more often than the 30 min "
51-
"default. Each snapshot takes ~10 s to commit and holds the "
50+
"default. Each snapshot takes ~21 s to commit and holds the "
5251
"filesystem for all of it, so history pages queue behind it that "
5352
"much more often, and flash wear rises by the same factor. "
5453
"Per-panel history also shrinks to ~%d days.",

components/tigo_monitor/tigo_history.h

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,20 @@ namespace tigo_monitor {
4646
//
4747
// Two costs still scale with it, and neither was fixed by that flag:
4848
//
49-
// * A commit stalls the writer for seconds and holds the flash lock the whole
50-
// time, so history queries queue behind it. Measured at ~21 s (sys 5135 ms
51-
// + panels 15854 ms) against upstream esp_tsdb 2.3.0; ~10.5 s once the
52-
// redundant fsync is gone (zakery292/esp_tsdb#5, currently pinned as a
53-
// fork). The cost is per fsync and per database — NOT per byte: a 1 MB DB
54-
// and a 192 KB DB measured the same, appending or evicting.
49+
// * A commit stalls the writer ~21 s and holds the flash lock the whole time,
50+
// so history queries queue behind it (sys 5349 ms + panels 17269 ms,
51+
// esp_tsdb 2.3.0, panel rings full).
5552
// * Flash wear scales with 1/interval, since the write volume per commit is
5653
// roughly fixed. The absolute figure has not been measured; don't quote one.
5754
//
58-
// The floor is 4 fsyncs per snapshot (one per open database), so going below
59-
// ~10 s needs either fewer databases or syncing every Nth snapshot.
55+
// The 21 s is NOT understood, and two plausible explanations have been measured
56+
// and killed. It is not proportional to bytes: a 1 MB DB and a 192 KB DB cost
57+
// the same, appending or evicting. It is not the fsync count either: halving
58+
// fsyncs per write (8 -> 4 per snapshot) changed nothing, because the removed
59+
// sync's work simply moved to the remaining one. Whatever dominates is per
60+
// commit and per database, ~5.3 s each. Anyone shortening the interval should
61+
// know this cost is currently a fixed, unexplained tax — measure before
62+
// theorising, and don't trust a third guess without numbers.
6063
//
6164
// Retention scales with the interval too, since each DB holds a fixed record
6265
// count: the 5404-record panel rings span ~112 days at 30 min, ~37.5 at 10;

site/boards.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,8 @@ export const BOARDS = [
1010
// out ESP-IDF's cross-core cache-disable, which was racing WiFi/BLE-coex
1111
// ISRs on every history commit (MTTF 13.5-42 h). Costs ~1.7 MiB of PSRAM.
1212
frameworkAdvanced: { enable_idf_experimental_features: false, execute_from_psram: true },
13-
frameworkComponents: ['joltwallet/littlefs^1.16'],
14-
// Temporarily a fork pin: upstream v2.3.0 fsyncs twice per record write, and
15-
// on LittleFS each fsync is a ~2.6 s journal commit — four DBs per snapshot
16-
// made a ~21 s stall that blocked every history query behind it. Branch drops
17-
// the redundant one. Revert to 'zakery292/esp_tsdb^2.4.0' in
18-
// frameworkComponents once zakery292/esp_tsdb#5 ships.
19-
hostedComponent: { source: 'https://github.com/RAR/esp_tsdb.git', ref: 'perf/single-fsync-per-write' },
13+
frameworkComponents: ['zakery292/esp_tsdb^2.1.0', 'joltwallet/littlefs^1.16'],
14+
hostedComponent: null,
2015
sdkconfig: {
2116
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: 'y',
2217
CONFIG_UART_ISR_IN_IRAM: 'y',

site/src/content/docs/guides/tsdb-integration.md

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,23 +80,29 @@ esp32:
8080
`history_interval` accepts 5 to 1440 minutes and defaults to 30. Everything
8181
scales linearly with it, in both directions:
8282

83-
| Interval | Per-panel history | System history | Device time spent committing | Chart resolution |
84-
|----------|-------------------|----------------|------------------------------|------------------|
85-
| 10 min | ~5 weeks | ~7 months | ~1.8% | finest |
86-
| **30 min (default)** | **~4 months** | **~2 years** | **~0.6%** | balanced |
87-
| 60 min | ~7.5 months | ~3.7 years | ~0.3% | coarse |
83+
| Interval | Per-panel history | System history | Device time spent writing | Chart resolution |
84+
|----------|-------------------|----------------|---------------------------|------------------|
85+
| 10 min | ~5 weeks | ~7 months | ~3.5% | finest |
86+
| **30 min (default)** | **~4 months** | **~2 years** | **~1.2%** | balanced |
87+
| 60 min | ~7.5 months | ~3.7 years | ~0.6% | coarse |
8888

8989
The low end costs more than the resolution alone suggests. Writing a snapshot
90-
takes about 10 seconds — four databases, each needing a filesystem sync that
91-
LittleFS implements as a full journal commit — and the device holds the history
92-
lock for all of it, so chart requests arriving mid-commit have to wait it out
93-
(they allow 30 s before giving up). Flash wear rises by the same factor. Values
94-
under 15 minutes log a build-time warning.
95-
96-
That cost is per commit rather than per byte: a 1 MB database and a 192 KB one
97-
were measured taking the same time, whether appending or overwriting. So it
98-
scales with how *often* you write, not how much you store — which is exactly why
99-
the interval is the lever that matters.
90+
takes about **21 seconds**, and the device holds its history lock for all of it,
91+
so chart requests arriving mid-write have to wait it out (they allow 30 s before
92+
giving up). Flash wear rises by the same factor. Values under 15 minutes log a
93+
build-time warning.
94+
95+
That 21 seconds is a fixed cost per snapshot, not proportional to how much data
96+
you store — a 1 MB database and a 192 KB one measure the same. So the interval
97+
controls how much of the device's time goes into writing history, and it's the
98+
only lever that does.
99+
100+
:::caution[Known limitation]
101+
21 seconds is slower than it should be and the cause isn't yet understood. Two
102+
likely explanations were tested and ruled out: it doesn't scale with file size,
103+
and it isn't the number of filesystem syncs. Until that's solved, treat short
104+
intervals as genuinely expensive rather than merely finer.
105+
:::
100106

101107
Changing the interval later is safe: `period_e_*` stores energy since the
102108
previous snapshot rather than a running total, so lifetime figures stay correct

0 commit comments

Comments
 (0)