Skip to content

perf(py-lsp): O(1) type membership in the shared Python cross-registry build - #2420

Open
Fieldnote-Echo wants to merge 7 commits into
DeusData:mainfrom
Fieldnote-Echo:perf/py-cross-registry-index
Open

Fieldnote-Echo wants to merge 7 commits into
DeusData:mainfrom
Fieldnote-Echo:perf/py-cross-registry-index

Conversation

@Fieldnote-Echo

@Fieldnote-Echo Fieldnote-Echo commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2413

What does this PR do?

The shared Python cross-registry build registers defs one at a time, so each Method's receiver probe (internal/cbm/lsp/py_lsp.c:5049) is a linear scan over the types registered so far (type_registry.c:830-834). I back only that existence probe with a cbm_ht set of type QNs, updated after every cbm_registry_add_type, as the Go (959f4de) and Rust (10b3b79) fixes did. If the set cannot be allocated or an insert fails, probes fall back to the registry lookup. Keeping the per-def order makes the registry identical by construction (a types-first build would be simpler but can reorder auto-registered receiver types). Sorted node and edge dumps match main byte for byte, and peak RSS is unchanged.

Linux x86_64, 32 logical CPUs, GCC 16 main this PR
Python builder, Python 3.14 stdlib + site-packages, 3 runs 67-75 s 0.20-0.25 s
Same builder, generated 2k / 8k / 32k classes 0.11 / 1.5 / 22 s 8 / 28 / 135 ms
Whole index wall time, Python 3.14 tree 175-182 s 113 s

pylsp_scale_cross_registry_build_linear fails on main: going from 1k to 16k classes costs over 100× more time there, and under 8× here. It is in the py_lsp_scale perf suite, which PR CI skips via CBM_SKIP_PERF, so it needs a local run. pylsp_cross_registry_interleaved_order pins the registry order.

Checklist

  • Every commit is signed off (git commit -s) — required, CI rejects
    unsigned commits (DCO, see CONTRIBUTING.md)
  • Tests pass locally (make -f Makefile.cbm test)
    (run as make -f Makefile.cbm test-par)
  • Lint passes (make -f Makefile.cbm lint-ci)
    (for this PR's changes: CI's lint job passes on this head, and local cppcheck 2.22 / clang-format 23 add no findings over main)
  • New behavior is covered by a test (reproduce-first for bug fixes)

Prepared with AI assistance (exploration, reproduction, verification, implementation); the spec and design decisions are mine, and I reviewed and ran everything above.

…y build

The shared tier-2 registry registered Python defs one at a time without a
mid-build finalize, so every Method's receiver probe scanned all registered
types linearly: O(methods x types), single-threaded in lsp_cross_prepare.
Keep the per-def registration order and back only the existence probe with a
cbm_ht set of type QNs, as the Go and Rust builders do; the registry is
identical by construction.

On the Python 3.14 stdlib + site-packages the builder drops from 67-75 s to
0.20-0.25 s, with a byte-identical graph.

Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
@DeusData DeusData added the stability/performance Server crashes, OOM, hangs, high CPU/memory label Oct 19, 2026
@DeusData

Copy link
Copy Markdown
Owner

Thanks for the Python registry measurements and the registry-order control. I have read the fallback and byte-equivalence claims; they remain to be checked in review, including the perf suite that PR CI skips. No routine rebase is requested here.

Brings DeusData#2420 (O(1) type membership in the shared Python cross-registry
build, DeusData#2413) up to date with main a222715.

Conflicts in internal/cbm/lsp/py_lsp.c, resolved as follows:

- Includes: main already includes src/foundation/hash_table.h (module
  re-exports) and added lsp_node_iter.h. Main's lines are kept; the
  branch's duplicate include is dropped.

- py_register_lsp_defs: main changed it to return bool (false when the
  package scope cannot be normalized). The merged signature keeps main's
  bool return and adds this branch's PyKnownTypes *known parameter.

- cbm_run_py_lsp_cross_status: main's failure handling is kept and
  passes known = NULL, as the branch did for the per-file path.

- cbm_py_build_cross_registry: main added an early `return NULL` when a
  def fails to register. The merged loop passes &known and frees the
  type-QN set before that early return, so the set does not leak on the
  failure path.

