Skip to content

Commit 0475a88

Browse files
committed
perf(extract): dual-ended work queue bounds concurrent giant parses
Phase-aligned RSS attribution located the global peak (24.4 GB at t=47s, ~14 GB transient above the retained floor) INSIDE parallel extraction: the size-descending queue put the kernel's 12 largest files into 12 workers simultaneously, each holding a multi-MB source + its 10-50x TSTree + arena. Every post-extraction optimization sat below this waterline. Replace the single biggest-first cursor with a dual-ended claim queue over the same size-sorted list: big_workers = clamp(workers/6, 1, 3) claim from the BIG end while the rest rush the small end, so at most big_workers giant working-sets coexist -- a structural bound, not a scheduling heuristic (a static interleave cannot bound concurrency under dynamic pulls: smalls finish in ms and workers converge back onto the giants). Giants still start at t=0, preserving the LPT no-straggler property; workers never idle while either end has files. Per-file claim flags (atomic exchange) make the crossover race-free; end-of-queue rescans are O(n) bytes once. Intra-file parallel parsing was researched and ruled out: tree-sitter has no such concept (incremental != parallel; one parser per thread is the model), and data-parallel parsing theory (PAPAGENO through SLE 2025 cyclic OPG work) requires operator-precedence grammars with local parsability -- none of the 159 vendored GLR grammars qualify, and C's preprocessor makes byte-chunking unsound. Output is schedule-independent since the determinism fix and verified so: xfs sorted edge set byte-identical to the previous commit. pipeline 216 passed. Worst case (all-giant repo) unchanged by design. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent ff61624 commit 0475a88

1 file changed

Lines changed: 65 additions & 6 deletions

File tree

src/pipeline/pass_parallel.c

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,22 @@ typedef struct {
687687
CBMFileResult **result_cache;
688688
_Atomic int64_t *shared_ids;
689689
_Atomic int *cancelled;
690-
_Atomic int next_file_idx;
690+
_Atomic int next_file_idx; /* legacy single-cursor (unused when claimed != NULL) */
691+
692+
/* Dual-ended work queue (peak-RSS control): sorted[] is size-DESCENDING;
693+
* a small band of workers (big_workers = f(worker count)) claims from the
694+
* BIG end while the rest claim from the SMALL end. The number of giant
695+
* parse working-sets in flight is structurally bounded by big_workers —
696+
* the old single biggest-first cursor put the 12 largest files of the
697+
* kernel into 12 workers at once (global RSS peak 24.4 GB at t=47s, a
698+
* ~14 GB transient above the retained floor). Giants still start at t=0
699+
* (LPT makespan preserved). Per-file claim flags make the crossover
700+
* race-free; hints are advisory scan starts. Output is schedule-
701+
* independent (content-canonical since the determinism fix). */
702+
_Atomic unsigned char *claimed;
703+
_Atomic int big_hint;
704+
_Atomic int small_hint;
705+
int big_workers;
691706

692707
cbm_pkg_entries_t *pkg_entries; /* per-worker manifest arrays (separate allocation) */
693708

@@ -710,6 +725,28 @@ typedef struct {
710725
_Atomic int bp_futile;
711726
} extract_ctx_t;
712727

728+
/* Claim the next unclaimed file from one end of the dual-ended queue.
729+
* Returns the sorted[] index, or -1 when the queue is exhausted. */
730+
static int pp_claim_next(extract_ctx_t *ec, bool from_big) {
731+
if (from_big) {
732+
for (int i = atomic_load_explicit(&ec->big_hint, memory_order_relaxed);
733+
i < ec->file_count; i++) {
734+
if (!atomic_exchange_explicit(&ec->claimed[i], 1, memory_order_relaxed)) {
735+
atomic_store_explicit(&ec->big_hint, i + 1, memory_order_relaxed);
736+
return i;
737+
}
738+
}
739+
} else {
740+
for (int i = atomic_load_explicit(&ec->small_hint, memory_order_relaxed); i >= 0; i--) {
741+
if (!atomic_exchange_explicit(&ec->claimed[i], 1, memory_order_relaxed)) {
742+
atomic_store_explicit(&ec->small_hint, i - 1, memory_order_relaxed);
743+
return i;
744+
}
745+
}
746+
}
747+
return -1;
748+
}
749+
713750
/* Cap on the number of index.file_oversized WARN lines (the full list still goes
714751
* to the response/logfile — this only throttles the stderr noise). */
715752
enum { PP_OVERSIZED_WARN_MAX = 32 };
@@ -766,12 +803,22 @@ static void extract_worker(int worker_id, void *ctx_ptr) {
766803
ws->local_gbuf = cbm_gbuf_new_shared_ids(ec->project_name, ec->repo_path, ec->shared_ids);
767804
}
768805

769-
/* Pull files from shared atomic counter */
806+
/* Pull files: dual-ended claim queue (see extract_ctx_t), with the
807+
* legacy single cursor as fallback when claim flags are unavailable. */
808+
bool from_big = worker_id < ec->big_workers;
770809
while (SKIP_ONE) {
771-
int sort_pos =
772-
atomic_fetch_add_explicit(&ec->next_file_idx, SKIP_ONE, memory_order_relaxed);
773-
if (sort_pos >= ec->file_count) {
774-
break;
810+
int sort_pos;
811+
if (ec->claimed) {
812+
sort_pos = pp_claim_next(ec, from_big);
813+
if (sort_pos < 0) {
814+
break;
815+
}
816+
} else {
817+
sort_pos = atomic_fetch_add_explicit(&ec->next_file_idx, SKIP_ONE,
818+
memory_order_relaxed);
819+
if (sort_pos >= ec->file_count) {
820+
break;
821+
}
775822
}
776823
if (atomic_load_explicit(ec->cancelled, memory_order_relaxed)) {
777824
break;
@@ -1116,6 +1163,16 @@ int cbm_parallel_extract_ex(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
11161163
};
11171164
atomic_init(&ec.next_worker_id, 0);
11181165
atomic_init(&ec.next_file_idx, 0);
1166+
ec.claimed = calloc((size_t)file_count, sizeof(_Atomic unsigned char));
1167+
atomic_init(&ec.big_hint, 0);
1168+
atomic_init(&ec.small_hint, file_count - 1);
1169+
ec.big_workers = worker_count / 6;
1170+
if (ec.big_workers < 1) {
1171+
ec.big_workers = 1;
1172+
}
1173+
if (ec.big_workers > 3) {
1174+
ec.big_workers = 3;
1175+
}
11191176
atomic_init(&ec.retained_bytes, 0);
11201177
atomic_init(&ec.retain_cap_warned, 0);
11211178
atomic_init(&ec.oversized_warned, 0);
@@ -1126,6 +1183,8 @@ int cbm_parallel_extract_ex(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
11261183
cbm_parallel_for_opts_t parallel_opts = {.max_workers = worker_count, .force_pthreads = false};
11271184
cbm_parallel_for(worker_count, extract_worker, &ec, parallel_opts);
11281185
CBM_PROF_END_N("parallel_extract", "3_dispatch_workers_parallel", t_dispatch, file_count);
1186+
free((void *)ec.claimed);
1187+
ec.claimed = NULL;
11291188

11301189
/* Sub-phase: Merge all local gbufs into main gbuf (SEQUENTIAL, gbuf not thread-safe) */
11311190
CBM_PROF_START(t_merge);

0 commit comments

Comments
 (0)