You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Split out of #574, which fixed the crash but deliberately left this. #574's own fix list had a fourth item — "Consider a depth cap: past N nodes, collapse to a named leaf holding the value. An explanation 200,000 steps deep is not an explanation." — and PR #581 did not do it, because it changes equation()'s documented output contract rather than fixing a defect. Filing it so closing #574 does not lose it, together with the cost measurement #574 did not have.
Verification status: reproduced
Revision: fix-574-quantity-provenance-locale @ 3a1effd0 (PR #581), i.e. after the iterative-traversal fix. Nothing below is a crash any more; this is purely about what the output costs and what it is worth.
equation() builds its formula by repeated concatenation in EquationRenderer::combine, so rendering a chain of n derivation nodes copies O(n²) bytes, twice (once symbolic, once substituted). Measured on clang 22.1.8, running total = total + onen times and then calling equation():
n
wall, -O1 + ASan+UBSan
first output line
40,000
11.2 s
200,001 chars
70,000
32.3 s
350,001 chars
100,000
82.8 s
500,001 chars
$ ./prov_san 100000
equation() lines=4 first=500001 chars
destroyed cleanly
100000 16.87s user 65.47s system 99% cpu 1:22.81 total
The shape of that first line is 0 + c1 + c1 + c1 + ... repeated 100,000 times, with a three-line where c1 = 1 legend under it. It is returned as a std::vector<std::string> of "print-ready lines", so a caller that prints it emits a single half-megabyte line.
Why it matters
Two separate things, and they may want different answers:
A depth cap fixes both. Reserving the output would fix only the second (the string still has to be built).
What would change the verdict
Close as "works as intended" if a derivation that deep is considered out of contract for equation() — but then the contract should say so, and say what happens instead, because today nothing bounds it.
Whether reserving in combine (instead of capping) brings the cost down enough to matter — the quadratic copying is inherent to building one string per level, so probably not, but it was not measured.
What a good cap would be. "200,000 steps is not an explanation" is clear; "50 is" is not obviously true.
Split out of #574, which fixed the crash but deliberately left this. #574's own fix list had a fourth item — "Consider a depth cap: past N nodes, collapse to a named leaf holding the value. An explanation 200,000 steps deep is not an explanation." — and PR #581 did not do it, because it changes
equation()'s documented output contract rather than fixing a defect. Filing it so closing #574 does not lose it, together with the cost measurement #574 did not have.Verification status: reproduced
Revision:
fix-574-quantity-provenance-locale@3a1effd0(PR #581), i.e. after the iterative-traversal fix. Nothing below is a crash any more; this is purely about what the output costs and what it is worth.equation()builds its formula by repeated concatenation inEquationRenderer::combine, so rendering a chain of n derivation nodes copies O(n²) bytes, twice (once symbolic, once substituted). Measured on clang 22.1.8, runningtotal = total + onen times and then callingequation():-O1+ ASan+UBSanThe shape of that first line is
0 + c1 + c1 + c1 + ...repeated 100,000 times, with a three-linewhere c1 = 1legend under it. It is returned as astd::vector<std::string>of "print-ready lines", so a caller that prints it emits a single half-megabyte line.Why it matters
Two separate things, and they may want different answers:
docs/spec/util/quantity_type.mddescribesequation()as "the worked formula as print-ready lines" — something a domain user reads to see why a number is what it is. Half a megabyte of+ c1is not that. util/render: two reproduced value-type defects that ship by default — Quantity provenance chains and unvalidated locale group-separator stripping #574's suggestion was to collapse past some depth to a named leaf holding the value.equation()on the result. Since Fix two shipped value-type defects: provenance chains that overflow the stack, and locale group separators that are stripped instead of validated #581 the walk no longer overflows the stack, so a deep chain now reaches this instead of crashing — which is the right trade, but it means the cost is reachable where the crash used to be.A depth cap fixes both. Reserving the output would fix only the second (the string still has to be built).
What would change the verdict
equation()— but then the contract should say so, and say what happens instead, because today nothing bounds it.Not verified
combine(instead of capping) brings the cost down enough to matter — the quadratic copying is inherent to building one string per level, so probably not, but it was not measured.