Skip to content

Guard the export progress divisions that produced Infinity - #1838

Merged
jcschaff merged 1 commit into
masterfrom
fix/export-progress-division
Aug 6, 2026
Merged

Guard the export progress divisions that produced Infinity#1838
jcschaff merged 1 commit into
masterfrom
fix/export-progress-division

Conversation

@jcschaff

@jcschaff jcschaff commented Aug 6, 2026

Copy link
Copy Markdown
Member

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 — so x / 0.0 silently yields Infinity (or NaN when x is 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's TOTAL_EXPORTS_OPS:

SIM_COUNT * PARAMSCAN_COUNT * variableNames.length
  * (geometrySpecs.getCurves().length + (getPointCount() > 0 ? 1 : 0))

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.

Correction to my earlier analysis in #1837: I first suspected ASCIIExporter's per-variable loop. That is not reachable — both enclosing loops guarantee variableNames.length >= 1 and endIndex >= 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 every EXPORT_PROGRESS event 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

fraction(  5.0, 10.0) = 0.5   finite, in [0,1] ✓
fraction(  0.0,  0.0) = 0.0   ✓   (was NaN — no curves and no points)
fraction(  3.0,  0.0) = 0.0   ✓   (was Infinity)
fraction( -1.0, 10.0) = 0.0   ✓
fraction( 15.0, 10.0) = 1.0   ✓
fraction(  NaN, 10.0) = 0.0   ✓

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

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant