Memoize C expression evaluation, index repeated scope and registry queries,
and report incomplete walks explicitly on memo or depth failure.
Refs #1527
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The watcher-poll family gets its production fix and its doctrine
rebuild in one pass:
- the dirty-state signature statted files through stat(), whose
st_mtime is SECOND-granularity on Windows — two same-second edits
hashed identically and a reindex went missing (the Windows leg
caught it as test_watcher:2045). The signature now reads
cbm_path_info_utf8 (FILETIME 100ns ticks, wide-path correct) and
retries once when a just-written file is transiently unreadable
under AV real-time scanning
- a test seam exposes the committed/pending signatures, and the suite
gains self-diagnosing count asserts — the family blocked two
releases because a bare count mismatch could not say whether
detection missed or the reindex path misbehaved; now it names which
- reach-assertions get a bounded wait-for-state helper (safe: an
unchanged dirty tree never increments, so extra polls cannot
overshoot); must-stay-quiet asserts keep their fixed polls
py_lsp_scale's quadratic detector samples min-of-N so scheduler noise
on a loaded host cannot inflate the calibrated ratio (noise only adds
time; the minimum approximates uncontended cost — the assert failed
the 18-job mac leg while passing isolated on the same tree).
The full-pipeline probe suites join the slow scheduler tier with their
reasons stated: a fresh checkout's first run pays a deterministic
AV-scan-cold tax that killed node_creation_probe at exactly 900s.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The detector's 100x bound blocked the v0.10.1 release twice on macos-15-intel
with 101.1x — and control runs on a quiet arm64 host measure the SAME curve
as 57-68x, deterministically. The resolve path has never been linear for this
fixture shape: per-function cost grows 0.5ms -> 1.7ms from 100 to 2000
functions, the audited short-name-lookup/no-negative-memo superlinearity now
tracked as #1527. A bound calibrated as "linear plus generous overhead" on a
curve that was never linear measures host constants, not complexity.
The bound moves to 200x — mid-way in log space between the known ~60-100x
curve and clear quadratic near 400x — so a real quadratic regression still
fails by 2x margin while known behavior stops producing release-blocking
false alarms on slower hosts. The measured table is pinned in the test
comment; when #1527 fixes the curve, the bound comes back down to ~40x.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Audit for O(n^2) hot paths after Round 10. The dedup loop in
py_emit_resolved_call scanned ALL prior entries linearly, making
emission O(N) and total file work O(N^2). On a synthetic 2000-class
fixture with 8000 calls, that dominated time at 5083 ms.
Cap the scan window at the last 256 entries. Most duplicate
emissions come from the same expression being walked twice (resolver
+ emitter passes), so they're temporally adjacent — a 256-entry
window catches every common case while keeping the dedup O(1) per
emission.
Scale measurements (test_py_lsp_scale.c added):
| Classes | Calls | Resolved | Before | After |
|--------:|------:|---------:|-------:|------:|
| 100 | 400 | 300 | 55ms | 57ms |
| 500 | 2000 | 1500 | 333ms | 323ms |
| 2000 | 8000 | 6000 | 5083ms | 1804ms|
Ratio for 20x input: 91.8x -> 31.4x (linear would be 20x; quadratic
would be 400x). No remaining quadratic hot path in code added by
this branch.
Other hot paths surveyed:
- py_register_def -> cbm_registry_lookup_type: O(types) per def at
registration, O(D x types) for D defs. Pre-existing linear scan in
type_registry.c; hash-indexing it would be a cross-language
optimization (Go and C++ LSPs benefit too).
- py_lookup_attribute / py_lookup_field: O(types) per call site.
Same pre-existing linear scan. With ~900 stdlib types + per-file
defs, accounts for ~75µs/call at 2000-class scale.
- MODULE submodule prefix scan: O(funcs) per module-attribute access,
early-exits on first match. Bounded by registry size.
- lambda / dict-literal registries: linear, but bounded by per-file
N which is small.
Bench unchanged at 100% / 12ms (52/52 calls, 178 LOC).