Commit Graph
4 Commits
Author SHA1 Message Date
Martin Vogel c3c97a99d7 fix(lsp): bound repeated resolver work without truncating large files
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>
2026-10-12 21:29:00 +02:00
Martin Vogel 32e910aab5 fix(watcher): FILETIME-precision dirty signatures; flaky-family rebuilds
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>
2026-08-28 09:14:40 +02:00
Martin Vogel 0cdcf9a442 test(scale): calibrate the py-LSP quadratic detector to detect quadratic (#1527)
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>
2026-08-11 05:30:17 +02:00
Martin Vogel ecfbbf48d6 perf(py_lsp): cap dedup window at 256 entries — 91.8x -> 31.4x scaling
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).
2026-05-09 15:44:09 +02:00