Fix two more divide-by-zero bugs found by the formalization audit - #1078
Merged
Conversation
…only) The stacks-normalization loop in ScaleneJSON.output_profiles divided each recorded stack's timings by stats.cpu_stats.total_cpu_samples with no guard. That denominator can be 0.0 while stats.stacks is non-empty: a memory-only run with --stacks records stack entries (from the CPU sampler's stack collection) while it is *memory* activity -- not CPU activity -- that passes the 'nothing to output' gate at the top of output_profiles. With no CPU sample ever recorded, total_cpu_samples stays at its initial 0.0 and the loop raises ZeroDivisionError. The sibling per-file/per-line CPU normalizations (~556, ~1259, ~1337) already guard this; this site was missed. Same bug class as the leak-velocity divide (#1077). Found by re-running the formalization audit's 'every denominator is a claim to verify' sweep across the output path (formal/README.md 'Bugs the formalization found', now three). Guard the loop on total_cpu_samples (raw stack entries preserved when 0). Regression test drives the full output_profiles path, so it crashes pre-fix and passes after.
Bug #1 (fixed in #1077) was an unguarded leak_velocity / stats.elapsed_time in the JSON renderer. Scalene has three separate output renderers (Scalene-Debugging.md); the CLI renderer scalene_output.py:699 (scalene view --cli) carried the identical unguarded divide, which #1077 did not touch. Same reachability as #1: compute_leaks gates on allocation growth rate, not wall-clock time, so a leak can be reported on a sub-millisecond run where elapsed_time is still 0.0 -> ZeroDivisionError. Found by re-running the denominator audit across the CLI output path (the other output.py divides at ~339/~379/~416/~657 are already guarded). Guard the velocity denominator, mirroring the #1077 json.py fix. Regression test added. Also updates formal/README.md + HANDOFF.md (now four bugs found). The takeaway, now recorded in HANDOFF: a fix in one renderer does not cover its siblings -- audit all three.
This was referenced Jul 1, 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.
Two more divide-by-zero defects of the same class as #1077, found by re-running the formalization audit's "every denominator is a claim to verify against the code" sweep (
formal/README.md→ "Bugs the formalization found") across all three of Scalene's output renderers.Bug 1 — per-stack CPU normalization (
scalene_json.py)The
stats.stacksnormalization loop divided bystats.cpu_stats.total_cpu_sampleswith no guard. That denominator can be0.0whilestats.stacksis non-empty:--stacksrecords stack entries (the CPU sampler's stack collection runs), but it is memory activity — not CPU — that passes the "nothing to output" gate at the top ofoutput_profiles.total_cpu_samplesstays0.0⇒ZeroDivisionError.Sibling per-file/per-line normalizations (
~556,~1259,~1337) were already guarded; this one was missed. Fix guards the loop (raw stack entries preserved when the total is 0).tests/test_stacks_zero_cpu_samples.pydrives the fulloutput_profilespath — crashes pre-fix, passes after.Bug 2 — CLI-renderer twin of the leak-velocity divide (
scalene_output.py)Scalene has three separate output renderers (see
Scalene-Debugging.md). #1077 fixed the unguardedleak_velocity / stats.elapsed_timeonly in the JSON renderer; the CLI renderer (scalene view --cli,scalene_output.py:699) carried the identical unguarded divide.Same reachability:
compute_leaksgates on allocation growth rate, not wall-clock time, so a leak can be reported on a sub-ms run whereelapsed_timeis still0.0. Fix mirrors the #1077 guard;tests/test_cli_leak_velocity_zero_elapsed.pyadded.The other CLI-path divides (
~339,~379,~416,~657) were audited and are already guarded.Notes
formal/HANDOFF.md: a fix in one renderer does not cover its siblings — audit all three. The formalization "bugs found" tally is now four.