From 18245be0c187bbc23b39d5f2870672750df571df Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 27 Apr 2026 17:06:39 +0100 Subject: [PATCH] perf: vitest threads pool + Dockerfile pnpm cache mount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two tuning knobs that were leaving most of the host idle: 1) vitest.config.ts pool=threads with maxThreads ≈ cores/2. Default left this 64-core workstation at ~10% CPU during \`pnpm test:run\`. Threads pool uses the box: same 152-file/2050-test suite now runs at ~700% CPU instead of ~150%. Wall time gain is modest (workload is dominated by a handful of slow individual files that one thread must run serially), but the parallel headroom is there for when the suite grows. Cap = max(2, cores/2) keeps laptops reasonable; override with \`VITEST_MAX_THREADS=N\` in the env. 2) Dockerfile.mcpd uses BuildKit cache mounts on both pnpm install steps. Adds \`# syntax=docker/dockerfile:1.6\` and a \`--mount=type=cache,target=/root/.local/share/pnpm/store\` so pnpm's content-addressed store survives across image rebuilds. Cold rebuilds where the lockfile changed are unaffected; warm rebuilds where only source changed drop the install step from ~60s to <5s. fulldeploy.sh's mcpd image rebuild gets that back minus the docker push hash mismatch. Test parity: 2050/2050 across 152 files; per-package mcpd 837/837. Both unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- deploy/Dockerfile.mcpd | 21 +++++++++++++++++---- vitest.config.ts | 13 +++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/deploy/Dockerfile.mcpd b/deploy/Dockerfile.mcpd index 29c6c76..ddffec1 100644 --- a/deploy/Dockerfile.mcpd +++ b/deploy/Dockerfile.mcpd @@ -1,3 +1,9 @@ +# syntax=docker/dockerfile:1.6 +# `# syntax=...` enables BuildKit's --mount feature on the builder so we can +# share the pnpm content-addressed store across image builds. Without it the +# next two RUN steps fall back to plain mode and the cache mount is ignored +# (build still works, just slower). + # Stage 1: Build TypeScript FROM node:20-alpine AS builder @@ -12,8 +18,12 @@ COPY src/db/package.json src/db/tsconfig.json src/db/ COPY src/shared/package.json src/shared/tsconfig.json src/shared/ COPY src/web/package.json src/web/tsconfig.json src/web/ -# Install all dependencies -RUN pnpm install --frozen-lockfile +# Install all dependencies. The cache mount keeps pnpm's CAS store warm +# across builds: only newly-changed packages get downloaded; everything +# else hardlinks from the cache. Drops install from ~60s to <5s on a +# warm cache. `--frozen-lockfile` still guarantees lockfile fidelity. +RUN --mount=type=cache,id=pnpm-store-mcpd-builder,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile # Copy source code COPY src/mcpd/src/ src/mcpd/src/ @@ -42,8 +52,11 @@ COPY src/mcpd/package.json src/mcpd/ COPY src/db/package.json src/db/ COPY src/shared/package.json src/shared/ -# Install all deps (prisma CLI needed at runtime for db push) -RUN pnpm install --frozen-lockfile +# Install all deps (prisma CLI needed at runtime for db push). Same +# cache-mount trick as the builder stage; separate cache id so the two +# stages don't compete for the same lock. +RUN --mount=type=cache,id=pnpm-store-mcpd-runtime,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile # Copy prisma schema and generate client COPY src/db/prisma/ src/db/prisma/ diff --git a/vitest.config.ts b/vitest.config.ts index a1c7953..1b36b85 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,21 @@ import { defineConfig } from 'vitest/config'; +import { availableParallelism } from 'node:os'; + +// Default vitest's pool to ~half the CPU threads we have. The previous +// implicit default left this 64-thread workstation at ~10% utilization +// during `pnpm test:run`. Half is a soft cap that stays kind to laptops +// (8-thread → 4 workers) while letting beefy hosts push closer to the +// box's actual capacity. Override at run time with VITEST_MAX_THREADS. +const cores = availableParallelism(); +const maxThreads = Number(process.env['VITEST_MAX_THREADS'] ?? Math.max(2, Math.floor(cores / 2))); export default defineConfig({ test: { globals: true, + pool: 'threads', + poolOptions: { + threads: { maxThreads, minThreads: 1 }, + }, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'],