Skip to content

Stop losing and double-counting lap time - #7

Merged
devRMA merged 3 commits into
chore/security-policyfrom
fix/timing-correctness
Aug 2, 2026
Merged

Stop losing and double-counting lap time#7
devRMA merged 3 commits into
chore/security-policyfrom
fix/timing-correctness

Conversation

@devRMA

@devRMAdevRMA commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Three ways the stopwatch reported wrong numbers without ever raising.

lap() was a @contextmanager with a bare yield between start() and
stop(), so an exception inside the block skipped stop() entirely.
The lap stayed open forever, and because Lap.elapsed falls back to
perf_counter() - self._start while running, elapsed, statistics
and report() then returned a different value on every read. Measured
before the fix: 0.0502 then 0.1004 from two consecutive reads of the
same stopwatch. Now wrapped in try/finally.

Nested lap() calls silently discarded the outer lap's time. start()
returns early when the stopwatch is already running, so the inner call
opened no lap, and the inner stop() closed the outer one -- leaving
the outer stop() a no-op and everything after the inner block
unmeasured. A 0.25s outer block containing a 0.10s inner block reported
0.15s in a single lap. Each lap() now owns a distinct lap.

The reuse of the running lap is deliberate and kept: a Stopwatch starts
measuring on construction, so the first lap() adopts that lap rather
than opening a second one, which is what stops
with Stopwatch() as sw: with sw.lap(): from recording a phantom lap.
_take_running_lap() is what hands ownership to the context manager and
lets a nested call open its own lap.

Known limitation, documented in the lap() docstring: time between
construction and the first lap() is still billed to that first lap.
Call reset() first to leave it out. Making this go away means giving
Stopwatch an autostart=False option, which is an API decision left
open deliberately.

running now asks the laps instead of _current_lap, which is None
while a lap() block owns its lap.

Lap.stop() is idempotent. It used to append perf_counter() - self._start unconditionally after having reset _start to 0.0, so a
second call recorded the machine uptime as a lap fraction. Not reachable
through Stopwatch today, but Lap is importable and the failure mode is
silent garbage rather than an error.

Each fix has a regression test. Verified they catch the bugs by
reverting the three fixes and re-running: exactly those three tests
fail, the other 35 pass.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com


Stack created with GitHub Stacks CLIGive Feedback 💬

@vercel

vercelBot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
stopwatch2-omipErrorErrorAug 2, 2026 8:59pm

Three ways the stopwatch reported wrong numbers without ever raising.
`lap()` was a @contextmanager with a bare `yield` between `start()` and
`stop()`, so an exception inside the block skipped `stop()` entirely.
The lap stayed open forever, and because `Lap.elapsed` falls back to
`perf_counter() - self._start` while running, `elapsed`, `statistics`
and `report()` then returned a different value on every read. Measured
before the fix: 0.0502 then 0.1004 from two consecutive reads of the
same stopwatch. Now wrapped in try/finally.
Nested `lap()` calls silently discarded the outer lap's time. `start()`
returns early when the stopwatch is already running, so the inner call
opened no lap, and the inner `stop()` closed the *outer* one -- leaving
the outer `stop()` a no-op and everything after the inner block
unmeasured. A 0.25s outer block containing a 0.10s inner block reported
0.15s in a single lap. Each `lap()` now owns a distinct lap.
The reuse of the running lap is deliberate and kept: a Stopwatch starts
measuring on construction, so the first `lap()` adopts that lap rather
than opening a second one, which is what stops
`with Stopwatch() as sw: with sw.lap():` from recording a phantom lap.
Clearing `_current_lap` is what hands ownership to the context manager
and lets a nested call open its own lap. What remains is noted as a
ponytail comment on `lap()`: time between construction and the first
`lap()` is still billed to that first lap.
`running` now asks the laps instead of `_current_lap`, which is None
while a `lap()` block owns its lap.
`Lap.stop()` is idempotent. It used to append `perf_counter() -
self._start` unconditionally after having reset `_start` to 0.0, so a
second call recorded the machine uptime as a lap fraction. Not reachable
through Stopwatch today, but `Lap` is importable and the failure mode is
silent garbage rather than an error.
Each fix has a regression test. Verified they catch the bugs by
reverting the three fixes and re-running: exactly those three tests
fail, the other 35 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drop the inline comments and the ponytail marker, and make the lap
handover legible on its own: `_take_running_lap() or _open_lap()` says
what the branch did. `_open_lap` also removes the duplicated
append/start that `start()` was repeating.
The remaining docstrings state behaviour only, no rationale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A stopwatch starts measuring on construction, and the first lap() takes
over that lap, so setup work between the constructor and the first lap()
is billed to it:
sw = Stopwatch()
load_config() # 5s
for item in items:
with sw.lap():
process(item) # 0.1s -> first lap reports 5.1s
`autostart=False` skips the initial start(), so only the blocks wrapped
in lap() are measured. The default stays True, so nothing changes for
existing callers, and using the stopwatch as a context manager still
starts it because __enter__ restarts regardless.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@devRMA
devRMA merged commit 3c8a995 into mainAug 2, 2026
8 of 9 checks passed
@devRMA
devRMA deleted the fix/timing-correctness branch August 2, 2026 21:22
Sign up for freeto 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

@devRMA