perf(lsp): shared Java cross-registry + own-file overlay (#1669)

Cross-file LSP for Java rebuilt a type registry for EVERY file from a def
set the module filter reduces by a constant factor, not to a constant
size: the JVM filter branch includes every def sharing the file's
namespace, so per-file work tracks the corpus (measured defs_per_file
1,292 -> 5,031 in lockstep as defs_total went 179k -> 689k). That makes
the pass O(files x corpus_defs) — 87% of a Java index and the bulk of
the v0.9.0 -> v0.10.x slowdown.

Three changes, one architecture (the pattern Go/Python/C/C#/TS already
use):

1. cbm_java_build_cross_registry — the JVM def universe (Java + Kotlin,
   for mixed source roots) built ONCE per run, sealed read-only, shared
   across resolve workers. Wired into both pipeline.c and
   pipeline_incremental.c.

2. Two-phase registration inside that build: all TYPES first, finalize
   (hash buckets exist), then FUNCS. Func registration parses signatures,
   and type-name qualification via cbm_registry_lookup_type is a LINEAR
   scan until finalize — a single mixed pass measured 0.44 ms/def at
   689k defs, a ~300 s sequential build that erased the sharing win.
   Two-phase: 306 s -> 2.8 s. Stable partition order because overload
   ties resolve to the first registered QN match.

3. cbm_run_java_lsp_cross_with_registry resolves each file against the
   shared base through an overlay holding exactly THIS FILE's defs
   (register_local_func_or_type_from_file). Own-file scope is the load-
   bearing choice: patch_one_method refines signatures from the AST and
   must write a private copy (its types live in the per-file arena, the
   base is sealed), and any wider scope re-imports the quadratic — a
   module/namespace-scoped overlay measured ratio 4.00 on the growing-
   package corpus, own-file measures 2.00.

Elasticsearch corpus (46,477 files), same host, CBM_PROFILE=1:

                      v0.10.5      this change      v0.9.0
  wall                419.9 s      85.6 s (4.9x)    61.5 s
  cross-LSP CPU       6,195,780ms  52,513ms (118x)  110,344 ms
  us_per_file         207,448      1,770            3,722
  us_per_file_per_kdef 300         2                —
  nodes               693,100      693,100          —
  edges               5,646,235    5,649,949        —

Cross-LSP CPU now beats v0.9.0. Nodes are identical; edges +0.066%,
consistent with the shared base resolving cross-package targets the old
per-file namespace/import filter could not see, plus source-order
independence from the two-phase build.

Guarded by the complexity suite's shared-package gate (RED at ratio 4.00
on the pre-change tree, 2.00 after — see the suite commit).

