Skip to content

Add Coverage.clear_data() for lightweight data reset - #2235

Open
pctablet505 wants to merge 3 commits into
coveragepy:mainfrom
pctablet505:clear-data-2139
Open

pctablet505 wants to merge 3 commits into
coveragepy:mainfrom
pctablet505:clear-data-2139

Conversation

@pctablet505

Copy link
Copy Markdown

Summary

erase() clears collected coverage data, but it also sets things up so
the next start() reruns _init_for_start() — rebuilding Core,
Collector, and InOrOut, and re-registering the atexit/sigterm
handlers. For code that starts/stops measurement on the same
Coverage instance many times in a row and only needs a clean slate
of data between cycles (tracing configuration doesn't change), that
reinitialization is pure overhead.

This adds Coverage.clear_data(): it closes and replaces the current
CoverageData, but leaves the existing collector/tracer setup alone
so the next start() doesn't redo the expensive part.

Closes #2139, which includes a benchmark showing the reinitialization
overhead and the speedup from avoiding it.

Changes

  • coverage/control.py: new Coverage.clear_data() method, plus a
    small _close_data() helper it shares the "close and forget the
    current CoverageData" logic with.
  • tests/test_api.py: tests that clear_data() gives a fresh data set,
    that repeated cycles behave like the existing erase()-based test,
    and that the collector/core/inorout objects from the first start()
    are still the ones in use after a clear_data() + start() cycle.
  • CHANGES.rst: entry under "Unreleased".

Test plan

  • pytest tests/test_api.py tests/test_data.py tests/test_collector.py — all passing
  • ruff format --check, pylint, and mypy --strict all clean on the touched files
  • Ran the benchmark script from the issue with clear_data() substituted for the manual workaround; confirms the same order-of-magnitude speedup and no extra atexit-callback accumulation

@pctablet505
pctablet505 marked this pull request as ready for review July 17, 2026 12:47
@pctablet505
pctablet505 force-pushed the clear-data-2139 branch 2 times, most recently from 082b00e to 4f796b8 Compare September 11, 2026 06:41
erase() clears collected data but also forces the next start() to
rerun _init_for_start(), rebuilding Core, Collector, and InOrOut and
re-registering atexit/sigterm handlers. For code that repeatedly
starts/stops measurement on the same Coverage instance and only needs
a clean slate of data between cycles, this reinitialization is pure
overhead.

clear_data() closes and replaces the current CoverageData, keeping
the existing collector/tracer setup intact for the next start().

Closes coveragepy#2139.
clear_data() closed the current CoverageData and made a fresh one, but
the closed object stayed referenced in _data_to_close for the life of
the process. Over many start/stop/clear cycles (e.g. long fuzzer runs)
these closed, empty objects piled up, partly defeating the point of the
lighter-weight reset. _close_data() now drops the object from
_data_to_close when it forgets it.

Also fill in the versionadded placeholder for Coverage.clear_data().
@hafizahmedzia

Copy link
Copy Markdown

One disk-backed edge case in pctablet505's implementation, beyond cyberthirst's data_file=None example in #2139:

At c01dea4114cfacf58481120f5b307298db685bdc, clear_data() drops a configured filename suffix between collection cycles:

from pathlib import Path
from tempfile import TemporaryDirectory
from coverage import Coverage

with TemporaryDirectory() as temp:
    root = Path(temp)
    cov = Coverage(config_file=False, data_file=str(root / ".coverage"),
                   data_suffix="workerA", include=[str(root / "*.py")])
    for n in (1, 2):
        if n == 2:
            cov.clear_data()
        source = root / f"cycle_{n}.py"
        source.write_text("answer = 42\n", encoding="utf-8")
        with cov.collect():
            exec(compile(source.read_text(encoding="utf-8"), str(source), "exec"), {})
        cov.save()
        print(Path(cov.get_data().data_filename()).name)

Output:

.coverage.workerA
.coverage

Reading both files confirms the suffixed file contains cycle 1 and the base file contains cycle 2. data_suffix=True and run:parallel=True also lose their generated suffix. Calling clear_data() before the first collection starts with the base filename immediately. In a separate sequential check, two instances configured with distinct custom suffixes both targeted that same base filename after clearing.

The recorded comparison reproduced this three times per configuration on CPython 3.12.14/Linux using CTracer, PyTracer, and SysMonitor. Default unsuffixed and no-disk controls passed. Replacing clear_data() with erase() preserved suffixes on both this head and pre-PR a4529f5f144cb61249638868c1eb4222661899af; that baseline has no clear_data() API. The focused API/data/collector suites passed on both revisions under all three cores.

The new method calls _init_data(suffix=None) while keeping the start setup. Is disk-backed use intended here? If so, could the configured suffix be retained, or should the API explicitly be limited to no-disk use?

The full suite and true concurrent workers were not tested.

This branch has not been deployed

No deployments
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.

Feature request: lightweight clear_data() for repeated collection without rerunning _init_for_start()

2 participants