Files
fusion/packages/engine/src/verification-concurrency.ts
gsxdsm e9f14bf024 perf: speed up local pnpm build and cap stacked verifications (#2134)
## Summary

- Extend the workspace content-hash skip cache to **all** packages (not
just plugins), with `--force` / `--full` flags
- Default local CLI packaging to a **fast mode** (bin/extension +
migrations only); full desktop/plugin/DTS staging runs on CI or `pnpm
build:full`
- Enable TypeScript `incremental` builds for warm recompiles
- Add `maxConcurrentVerifications` (default **1**) so concurrent tasks
cannot stack monorepo typecheck/build and peg CPU

Warm `pnpm build` measured ~**126s → ~0.8s** when nothing changed.

## Test plan

- [x] `node --test scripts/__tests__/build-workspace.test.mjs` (12 pass)
- [x] `pnpm --filter @fusion/engine exec vitest run
src/__tests__/verification-concurrency.test.ts`
- [x] `pnpm --filter @fusion/core exec vitest run
src/__tests__/settings-parity.test.ts`
- [x] Local: first `pnpm build` rebuilds as needed; second warm `pnpm
build` skips all packages (~0.8s)
- [x] Fast CLI packaging logs skip of desktop/plugin staging without
`FUSION_CLI_FULL_PACKAGE`
- [ ] CI: `pnpm build` still full-packages under `CI=true` (plugin
staging / release surfaces)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a Scheduling setting to limit concurrent verification tasks from
1–8, with a default of 1.
* Verification tasks now support cancellation while waiting or running.
  * Added options for forced and full workspace builds.

* **Performance**
* Local builds can skip unchanged packages and use incremental
compilation for faster rebuilds.
* Local CLI packaging is faster by default, while full packaging remains
available when needed.

* **Documentation**
* Updated the settings reference with the new verification concurrency
option.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-15 08:44:11 -07:00

106 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
FNXC:VerificationConcurrency 2026-07-15-03:35:
Multiple in-progress tasks each calling fn_run_verification (often `pnpm verify:fast` / full typecheck+build) pegged CPU by running several monorepo compiles in parallel. Cap concurrent verification subprocesses project-wide so task concurrency can stay higher without stacking heavy builds. Default limit is 1; operators raise maxConcurrentVerifications when the machine has spare cores.
FNXC:VerificationConcurrency 2026-07-15-08:20:
Greptile P1/P2: (1) clamp 1–8 so programmatic settings cannot open 50 slots; (2) do not re-set the process limit on every verification start (multi-project races last-writer-wins) — wire the limit from engine settings load/update only; (3) honor AbortSignal while queued so cancelled merge/verification does not block the slot queue.
FNXC:VerificationConcurrency 2026-07-15-09:05:
Greptile P1 multi-project: multiple ProjectEngine instances must not last-write the singleton limit. Register each project's desired cap; the effective process limit is the MIN of registered caps (most restrictive wins) so a project set to 1 cannot be overridden by a peer set to 8.
*/
import { AgentSemaphore, PRIORITY_EXECUTE } from "./concurrency.js";
/** Hard ceiling matching the Scheduling UI max. */
export const MAX_CONCURRENT_VERIFICATIONS_HARD_CAP = 8;
/** Floor — at least one verification can always run. */
export const MIN_CONCURRENT_VERIFICATIONS = 1;
/** projectId -> clamped desired limit for that engine instance */
const projectLimits = new Map<string, number>();
let fallbackLimit = MIN_CONCURRENT_VERIFICATIONS;
const verificationSemaphore = new AgentSemaphore(() => resolveEffectiveLimit());
/**
* Clamp a raw setting/API value into the enforced verification concurrency range.
*/
export function clampMaxConcurrentVerifications(next: number): number {
if (!Number.isFinite(next)) return MIN_CONCURRENT_VERIFICATIONS;
return Math.min(
MAX_CONCURRENT_VERIFICATIONS_HARD_CAP,
Math.max(MIN_CONCURRENT_VERIFICATIONS, Math.floor(next)),
);
}
function resolveEffectiveLimit(): number {
if (projectLimits.size === 0) return fallbackLimit;
let min = MAX_CONCURRENT_VERIFICATIONS_HARD_CAP;
for (const value of projectLimits.values()) {
if (value < min) min = value;
}
return min;
}
/**
* Register or update one project's desired verification concurrency.
* Effective process limit = min(registered project caps).
*/
export function registerProjectVerificationLimit(projectId: string, next: number): void {
if (!projectId) return;
projectLimits.set(projectId, clampMaxConcurrentVerifications(next));
}
/**
* Drop a project's registration when its engine stops so stale caps do not pin the min forever.
*/
export function unregisterProjectVerificationLimit(projectId: string): void {
if (!projectId) return;
projectLimits.delete(projectId);
}
/**
* Legacy setter used by tests and single-engine paths without a project id.
* Sets the fallback limit when no projects are registered; when projects are
* registered this is ignored for the effective min (use registerProjectVerificationLimit).
*/
export function setMaxConcurrentVerifications(next: number): void {
fallbackLimit = clampMaxConcurrentVerifications(next);
}
/** Current effective verification concurrency limit (after clamping / min aggregation). */
export function getMaxConcurrentVerifications(): number {
return verificationSemaphore.limit;
}
/** Test helper: clear project registrations. */
export function resetVerificationLimitRegistryForTests(): void {
projectLimits.clear();
fallbackLimit = MIN_CONCURRENT_VERIFICATIONS;
}
/**
* Run `fn` while holding one verification slot. Waiters queue at execute priority.
* When `signal` aborts while queued, the waiter is removed and the promise rejects
* with AbortError so cancelled work does not block the queue.
*/
export async function withVerificationSlot<T>(
fn: () => Promise<T>,
signal?: AbortSignal,
): Promise<T> {
await verificationSemaphore.acquire(PRIORITY_EXECUTE, signal);
try {
if (signal?.aborted) {
const err = new Error("The operation was aborted");
err.name = "AbortError";
throw err;
}
return await fn();
} finally {
verificationSemaphore.release();
}
}
/** Test/diagnostic access to the underlying semaphore. */
export function getVerificationSemaphore(): AgentSemaphore {
return verificationSemaphore;
}