Guard the export progress divisions that produced Infinity - #1838
Merged
Conversation
Export progress denominators are counts that can legitimately be zero, and
they are doubles — so x / 0.0 silently yields Infinity (or NaN when x is 0)
instead of throwing. That value was published as progress, reached JSON
serialization, and made the event permanently undeliverable.
The reachable case is ASCIIExporter's TOTAL_EXPORTS_OPS:
SIM_COUNT * PARAMSCAN_COUNT * variableNames.length
* (geometrySpecs.getCurves().length + (getPointCount() > 0 ? 1 : 0))
which is zero for a geometry selection with no curves and no points, or
with no variables chosen. RasterExporter.fireThrottledProgress divides by
endIndex+1, zero for an empty range.
Note the earlier suspect, ASCIIExporter's per-variable loop, is NOT
reachable: both enclosing loops guarantee variableNames.length >= 1 and
endIndex >= beginIndex, so that denominator is always >= 2.
Three changes:
- ASCIIExporter.fraction(completed, total) returns a value in [0,1] and 0
when there is nothing to divide by; the four progress divisions now use
it, and a zero total is logged once with the counts that caused it.
- RasterExporter.fireThrottledProgress uses the same helper.
- ClientExportEventController.fireExportProgress, the single place every
EXPORT_PROGRESS event is constructed, rejects a non-finite value as a
backstop — so no exporter can leak one even if it computes progress some
other way.
Verified: fraction(5,10)=0.5, (0,0)=0, (3,0)=0, (-1,10)=0, (15,10)=1,
(NaN,10)=0 — every case finite and within [0,1].
Complements the containment in RestEventService (#1837), which stops such
a value from wedging delivery; this stops it being produced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SY1XHgTZXZPqUECo2VBAWg
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root-cause companion to #1837, which contains the symptom. This stops the bad value being produced.
Cause
Export progress denominators are counts that can legitimately be zero, and they are
doubles — sox / 0.0silently yieldsInfinity(orNaNwhenxis 0) instead of throwing. That value was published as export progress, reached JSON serialization, and made the event permanently undeliverable — ~4,900 error lines a minute on production until the pods were rolled.The reachable case is
ASCIIExporter'sTOTAL_EXPORTS_OPS:zero for a geometry selection with no curves and no points, or with no variables chosen.
RasterExporter.fireThrottledProgressdivides byendIndex+1, zero for an empty range.Correction to my earlier analysis in #1837: I first suspected
ASCIIExporter's per-variable loop. That is not reachable — both enclosing loops guaranteevariableNames.length >= 1andendIndex >= beginIndex, so that denominator is always ≥ 2. The real culprit is the block above.Changes
ASCIIExporter.fraction(completed, total)— returns a value in [0,1], and 0 when there is nothing to divide by. The four progress divisions use it, and a zero total is logged once with the counts that caused it, so the offending export is identifiable.RasterExporter.fireThrottledProgress— same helper.ClientExportEventController.fireExportProgress— the single place everyEXPORT_PROGRESSevent is constructed, now rejects a non-finite value as a backstop. Guarding ~15 scattered call sites individually would be easy to get wrong; this one chokepoint is provably complete.Verified
Both edited exporters are CRLF files; line endings are preserved (diff is 45 insertions / 5 deletions, no whitespace churn).
🤖 Generated with Claude Code
https://claude.ai/code/session_01SY1XHgTZXZPqUECo2VBAWg