mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-27 06:53:11 +08:00
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).
97 lines
3.6 KiB
C
97 lines
3.6 KiB
C
/*
|
|
* test_py_lsp_scale.c — measure scaling behavior at 100 / 500 / 2000
|
|
* classes-and-calls. Asserts that doubling the input doesn't more than
|
|
* 4x the runtime (catches accidental O(n^2) in the resolver).
|
|
*/
|
|
#include "test_framework.h"
|
|
#include "cbm.h"
|
|
#include "lsp/py_lsp.h"
|
|
#include <time.h>
|
|
|
|
static double elapsed_ms(struct timespec t0, struct timespec t1) {
|
|
double s = (double)(t1.tv_sec - t0.tv_sec);
|
|
double ns = (double)(t1.tv_nsec - t0.tv_nsec);
|
|
return s * 1000.0 + ns / 1000000.0;
|
|
}
|
|
|
|
/* Build N synthetic class/call pairs into an arena-backed buffer. */
|
|
static char *build_fixture(int n_classes, int *out_len) {
|
|
/* Per class: ~140 chars (5-line def). Per call: ~50 chars. Overhead
|
|
* for the class number digits scales with log10(n) but the constant
|
|
* 256 covers up to 9-digit indices comfortably. */
|
|
int approx = n_classes * 256 + 1024;
|
|
char *buf = (char *)malloc((size_t)approx);
|
|
if (!buf) return NULL;
|
|
int pos = 0;
|
|
pos += snprintf(buf + pos, (size_t)(approx - pos),
|
|
"from typing import Self\n");
|
|
for (int i = 0; i < n_classes; i++) {
|
|
int n = snprintf(buf + pos, (size_t)(approx - pos),
|
|
"class Cls%d:\n"
|
|
" def method(self) -> int:\n"
|
|
" return %d\n"
|
|
" def chain(self) -> Self:\n"
|
|
" return self\n", i, i);
|
|
if (n < 0 || pos + n >= approx) break;
|
|
pos += n;
|
|
}
|
|
int n = snprintf(buf + pos, (size_t)(approx - pos), "def use():\n");
|
|
pos += n;
|
|
for (int i = 0; i < n_classes; i++) {
|
|
int m = snprintf(buf + pos, (size_t)(approx - pos),
|
|
" Cls%d().chain().chain().method()\n", i);
|
|
if (m < 0 || pos + m >= approx) break;
|
|
pos += m;
|
|
}
|
|
*out_len = pos;
|
|
return buf;
|
|
}
|
|
|
|
static double measure(int n_classes, int *out_calls, int *out_resolved) {
|
|
int slen = 0;
|
|
char *src = build_fixture(n_classes, &slen);
|
|
if (!src) return -1.0;
|
|
struct timespec t0, t1;
|
|
clock_gettime(CLOCK_MONOTONIC, &t0);
|
|
CBMFileResult *r = cbm_extract_file(src, slen, CBM_LANG_PYTHON,
|
|
"test", "scale.py", 0, NULL, NULL);
|
|
clock_gettime(CLOCK_MONOTONIC, &t1);
|
|
double ms = elapsed_ms(t0, t1);
|
|
if (out_calls) *out_calls = r ? r->calls.count : 0;
|
|
if (out_resolved) *out_resolved = r ? r->resolved_calls.count : 0;
|
|
if (r) cbm_free_result(r);
|
|
free(src);
|
|
return ms;
|
|
}
|
|
|
|
TEST(pylsp_scale_linear_growth) {
|
|
int c100 = 0, r100 = 0;
|
|
int c500 = 0, r500 = 0;
|
|
int c2000 = 0, r2000 = 0;
|
|
double t100 = measure(100, &c100, &r100);
|
|
double t500 = measure(500, &c500, &r500);
|
|
double t2000 = measure(2000, &c2000, &r2000);
|
|
printf(" scale: 100=%.1fms (calls=%d resolved=%d) 500=%.1fms (calls=%d resolved=%d) 2000=%.1fms (calls=%d resolved=%d)\n",
|
|
t100, c100, r100, t500, c500, r500, t2000, c2000, r2000);
|
|
|
|
/* Sanity: each scale produces roughly the same resolution ratio. */
|
|
double r_pct_100 = c100 ? (double)r100 / c100 : 0.0;
|
|
double r_pct_2000 = c2000 ? (double)r2000 / c2000 : 0.0;
|
|
ASSERT(r_pct_100 > 0.5);
|
|
ASSERT(r_pct_2000 > 0.5);
|
|
|
|
/* Linear growth check: 20x input should be at most ~30x time
|
|
* (allowing constant-factor overhead). 20x with quadratic would be
|
|
* 400x — easy to detect. */
|
|
if (t100 > 0.5) { // skip when t100 too small to compare reliably
|
|
double ratio = t2000 / t100;
|
|
printf(" scale ratio 2000/100: %.1fx (linear ~20x, quadratic ~400x)\n", ratio);
|
|
ASSERT(ratio < 100.0); // generous bound; flags clear quadratic
|
|
}
|
|
PASS();
|
|
}
|
|
|
|
SUITE(py_lsp_scale) {
|
|
RUN_TEST(pylsp_scale_linear_growth);
|
|
}
|