feat(diagnostics): report per-file cross-LSP registry cost

The per-file cross-LSP path rebuilds a type registry for every file. Whether
that is cheap or quadratic depends entirely on how many defs the module
filter leaves, and nothing reported it — so a filter that reduces by a
constant FACTOR rather than to a constant SIZE looked identical to one that
worked.

parallel.resolve.perfile_registry reports defs_per_file next to defs_total,
plus how often the filter failed. Measured on one Java tree:

    1/4 corpus:  defs_total=179033  defs_per_file=1292
    full corpus: defs_total=689216  defs_per_file=5031

defs_per_file grew 3.89x while the corpus grew 3.85x — lockstep, so per-file
work is proportional to the whole corpus and cross-file LSP is
O(files x corpus_defs). filter_failed=0 throughout, which is why this was
invisible: the filter always 'succeeds', it just does not bound the set.

Diagnostics only; no behaviour change.

Refs #1669.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-08-16 14:52:22 +02:00
parent 170590bc6c
commit cffb4e374a
3 changed files with 55 additions and 0 deletions
+30
View File
@@ -36,6 +36,7 @@
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdatomic.h>
#include <string.h>
/* ── Constants ─────────────────────────────────────────────────── */
@@ -1001,6 +1002,25 @@ void cbm_pxc_run_one_ts(CBMFileResult *r, const char *source, int source_len, co
* `rust_shared_get` supplies the lazily-built shared Rust all-defs registry
* (the parallel resolver owns its once-guard); NULL means "no shared rust
* registry available" and rust NULL-filter files take the per-file build. */
/* Per-file registry-build cost counters (#1669). Surfaced by pass_parallel at
* end of resolve. */
_Atomic uint64_t g_pxc_defs_registered = 0;
_Atomic uint64_t g_pxc_build_files = 0;
_Atomic uint64_t g_pxc_filter_files = 0;
_Atomic uint64_t g_pxc_filter_failed = 0;
void cbm_pxc_filter_stats(uint64_t *defs_registered, uint64_t *build_files, uint64_t *filter_files,
uint64_t *filter_failed) {
if (defs_registered)
*defs_registered = atomic_load_explicit(&g_pxc_defs_registered, memory_order_relaxed);
if (build_files)
*build_files = atomic_load_explicit(&g_pxc_build_files, memory_order_relaxed);
if (filter_files)
*filter_files = atomic_load_explicit(&g_pxc_filter_files, memory_order_relaxed);
if (filter_failed)
*filter_failed = atomic_load_explicit(&g_pxc_filter_failed, memory_order_relaxed);
}
void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *source,
int source_len, const char *rel, const char *def_module,
const CBMCrossLspRegistries *cross_registries,
@@ -1112,7 +1132,17 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
file_defs = filtered;
file_def_count = filtered_count;
}
atomic_fetch_add_explicit(&g_pxc_filter_files, 1, memory_order_relaxed);
if (!filter_succeeded) {
atomic_fetch_add_explicit(&g_pxc_filter_failed, 1, memory_order_relaxed);
}
}
/* Per-file registry build cost is driven by THIS number. If it tracks the
* corpus instead of the file's own module + imports, cross-file LSP is
* O(files x corpus_defs) — see #1669. */
atomic_fetch_add_explicit(&g_pxc_defs_registered, (uint64_t)file_def_count,
memory_order_relaxed);
atomic_fetch_add_explicit(&g_pxc_build_files, 1, memory_order_relaxed);
if (lang == CBM_LANG_RUST) {
CBMTypeRegistry *shared = rust_shared_get ? rust_shared_get(rust_shared_ctx) : NULL;
if (shared) {
+5
View File
@@ -131,6 +131,11 @@ typedef struct {
/* Return the appropriate pre-built registry for a language, or NULL
* 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. */
void cbm_pxc_filter_stats(uint64_t *defs_registered, uint64_t *build_files, uint64_t *filter_files,
uint64_t *filter_failed);
static inline CBMTypeRegistry *cbm_pxc_registry_for_lang(const CBMCrossLspRegistries *r,
CBMLanguage lang) {
if (!r)
+20
View File
@@ -3315,6 +3315,26 @@ int cbm_parallel_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
cbm_log_info("parallel.resolve.cross_lsp_cost", "cross_lsp_ms", cu_buf, "files", nf_buf,
"us_per_file", cf_buf, "defs_total", itoa_log(def_count),
"us_per_file_per_kdef", pk_buf);
/* What the per-file registry build actually cost. defs_per_file is the
* lever: if it tracks defs_total rather than the file's own module plus
* imports, the module filter is not containing the work and cross-file
* LSP is O(files x corpus_defs). */
uint64_t reg_defs = 0;
uint64_t reg_files = 0;
uint64_t flt_files = 0;
uint64_t flt_failed = 0;
cbm_pxc_filter_stats(&reg_defs, &reg_files, &flt_files, &flt_failed);
if (reg_files > 0) {
char rd_buf[CBM_SZ_32];
char ff_buf[CBM_SZ_32];
char fp_buf[CBM_SZ_32];
snprintf(rd_buf, sizeof(rd_buf), "%llu", (unsigned long long)(reg_defs / reg_files));
snprintf(ff_buf, sizeof(ff_buf), "%llu", (unsigned long long)flt_failed);
snprintf(fp_buf, sizeof(fp_buf), "%llu", (unsigned long long)flt_files);
cbm_log_info("parallel.resolve.perfile_registry", "defs_per_file", rd_buf, "defs_total",
itoa_log(def_count), "filtered_files", fp_buf, "filter_failed", ff_buf);
}
}
cbm_log_info("parallel.resolve.done", "calls", itoa_log(total_calls), "usages",