|
@@ -15,6 +15,7 @@ import * as path from 'path';
|
|
|
import * as os from 'os';
|
|
import * as os from 'os';
|
|
|
import type { Edge, UnresolvedReference } from '../types';
|
|
import type { Edge, UnresolvedReference } from '../types';
|
|
|
import type { ResolvedRef, UnresolvedRef } from './types';
|
|
import type { ResolvedRef, UnresolvedRef } from './types';
|
|
|
|
|
+import { memoryBudgetBytes } from './memory-budget';
|
|
|
|
|
|
|
|
/** One synthesis pass's output: its edge list + worker-measured wall clock. */
|
|
/** One synthesis pass's output: its edge list + worker-measured wall clock. */
|
|
|
export interface SynthPassResult {
|
|
export interface SynthPassResult {
|
|
@@ -64,17 +65,70 @@ export class ResolverPool {
|
|
|
private synthWaiters = new Map<number, { resolve: (r: SynthPassResult) => void; reject: (e: Error) => void }>();
|
|
private synthWaiters = new Map<number, { resolve: (r: SynthPassResult) => void; reject: (e: Error) => void }>();
|
|
|
private failed: Error | null = null;
|
|
private failed: Error | null = null;
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Pool size from CPU headroom, memory headroom, and the explicit override.
|
|
|
|
|
+ * Pure — every input injected — so the whole matrix is unit-testable.
|
|
|
|
|
+ *
|
|
|
|
|
+ * CPU term: `availableParallelism` (cpuset/affinity-honest — `os.cpus()`
|
|
|
|
|
+ * enumerates the host's CPUs and sized SIX workers inside a 2-CPU cpuset,
|
|
|
|
|
+ * §7a.1's false-premise finding), minus one for the persisting main thread,
|
|
|
|
|
+ * floored at 2 so a true 2-core box keeps the pool's ~2× on synthesis,
|
|
|
|
|
+ * capped at the long-standing 6.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Memory term: workers hold real heap at scale (~1GB each against a 4.6GB
|
|
|
|
|
+ * kernel-scale DB — six of them OOM-killed a 7GB container once real
|
|
|
|
|
+ * 8-core concurrency let them peak simultaneously). Estimate per-worker
|
|
|
|
|
+ * cost from the DB size, keep 30% of the budget for the main thread, and
|
|
|
|
|
+ * let the smaller term win. Below 2 workers the pool isn't worth its boot
|
|
|
|
|
+ * cost — callers get null and stay sequential.
|
|
|
|
|
+ */
|
|
|
|
|
+ static resolvePoolSize(opts: {
|
|
|
|
|
+ explicit?: string;
|
|
|
|
|
+ availableParallelism: number;
|
|
|
|
|
+ memoryBudget: number;
|
|
|
|
|
+ dbSizeBytes: number;
|
|
|
|
|
+ }): number | null {
|
|
|
|
|
+ if (opts.explicit !== undefined && opts.explicit !== '') {
|
|
|
|
|
+ const n = Number.parseInt(opts.explicit, 10);
|
|
|
|
|
+ if (Number.isFinite(n)) {
|
|
|
|
|
+ if (n <= 0) return null;
|
|
|
|
|
+ return Math.min(n, 16);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const cpuCap = Math.max(2, Math.min(opts.availableParallelism - 1, 6));
|
|
|
|
|
+ const perWorker = Math.min(Math.max(opts.dbSizeBytes * 0.2, 256 * 1024 * 1024), 1.5 * 1024 * 1024 * 1024);
|
|
|
|
|
+ const memCap = Math.floor((opts.memoryBudget * 0.7) / perWorker);
|
|
|
|
|
+ const size = Math.min(cpuCap, memCap);
|
|
|
|
|
+ return size >= 2 ? size : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Create a pool when the compiled worker exists (absent when running from
|
|
* Create a pool when the compiled worker exists (absent when running from
|
|
|
* source in tests → callers use the sequential path), the kill switch is
|
|
* source in tests → callers use the sequential path), the kill switch is
|
|
|
- * off, and the machine has cores to spare. Returns null otherwise.
|
|
|
|
|
|
|
+ * off, and the machine has the cores AND memory to carry it. Returns null
|
|
|
|
|
+ * otherwise. `CODEGRAPH_RESOLVE_WORKERS` overrides the computed size
|
|
|
|
|
+ * (0 disables the pool; values are capped at 16).
|
|
|
*/
|
|
*/
|
|
|
static tryCreate(dbPath: string, projectRoot: string): ResolverPool | null {
|
|
static tryCreate(dbPath: string, projectRoot: string): ResolverPool | null {
|
|
|
if (process.env.CODEGRAPH_NO_PARALLEL_RESOLVE === '1') return null;
|
|
if (process.env.CODEGRAPH_NO_PARALLEL_RESOLVE === '1') return null;
|
|
|
const workerScript = path.join(__dirname, 'resolver-worker.js');
|
|
const workerScript = path.join(__dirname, 'resolver-worker.js');
|
|
|
if (!fs.existsSync(workerScript)) return null;
|
|
if (!fs.existsSync(workerScript)) return null;
|
|
|
- const size = Math.max(1, Math.min(os.cpus().length - 2, 6));
|
|
|
|
|
- if (size < 2) return null;
|
|
|
|
|
|
|
+ let dbSizeBytes = 0;
|
|
|
|
|
+ try {
|
|
|
|
|
+ dbSizeBytes = fs.statSync(dbPath).size;
|
|
|
|
|
+ } catch { /* fresh/missing file — the 256MB per-worker floor applies */ }
|
|
|
|
|
+ const size = ResolverPool.resolvePoolSize({
|
|
|
|
|
+ explicit: process.env.CODEGRAPH_RESOLVE_WORKERS,
|
|
|
|
|
+ availableParallelism: os.availableParallelism(),
|
|
|
|
|
+ memoryBudget: memoryBudgetBytes(),
|
|
|
|
|
+ dbSizeBytes,
|
|
|
|
|
+ });
|
|
|
|
|
+ if (size === null) return null;
|
|
|
|
|
+ if (process.env.CODEGRAPH_SYNTH_TIMINGS) {
|
|
|
|
|
+ console.error(
|
|
|
|
|
+ `[pool-timing] pool size=${size} (ap=${os.availableParallelism()} budget=${Math.round(memoryBudgetBytes() / 1024 / 1024)}MB db=${Math.round(dbSizeBytes / 1024 / 1024)}MB)`
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
try {
|
|
try {
|
|
|
return new ResolverPool(workerScript, dbPath, projectRoot, size);
|
|
return new ResolverPool(workerScript, dbPath, projectRoot, size);
|
|
|
} catch {
|
|
} catch {
|