- The test seam cbm_py_lsp_test_register_defs (added on main) passes
  known = NULL.

tests/test_py_lsp.c and tests/test_py_lsp_scale.c merged cleanly.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Replace the wall-clock ratio test pylsp_scale_cross_registry_build_linear
(py_lsp_scale suite) with pylsp_cross_registry_build_work_is_linear in
the py_lsp suite. The new test builds the same generated input (n
classes, each followed by two methods, in extraction order) and reads
the seam-build CBM_LSP_WORK counter instead of a clock, so its verdict
does not depend on host load, and it runs in PR CI, which skips the
perf suites.

4x the classes must cost at most ~4x the counted work. With the
type-QN set the build examines no registry candidates (0 at 1000 and
4000 classes); with the scanning receiver probe the work grows from
2,809,000 to 23,236,000 and the test fails.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you, @Fieldnote-Echo, and sorry this waited three weeks for us. The change is exactly the right size: it backs only the receiver-existence probe with a set, keeps the per-def registration order so the registry is identical by construction (and you explained why a types-first build would not be), falls back to the registry probe if the set cannot be allocated, and pylsp_cross_registry_interleaved_order pins the order so nobody can quietly change it later.

What we changed on your branch, so you know what landed:

  1. Merged current main in (one merge commit on top of your 3b79e3d, signed off; your commits are untouched). main had changed py_register_lsp_defs to return bool, added an early return NULL to cbm_py_build_cross_registry when a def fails to register, added a test seam that calls py_register_lsp_defs, and already includes hash_table.h. The merge keeps all of that and adds your known parameter. The type-QN set is now freed before the new early return so it does not leak on that path, the test seam passes NULL, and the duplicate include is dropped.
  2. Replaced the timing test with a counted-work test (one commit). Test verdicts here must not depend on the clock, since a busy host can fail a wall-time ratio, and the py_lsp_scale suite is skipped in PR CI, so that test would never have gated a PR. pylsp_cross_registry_build_work_is_linear in the py_lsp suite builds the same input your test generated (n classes, each followed by two methods) and reads the CBM_LSP_WORK counter instead: 4x the classes may cost at most about 4x the work. With your set the build examines no registry candidates at all; with the old scanning probe the counted work goes from 2,809,000 to 23,236,000 and the test fails.

Before pushing we ran the merged tree on real input (macOS arm64):

  • Python 3.14 standard library (1,852 files): the graph is identical with and without your change, 94,458 nodes and 574,480 edges.
  • Standard library + Django + SymPy in one tree (6,388 files, 28,426 classes, 102,312 methods): the graph is identical node for node and edge for edge (212,833 nodes, 1,326,298 edges), and a cold index went from 46.2 and 50.7 s to 33.8 and 35.7 s over two runs each.
  • Tests: the py_lsp, py_lsp_scale, c_lsp, pipeline, pipeline_frozen and parallel suites pass on the merged tree.

Nothing is needed from you. CI runs again on the refreshed branch, and once it is green the merge decision is the last step on our side. Thanks again for isolating this builder from the per-file path and for measuring it so carefully.

@DeusData

Copy link
Copy Markdown
Owner

Thank you again for the Python shared-registry improvement. The October updates record the deterministic counted-work regression and branch integration as completed on our side. No further contributor work is requested; sorry for the delay in the remaining follow-up, and I’ll follow up soon.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you again, @Fieldnote-Echo, and for your patience while this waited on us. The CI run after our October 20 update stopped early on every test leg at a repository-wide check of the published language counts, which was broken on main at the time and has since been repaired, so none of the suites (including py_lsp) got to run. We merged current main into your branch once more (one signed-off merge commit, 2be8ee3; your commits are untouched) so CI runs fresh. Because this change speeds up the shared Python registry build, we will also compare the graph itself on Django and the Python standard library before merging, and post the numbers here. Nothing is needed from you. If you push to this branch again, please pull first so the merge commit is kept.

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

stability/performance Server crashes, OOM, hangs, high CPU/memory

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(py-lsp): shared Python cross-registry build is O(methods × types)

2 participants