Card version: v3.3.5 (commit 20e9686)
Summary
Cache.add() merges freshly fetched history into the cumulative cache using a spread call:
// src/cache/Cache.ts, add()
h.push(...states);
When states has tens of thousands of elements (a raw, non-statistics entity with a high
state-change rate, viewed over a multi-day range), this spread throws
RangeError: Maximum call stack size exceeded — this is the JS engine's argument-count limit
for spread/apply-style calls, not "real" recursion, but the resulting error is identical.
How it surfaces to users
Because ConfigParser.evalNode() wraps each per-entity property evaluation in its own
try/catch (parse-config.ts), the crash doesn't abort the whole card — it gets caught and
re-thrown, once independently, for every $fn-valued default property of the affected
entity that triggers a fetch (on_legend_click, on_legend_dblclick, on_click,
unit_of_measurement, hovertemplate, yaxis, or line.color if the entity has no explicit
color). Each of these properties independently calls fetchDataForEntity() again (since the
throw happens before this.fnParam.xs is ever set), so the same crash — and a full history
re-fetch — happens repeatedly per render attempt. The end-user symptom is a card-level error
block listing several unrelated-looking config paths, e.g.:
Error: at [entities.6.on_legend_click / on_legend_dblclick / on_click /
unit_of_measurement / hovertemplate / yaxis]: Maximum call stack size exceeded
Error: at [entities.7.line.color]: Maximum call stack size exceeded
Reproduction / evidence
Card config: 19-trace stacked area chart (fill: tonexty/tozeroy) over several distinct
Home Assistant power sensors (state_class: measurement, no statistic/period set — i.e.
raw states, ~10s update cadence from the underlying source). Rangeselector buttons up to 14
days. Counting raw states rows per sensor over trailing windows (from the HA recorder DB):
| entity |
3d |
7d |
14d |
| sensor.pv_bilanz_verb_c2 |
72,883 |
169,991 |
240,056 |
| sensor.pv_hauslast_negativ |
73,286 |
171,009 |
242,699 |
| sensor.pv_bilanz_prod_c2 |
53,574 |
127,134 |
179,827 |
| sensor.pv_echte_leistung_gesamt |
43,221 |
103,068 |
181,691 |
| sensor.pv_bilanz_verb_c1 |
46,959 |
110,714 |
156,441 |
| (6 more sensors on the same card) |
8k–21k |
20k–49k |
36k–77k |
The crash was first observed (in production) at the "3d" button, on the two entities with the
highest point counts at that window (72,883 and 73,286) — consistent with a hard argument-count
ceiling somewhere in the 65k-ish range (varies by JS engine/platform; mobile WebKit is known to
have a lower ceiling than desktop V8). At 7d/14d essentially all raw-state traces on this card
are past the danger zone.
Expected behavior
Large raw-state histories should merge into the cache without hitting an engine argument limit.
Suggested fix
Replace the spread with an iterative push (functionally identical, no behavior change, avoids
building a giant argument list):
- h.push(...states);
+ for (const s of states) h.push(s);
(equivalently states.forEach((s) => h.push(s))). Happy to open a PR if useful — this is a
one-line change with no test coverage implications I can see, but I don't know your preferred
contribution process.
Workaround considered and rejected on our end
We considered switching the affected entities to statistic/period (long-term statistics)
instead of raw states, which sidesteps the point-count problem entirely. We didn't do this
because (a) it permanently reduces resolution even for short (1h/6h) ranges where raw data
would render fine, and (b) our traces rely on exact per-point min/max relationships between
several derived sensors to guarantee correct stacking (fill: tonexty), which isn't preserved
by independently-averaged 5-minute means. We'd rather wait for/contribute the real fix than
carry that trade-off.
Card version: v3.3.5 (commit
20e9686)Summary
Cache.add()merges freshly fetched history into the cumulative cache using a spread call:When
stateshas tens of thousands of elements (a raw, non-statistics entity with a highstate-change rate, viewed over a multi-day range), this spread throws
RangeError: Maximum call stack size exceeded— this is the JS engine's argument-count limitfor spread/
apply-style calls, not "real" recursion, but the resulting error is identical.How it surfaces to users
Because
ConfigParser.evalNode()wraps each per-entity property evaluation in its owntry/catch (
parse-config.ts), the crash doesn't abort the whole card — it gets caught andre-thrown, once independently, for every
$fn-valued default property of the affectedentity that triggers a fetch (
on_legend_click,on_legend_dblclick,on_click,unit_of_measurement,hovertemplate,yaxis, orline.colorif the entity has no explicitcolor). Each of these properties independently calls
fetchDataForEntity()again (since thethrow happens before
this.fnParam.xsis ever set), so the same crash — and a full historyre-fetch — happens repeatedly per render attempt. The end-user symptom is a card-level error
block listing several unrelated-looking config paths, e.g.:
Reproduction / evidence
Card config: 19-trace stacked area chart (
fill: tonexty/tozeroy) over several distinctHome Assistant power sensors (
state_class: measurement, nostatistic/periodset — i.e.raw states, ~10s update cadence from the underlying source). Rangeselector buttons up to 14
days. Counting raw
statesrows per sensor over trailing windows (from the HA recorder DB):The crash was first observed (in production) at the "3d" button, on the two entities with the
highest point counts at that window (72,883 and 73,286) — consistent with a hard argument-count
ceiling somewhere in the 65k-ish range (varies by JS engine/platform; mobile WebKit is known to
have a lower ceiling than desktop V8). At 7d/14d essentially all raw-state traces on this card
are past the danger zone.
Expected behavior
Large raw-state histories should merge into the cache without hitting an engine argument limit.
Suggested fix
Replace the spread with an iterative push (functionally identical, no behavior change, avoids
building a giant argument list):
(equivalently
states.forEach((s) => h.push(s))). Happy to open a PR if useful — this is aone-line change with no test coverage implications I can see, but I don't know your preferred
contribution process.
Workaround considered and rejected on our end
We considered switching the affected entities to
statistic/period(long-term statistics)instead of raw states, which sidesteps the point-count problem entirely. We didn't do this
because (a) it permanently reduces resolution even for short (1h/6h) ranges where raw data
would render fine, and (b) our traces rely on exact per-point min/max relationships between
several derived sensors to guarantee correct stacking (
fill: tonexty), which isn't preservedby independently-averaged 5-minute means. We'd rather wait for/contribute the real fix than
carry that trade-off.