mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
perf(extract): O(1) walk-state maintenance; gate parent probes by language
The deep-nesting torture tests (stack_overflow_a/b) went from 0-1s per test on main to 39-119s on this branch -- the 900s suite budget killed them on every venue except the M4 (three GitHub CI platforms and the local Linux leg, each dying mid-suite at a DIFFERENT test, which is what pointed at shared machinery rather than any one language). Sampling the child process found two quadratic layers, both branch-added: 1. recompute_state iterated the WHOLE scope stack on every code-bearing node to rebuild the walk-state flags -- O(depth) per node, and a deep descent pushes a frame per level, so deep trees paid O(n x depth). Each frame now saves the complete walk-state tuple it displaces and pop restores it verbatim: push and pop are O(1) and kind-agnostic, and the per-node recompute is gone entirely. The CALL frame's effect is applied by push_call_scope after the caller fills the invocation triple, preserving the old ordering exactly. 2. is_reference_node fetched ts_node_parent for EVERY identifier in EVERY language to serve a Puppet/Vimscript sigil-wrapper check -- the language gate sat inside the condition, after the fetch. ts_node_parent descends from the root (O(depth)), so all languages paid O(depth) per identifier. The gate now precedes the fetch; only Puppet/Vimscript files pay it. macOS, both suites together: 543s -> 54s. Per test: ts_cyclic 119s -> 6s, python_deep 105s -> 6s, go_deep 39s -> 8s, php_deep 45s -> 8s. The residual 6-8s vs main's 0-1s is the branch's larger legitimate per-node work; the remaining Python attribute-site parent walk is a bounded follow-up, recorded. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
43e861e36a
commit
004a9a499f
3 files changed
+98
-56
No files matched your search
@@ -123,13 +123,49 @@ static bool push_scope(WalkState *state, uint8_t kind, uint32_t depth, const cha
|
||||
if (!ensure_scope_capacity(state)) {
|
||||
return false;
|
||||
}
|
||||
state->scopes[state->scope_top].kind = kind;
|
||||
state->scopes[state->scope_top].depth = depth;
|
||||
state->scopes[state->scope_top].qn = qn;
|
||||
state->scopes[state->scope_top].lexical_scope_id = 0;
|
||||
state->scopes[state->scope_top].invocation_kind = CBM_INVOCATION_NONE;
|
||||
state->scopes[state->scope_top].callee_expr = (TSNode){0};
|
||||
state->scopes[state->scope_top].callee_leaf = (TSNode){0};
|
||||
CBMWalkScope *f = &state->scopes[state->scope_top];
|
||||
f->kind = kind;
|
||||
f->depth = depth;
|
||||
f->qn = qn;
|
||||
f->lexical_scope_id = 0;
|
||||
f->invocation_kind = CBM_INVOCATION_NONE;
|
||||
f->callee_expr = (TSNode){0};
|
||||
f->callee_leaf = (TSNode){0};
|
||||
/* Save the complete displaced tuple, then apply this frame's effect --
|
||||
* pop_expired_scopes restores verbatim. O(1) either way; see the
|
||||
* CBMWalkScope comment for the quadratic recompute this replaces. */
|
||||
f->prev_enclosing_func_qn = state->enclosing_func_qn;
|
||||
f->prev_enclosing_class_qn = state->enclosing_class_qn;
|
||||
f->prev_invocation_kind = state->invocation_kind;
|
||||
f->prev_callee_expr = state->callee_expr;
|
||||
f->prev_callee_leaf = state->callee_leaf;
|
||||
f->prev_inside_import = state->inside_import;
|
||||
f->prev_loop_depth = state->loop_depth;
|
||||
f->prev_branch_depth = state->branch_depth;
|
||||
switch (kind) {
|
||||
case SCOPE_FUNC:
|
||||
state->enclosing_func_qn = qn;
|
||||
break;
|
||||
case SCOPE_CLASS:
|
||||
case SCOPE_NAMESPACE:
|
||||
state->enclosing_class_qn = qn;
|
||||
break;
|
||||
case SCOPE_CALL:
|
||||
/* The invocation triple is applied by push_call_scope once the caller
|
||||
* has filled the frame fields. */
|
||||
break;
|
||||
case SCOPE_IMPORT:
|
||||
state->inside_import = true;
|
||||
break;
|
||||
case SCOPE_LOOP:
|
||||
state->loop_depth++;
|
||||
break;
|
||||
case SCOPE_BRANCH:
|
||||
state->branch_depth++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
state->scope_top++;
|
||||
return true;
|
||||
}
|
||||
@@ -203,52 +239,27 @@ static void push_call_scope(WalkState *state, uint32_t depth,
|
||||
state->scopes[state->scope_top - SKIP_ONE].invocation_kind = invocation->kind;
|
||||
state->scopes[state->scope_top - SKIP_ONE].callee_expr = invocation->callee_expr;
|
||||
state->scopes[state->scope_top - SKIP_ONE].callee_leaf = invocation->callee_leaf;
|
||||
/* Apply the CALL effect (push_scope saved the displaced tuple already;
|
||||
* the frame fields had to be filled first). */
|
||||
state->invocation_kind = invocation->kind;
|
||||
state->callee_expr = invocation->callee_expr;
|
||||
state->callee_leaf = invocation->callee_leaf;
|
||||
}
|
||||
|
||||
// Pop scopes that we've ascended out of (depth >= current cursor depth).
|
||||
// Pop scopes that we've ascended out of (depth >= current cursor depth),
|
||||
// restoring the walk-state tuple each frame displaced. LIFO order keeps the
|
||||
// restores exact; O(1) per frame.
|
||||
static void pop_expired_scopes(WalkState *state, uint32_t cur_depth) {
|
||||
while (state->scope_top > 0 && state->scopes[state->scope_top - SKIP_ONE].depth >= cur_depth) {
|
||||
state->scope_top--;
|
||||
}
|
||||
}
|
||||
|
||||
// Recompute state flags from the current scope stack.
|
||||
static void recompute_state(WalkState *state, const char *module_qn) {
|
||||
state->enclosing_func_qn = module_qn;
|
||||
state->enclosing_class_qn = NULL;
|
||||
state->invocation_kind = CBM_INVOCATION_NONE;
|
||||
state->callee_expr = (TSNode){0};
|
||||
state->callee_leaf = (TSNode){0};
|
||||
state->inside_import = false;
|
||||
state->loop_depth = 0;
|
||||
state->branch_depth = 0;
|
||||
|
||||
for (int i = 0; i < state->scope_top; i++) {
|
||||
switch (state->scopes[i].kind) {
|
||||
case SCOPE_FUNC:
|
||||
state->enclosing_func_qn = state->scopes[i].qn;
|
||||
break;
|
||||
case SCOPE_CLASS:
|
||||
case SCOPE_NAMESPACE:
|
||||
state->enclosing_class_qn = state->scopes[i].qn;
|
||||
break;
|
||||
case SCOPE_CALL:
|
||||
state->invocation_kind = state->scopes[i].invocation_kind;
|
||||
state->callee_expr = state->scopes[i].callee_expr;
|
||||
state->callee_leaf = state->scopes[i].callee_leaf;
|
||||
break;
|
||||
case SCOPE_IMPORT:
|
||||
state->inside_import = true;
|
||||
break;
|
||||
case SCOPE_LOOP:
|
||||
state->loop_depth++;
|
||||
break;
|
||||
case SCOPE_BRANCH:
|
||||
state->branch_depth++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const CBMWalkScope *f = &state->scopes[--state->scope_top];
|
||||
state->enclosing_func_qn = f->prev_enclosing_func_qn;
|
||||
state->enclosing_class_qn = f->prev_enclosing_class_qn;
|
||||
state->invocation_kind = f->prev_invocation_kind;
|
||||
state->callee_expr = f->prev_callee_expr;
|
||||
state->callee_leaf = f->prev_callee_leaf;
|
||||
state->inside_import = f->prev_inside_import;
|
||||
state->loop_depth = f->prev_loop_depth;
|
||||
state->branch_depth = f->prev_branch_depth;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2089,6 +2100,16 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
|
||||
state.lexical_scope_capacity = INLINE_LEXICAL_SCOPES;
|
||||
state.usage_start_index = ctx->result->usages.count;
|
||||
state.root_lexical_scope_id = add_lexical_scope(&state, ctx->root, CBM_LEXICAL_SCOPE_MODULE);
|
||||
/* Base walk-state tuple (previously established by the first
|
||||
* recompute_state call): module scope, nothing else active. */
|
||||
state.enclosing_func_qn = ctx->module_qn;
|
||||
state.enclosing_class_qn = NULL;
|
||||
state.invocation_kind = CBM_INVOCATION_NONE;
|
||||
state.callee_expr = (TSNode){0};
|
||||
state.callee_leaf = (TSNode){0};
|
||||
state.inside_import = false;
|
||||
state.loop_depth = 0;
|
||||
state.branch_depth = 0;
|
||||
|
||||
uint32_t depth = 0;
|
||||
|
||||
@@ -2097,12 +2118,12 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
|
||||
bool trivia = is_unified_trivia_node(node);
|
||||
if (!trivia) {
|
||||
/* Trivia consumes no semantic state. Scope expiry may be deferred
|
||||
* until the next code-bearing node, which recomputes before use. */
|
||||
* until the next code-bearing node; pop restores the displaced
|
||||
* tuple and push applies the new frame's effect, both O(1) --
|
||||
* the old whole-stack recompute here was O(depth) per node and
|
||||
* quadratic on the deep-nesting torture tests. */
|
||||
pop_expired_scopes(&state, depth);
|
||||
recompute_state(&state, ctx->module_qn);
|
||||
if (push_pre_node_scope(ctx, node, spec, &state, depth)) {
|
||||
recompute_state(&state, ctx->module_qn);
|
||||
}
|
||||
(void)push_pre_node_scope(ctx, node, spec, &state, depth);
|
||||
|
||||
handle_string_constants(ctx, node, &state);
|
||||
handle_objectscript_type_map(ctx, node, &state);
|
||||
|
||||
@@ -59,6 +59,20 @@ typedef struct {
|
||||
CBMInvocationKind invocation_kind;
|
||||
TSNode callee_expr;
|
||||
TSNode callee_leaf;
|
||||
/* The complete walk-state tuple this frame displaced, restored verbatim on
|
||||
* pop. Saving the full tuple makes push and pop O(1) and kind-agnostic;
|
||||
* the previous design recomputed the state by iterating the WHOLE scope
|
||||
* stack on every code-bearing node, which is O(depth) per node and turned
|
||||
* the deep-nesting torture tests quadratic (0-1s on main, 39-119s here,
|
||||
* suite-budget kills on every non-M4 venue). */
|
||||
const char *prev_enclosing_func_qn;
|
||||
const char *prev_enclosing_class_qn;
|
||||
CBMInvocationKind prev_invocation_kind;
|
||||
TSNode prev_callee_expr;
|
||||
TSNode prev_callee_leaf;
|
||||
bool prev_inside_import;
|
||||
int prev_loop_depth;
|
||||
int prev_branch_depth;
|
||||
} CBMWalkScope;
|
||||
|
||||
typedef enum {
|
||||
|
||||
@@ -106,8 +106,15 @@ static bool is_reference_node(TSNode node, CBMLanguage lang) {
|
||||
|
||||
/* Some grammars expose the sigil/scope as a named wrapper around a generic
|
||||
* identifier. Keep exactly one occurrence: the wrapper carries the source
|
||||
* spelling that resolution needs (`$watched`, `a:watched`). */
|
||||
if (strcmp(kind, "identifier") == 0) {
|
||||
* spelling that resolution needs (`$watched`, `a:watched`).
|
||||
*
|
||||
* The language gate MUST precede the parent fetch: ts_node_parent descends
|
||||
* from the root (O(depth)), and fetching it for every identifier in every
|
||||
* language made deep-nesting extraction quadratic across the board (the
|
||||
* stack_overflow deep tests went 0-1s -> 39-119s and blew the suite
|
||||
* budget on every non-M4 venue). */
|
||||
if ((lang == CBM_LANG_PUPPET || lang == CBM_LANG_VIMSCRIPT) &&
|
||||
strcmp(kind, "identifier") == 0) {
|
||||
TSNode parent = ts_node_parent(node);
|
||||
if (!ts_node_is_null(parent) &&
|
||||
((lang == CBM_LANG_PUPPET && strcmp(ts_node_type(parent), "variable") == 0) ||
|
||||
|
||||
Reference in new issue
Block a user