Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,24 @@ first, so the result is the same whatever `N`.
coretrace-python-analyzer --check src/ --cache .coretrace --jobs 4
```

## Performance

Measured on one core of a laptop, single process, cold start, with the bundled plugins
(September 2026):

| Project | Python lines | Functions | Time | Peak memory |
|---|---:|---:|---:|---:|
| healthchecks (Django application) | 45 000 | 2 566 | 7 s | 107 MB |
| rich (library) | 52 000 | 1 198 | 6 s | 106 MB |
| wagtail (Django CMS) | 273 000 | 12 146 | 85 s | 461 MB |

Time grows a little faster than linearly with the size of the project, because the
interprocedural summaries are iterated to a fixpoint over the module graph. A warm
`--cache` brings an unchanged healthchecks to about one second, `--jobs 4` saves a
quarter of the cold time at the cost of one process's memory per job. The
`healthchecks` repository is part of the regression corpus, so the analysis time of a
real 45 000-line project is exercised on every change.

## Plugins

The package ships 26 plugins, loaded by default. Security models for the standard library
Expand Down
23 changes: 21 additions & 2 deletions src/coretrace_python/ir/lowering.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from collections import OrderedDict
from dataclasses import dataclass, field
from typing import ClassVar, NoReturn

Expand Down Expand Up @@ -507,10 +508,28 @@ def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> FunctionIR:
return lowerer.function(function)


_FUNCTIONS_CACHE: OrderedDict[int, tuple[nodes.Module, tuple[nodes.Function, ...]]] = OrderedDict()
_FUNCTIONS_CACHE_SIZE = 32


def analyzable_functions(module: nodes.Module) -> tuple[nodes.Function, ...]:
"""Top-level functions, the methods of top-level classes, and the functions and
lambdas nested inside them, in source order."""

lambdas nested inside them, in source order. Computed once per module: the result is
memoised for the last few modules seen, keyed by identity, so every consumer shares
one tuple and the synthesized lambda functions it holds."""

cached = _FUNCTIONS_CACHE.get(id(module))
if cached is not None and cached[0] is module:
_FUNCTIONS_CACHE.move_to_end(id(module))
return cached[1]
functions = _analyzable_functions(module)
_FUNCTIONS_CACHE[id(module)] = (module, functions)
while len(_FUNCTIONS_CACHE) > _FUNCTIONS_CACHE_SIZE:
_FUNCTIONS_CACHE.popitem(last=False)
return functions


def _analyzable_functions(module: nodes.Module) -> tuple[nodes.Function, ...]:
functions: list[nodes.Function] = []
for statement in module.body:
if isinstance(statement, nodes.Function):
Expand Down
18 changes: 10 additions & 8 deletions src/coretrace_python/taint/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,23 +541,25 @@ def local_instances(function: nodes.Function, scopes: ScopeTable, symbols: Symbo
def _enclosing(module: nodes.Module, function: nodes.Function) -> nodes.Function | None:
"""The function whose body defines ``function``, if it is nested."""

def search(body: tuple[nodes.Statement, ...], parent: nodes.Function | None) -> nodes.Function | None:
def search(
body: tuple[nodes.Statement, ...], parent: nodes.Function | None
) -> tuple[bool, nodes.Function | None]:
for statement in body:
if isinstance(statement, nodes.Function):
if statement.span == function.span:
return parent
return True, parent
found = search(statement.body, statement)
if found is not None:
if found[0]:
return found
elif isinstance(statement, nodes.Class):
found = search(statement.body, None)
if found is not None:
if found[0]:
return found
return None
return False, None

found = search(module.body, None)
if found is not None:
return found
defined, parent = search(module.body, None)
if defined:
return parent
# Lambdas are synthesized functions: their enclosing function is the innermost
# analysable function whose span contains theirs.
innermost: nodes.Function | None = None
Expand Down
Loading
Loading