Toilal
## Summary `getfullargspec` is called on the hot matching path (via `loose.call` → `function_args` / `constructor_args` in `rebulk/loose.py`) for every validator/formatter/conflict-solver invocation. Its result depends only on the callable, and those callables are stable singletons for the lifetime of a `Rebulk` instance, so the introspection is fully redundant across calls and can be cached. ## Impact Measured while profiling guessit (which uses rebulk as its engine), on a set of realistic release names, best-of-3 wall-clock, ~16 `getfullargspec` calls per parse: | | µs/parse | |---|---:| | baseline | 13202 | | `getfullargspec` wrapped in `lru_cache` | 11738 | **≈ 11% faster** on repeated parses, for a one-line change. Instrumented wall-clock attribution independently put the cost at ~7.7% of a full parse (the A/B is higher because the instrumentation's own overhead diluted the ratio, and an `lru_cache` hit is near-free). ## Proposed fix Cache the signature introspection, e.g. in `rebulk/loose.py`: ```python from functools import lru_cache from inspect import getfullargspec as _getfullargspec getfullargspec = lru_cache(maxsize=None)(_getfullargspec) ``` The set of distinct callables is small and bounded (rule validators/formatters), so an unbounded cache is safe. Callables are hashable, so `lru_cache` works directly. `getfullargspec` is a pure function of its callable argument, so caching by identity cannot change behaviour.