Refs #1669.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-08-16 18:43:44 +02:00
parent cffb4e374a
commit e24f0c6328
6 changed files with 183 additions and 1 deletions
+17
View File
@@ -1009,6 +1009,15 @@ _Atomic uint64_t g_pxc_build_files = 0;
_Atomic uint64_t g_pxc_filter_files = 0;
_Atomic uint64_t g_pxc_filter_failed = 0;
/* Overlay registrations count as per-file registry work too: the complexity
* gate (test_complexity.c) sums ALL defs registered per file, whichever path
* built them. Without this the shared-registry languages would report zero and
* the linearity gate would pass vacuously. */
void cbm_pxc_count_perfile_defs(uint64_t defs) {
atomic_fetch_add_explicit(&g_pxc_defs_registered, defs, memory_order_relaxed);
atomic_fetch_add_explicit(&g_pxc_build_files, 1, memory_order_relaxed);
}
void cbm_pxc_filter_stats(uint64_t *defs_registered, uint64_t *build_files, uint64_t *filter_files,
uint64_t *filter_failed) {
if (defs_registered)
@@ -1070,6 +1079,14 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
&result->resolved_calls);
used_prebuilt = true;
break;
case CBM_LANG_JAVA:
/* Own-module defs go into a per-file overlay; imports and stdlib
* resolve through the shared base (#1669). */
cbm_run_java_lsp_cross_with_registry(
&result->arena, result, source, source_len, def_module, prebuilt, imp_keys,
imp_vals, imp_count, result->cached_tree, &result->resolved_calls);
used_prebuilt = true;
break;
case CBM_LANG_JAVASCRIPT:
case CBM_LANG_TYPESCRIPT:
case CBM_LANG_TSX: {
+7
View File
@@ -35,6 +35,7 @@
#include "lsp/c_lsp.h" /* cbm_c_build_cross_registry / cbm_run_c_lsp_cross_with_registry */
#include "lsp/cs_lsp.h" /* cbm_cs_build_cross_registry / cbm_run_cs_lsp_cross_with_registry */
#include "lsp/ts_lsp.h" /* cbm_ts_build_cross_registry / cbm_run_ts_lsp_cross_with_registry */
#include "lsp/java_lsp.h" /* cbm_java_build_cross_registry / cbm_run_java_lsp_cross_with_registry */
#include "lsp/rust_lsp.h" /* cbm_rust_build_cross_registry / cbm_run_rust_lsp_cross_with_registry */
#include "pipeline/pipeline_internal.h"
#include <stdbool.h>
@@ -125,6 +126,7 @@ typedef struct {
CBMTypeRegistry *ts; /* CBM_LANG_JAVASCRIPT, TYPESCRIPT, TSX */
CBMTypeRegistry *php; /* CBM_LANG_PHP */
CBMTypeRegistry *cs; /* CBM_LANG_CSHARP */
CBMTypeRegistry *java; /* CBM_LANG_JAVA (JVM def universe incl. Kotlin defs) */
/* CBM_LANG_RUST: intentionally absent — the shared rust registry is built
* LAZILY inside cbm_parallel_resolve (first NULL-filter rust file), not eagerly. */
} CBMCrossLspRegistries;
@@ -133,6 +135,9 @@ typedef struct {
* if none was built (or language has no cross-LSP entrypoint). */
/* Per-file registry-build cost (#1669): how many defs the per-file cross-LSP
* path actually registered, and how often the module filter failed. */
/* Count defs an overlay registered for one file (complexity-gate telemetry). */
void cbm_pxc_count_perfile_defs(uint64_t defs);
void cbm_pxc_filter_stats(uint64_t *defs_registered, uint64_t *build_files, uint64_t *filter_files,
uint64_t *filter_failed);
@@ -157,6 +162,8 @@ static inline CBMTypeRegistry *cbm_pxc_registry_for_lang(const CBMCrossLspRegist
return r->php;
case CBM_LANG_CSHARP:
return r->cs;
case CBM_LANG_JAVA:
return r->java;
default:
return NULL; /* incl. CBM_LANG_RUST — its shared registry is built lazily */
}
+3 -1
View File
@@ -1254,7 +1254,7 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
* Built ONCE here; shared READ-ONLY across all files of that language
* during resolve. Per-file work is then: parse + AST walk + O(1) lookups
* — no registry build, no Phase 1b mutations. Languages added so far:
* Go, Python. Others (C/C++, TS/JS, PHP, C#) fall back to per-file. */
* Go, Python, C/C++, C#, TS/JS, Java. Others (Kotlin, PHP) fall back to per-file. */
CBMArena cross_lsp_arena;
cbm_arena_init(&cross_lsp_arena);
CBMCrossLspRegistries cross_registries = {0};
@@ -1265,6 +1265,8 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
cross_registries.c = cbm_c_build_cross_registry(&cross_lsp_arena, all_defs, def_count);
cross_registries.cs = cbm_cs_build_cross_registry(&cross_lsp_arena, all_defs, def_count);
cross_registries.ts = cbm_ts_build_cross_registry(&cross_lsp_arena, all_defs, def_count);
cross_registries.java =
cbm_java_build_cross_registry(&cross_lsp_arena, all_defs, def_count);
/* Rust: NOT built here. The shared all_defs registry is built LAZILY on the
* first NULL-filter rust file (the amplifier files) inside cbm_parallel_resolve
* — repos whose rust files all filter to subsets never pay the build/RSS. */
+2
View File
@@ -1334,6 +1334,8 @@ static int run_extract_resolve(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed
cross_registries.c = cbm_c_build_cross_registry(xa, all_defs, all_def_count);
cross_registries.cs = cbm_cs_build_cross_registry(xa, all_defs, all_def_count);
cross_registries.ts = cbm_ts_build_cross_registry(xa, all_defs, all_def_count);
cross_registries.java =
cbm_java_build_cross_registry(xa, all_defs, all_def_count);
registries_arg = &cross_registries;
}
} else {