Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a6e271a
perf(computeTaskMetrics): replace map/reduce with int32array/loops
seonghobae Aug 15, 2026
7d07992
test(perf): lock task metric cache semantics
seonghobae Aug 15, 2026
e394998
test(perf): run task metric regression in coverage
seonghobae Aug 15, 2026
d19d560
test(perf): add exact-head task metric benchmark evidence
seonghobae Aug 15, 2026
3f3b6e6
perf(computeTaskMetrics): replace map/reduce with int32array/loops
seonghobae Aug 15, 2026
a118883
test(perf): benchmark metric computation against protected base
seonghobae Aug 15, 2026
e1ca5b7
ci(perf): run exact-base metrics benchmark
seonghobae Aug 15, 2026
0094fc0
test(perf): instrument metric benchmark without production globals
seonghobae Aug 15, 2026
765583d
Merge branch 'develop' into bolt-optimize-compute-metrics-50020643380…
opencode-agent[bot] Aug 16, 2026
cd52c06
merge(perf): reconcile protected XML import baseline
seonghobae Aug 16, 2026
038b8e3
Merge protected develop into metrics performance branch
seonghobae Aug 17, 2026
514b086
test(perf): require immutable benchmark base on push
seonghobae Aug 17, 2026
5a17020
fix(perf): resolve benchmark base for protected pushes
seonghobae Aug 17, 2026
6bb681d
fix(perf): fail closed on missing benchmark baselines
seonghobae Aug 17, 2026
e27f57d
test(perf): run benchmark base contract in unit CI
seonghobae Aug 17, 2026
24a4e5c
test(perf): always close benchmark browser contexts
seonghobae Aug 17, 2026
3a48fcf
merge(develop): reconcile metrics performance with OpenCode config
seonghobae Aug 17, 2026
1699198
merge(develop): reconcile metrics performance with current protected …
seonghobae Aug 19, 2026
0ae5a54
fix(stack): preserve protected orchestrator while reconciling metrics…
seonghobae Aug 19, 2026
77daa1d
fix(stack): inherit protected Hono runtime baseline
seonghobae Aug 20, 2026
e77846d
fix(stack): inherit protected Playwright 1.62.1 in metrics benchmark
seonghobae Aug 20, 2026
da9b54e
test(perf): expose metrics benchmark order bias
seonghobae Aug 24, 2026
9b6d98f
fix(perf): counterbalance metrics benchmark timings
seonghobae Aug 24, 2026
f119ac3
fix(perf): counterbalance metrics browser benchmark
seonghobae Aug 25, 2026
b36361e
test(perf): require immutable benchmark candidate
seonghobae Aug 25, 2026
32bc31b
fix(perf): bind metrics benchmark candidate identity
seonghobae Aug 25, 2026
59a8f03
fix(perf): benchmark exact contributor revision
seonghobae Aug 25, 2026
1cc8b59
test(perf): expose stale protected-base benchmark
seonghobae Aug 25, 2026
f6fafe1
fix(perf): verify live protected benchmark base
seonghobae Aug 25, 2026
388406f
fix(perf): reject stale protected-base benchmark
seonghobae Aug 25, 2026
642034f
test(perf): isolate benchmark from cloud e2e
seonghobae Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/server-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ jobs:
run: npx playwright install chromium --with-deps
- name: Cloud UI e2e
run: npm run test:e2e:cloud
- name: Metrics performance benchmark
run: npm run test:e2e:benchmark
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
## 2026-07-12 - Optimize renderTaskRow DOM allocations
**Learning:** Caching unattached template nodes and instantiating them via `.cloneNode(false)` reduces DOM instantiation overhead in O(N) render loops significantly.
**Action:** Apply this optimization to other hot-path rendering elements such as rows, cells, and stack containers.
## 2026-08-15 - O(N) penalty with Map and reduce/forEach
**Learning:** Using Map caching and Array.prototype.reduce/forEach in O(N) loops incurs overhead from hash lookups, callback allocation, and garbage collection, degrading performance in hot paths.
**Action:** For high-performance O(N) loops in JavaScript, replace Array.prototype.reduce/forEach and Map caching with standard for loops and typed arrays (e.g., Int32Array) to eliminate JS engine overhead.
21 changes: 12 additions & 9 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1370,21 +1370,24 @@ function validateDateRange(startLabel, startValue, endLabel, endValue, errors) {
}

function computeTaskMetrics() {
// ⚡ Bolt: Cache durationDays during total calculation to avoid recalculating for every task
const durationCache = new Map();
const totalDays = state.tasks.reduce((sum, task) => {
let totalDays = 0;
// ⚡ Bolt: Replace Map with Int32Array and reduce/forEach with standard for loops to eliminate hash-lookup, callback allocation, and GC overhead
const durationCache = new Int32Array(state.tasks.length);
for (let i = 0; i < state.tasks.length; i++) {
const task = state.tasks[i];
const duration = calculateDurationDays(task.plannedStartDate, task.plannedEndDate);
durationCache.set(task.id, duration);
return sum + duration;
}, 0);
durationCache[i] = duration;
totalDays += duration;
}

const baseDate = state.baseDate;
const byTask = new Map();
let totalWeightedPlannedRatio = 0;
let totalWeightedActualRatio = 0;

state.tasks.forEach((task) => {
const durationDays = durationCache.get(task.id);
for (let i = 0; i < state.tasks.length; i++) {
const task = state.tasks[i];
const durationDays = durationCache[i];
const weightRatio = totalDays > 0 ? durationDays / totalDays : 0;
const plannedProgressRatio = calculatePlannedProgressRatio(baseDate, task.plannedStartDate, task.plannedEndDate, durationDays);
const actualProgressRatio = (ACTUAL_PROGRESS_MAP[task.actualProgressStatus] || 0) / 100;
Expand All @@ -1408,7 +1411,7 @@ function computeTaskMetrics() {
plannedDateWarning,
actualDateWarning
});
});
}
Comment thread
seonghobae marked this conversation as resolved.

return {
totalDays,
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
"coverage": "npm run test:coverage",
"server": "node server/server.mjs",
"test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs && node tests/api/orchestrator-attribution.test.mjs",
"test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.test.mjs",
"test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/metrics-performance-base.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.test.mjs",
"test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/auth.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases",
"test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api",
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:cloud": "playwright install chromium && playwright test tests/e2e/cloud.spec.js tests/e2e/toast-accessibility.spec.js",
"test:e2e:benchmark": "playwright install chromium && playwright test tests/e2e/metrics-performance.spec.js",
"test:fuzz": "playwright install chromium && playwright test tests/e2e/csv_formula_fuzz.spec.js",
"fuzz": "node --test tests/fuzz/*.mjs"
},
Expand Down
281 changes: 281 additions & 0 deletions tests/e2e/metrics-performance.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';

import { test, expect } from '@playwright/test';

import {
counterbalancedBenchmarkRounds,
resolveBenchmarkCandidateSha,
resolveVerifiedBenchmarkBaseSha,
summarizeCounterbalancedSamples,
} from '../helpers/benchmark-base.mjs';

test.describe.configure({ retries: process.env.CI ? 2 : 0 });

const TASK_COUNT = 10_000;
const SAMPLE_COUNT = 7;
const WARMUP_COUNT = 3;
const TARGET_IMPROVEMENT_PERCENT = 15;
const BASE_DATE = '2026-02-15';

const DATE_WINDOWS = Object.freeze([
['2026-01-01', '2026-01-02'],
['2026-01-02', '2026-01-12'],
['2026-02-01', '2026-03-01'],
['2026-02-15', '2026-02-15'],
]);

function githubEvent() {
const eventPath = process.env.GITHUB_EVENT_PATH;
return eventPath ? JSON.parse(readFileSync(eventPath, 'utf8')) : {};
}

function readOriginBranchTip(baseRef) {
const branch = String(baseRef || '').trim();
if (!branch || branch.length > 255 || /[\u0000-\u001f\u007f]/u.test(branch)) {
throw new Error(`Invalid benchmark base ref: ${branch || '<missing>'}`);
}
const fullRef = `refs/heads/${branch}`;
let output;
try {
output = execFileSync('git', ['ls-remote', '--heads', 'origin', fullRef], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
} catch {
throw new Error(`Unable to resolve live benchmark base ${fullRef}`);
}

const matches = output
.split(/\r?\n/u)
.filter(Boolean)
.map((line) => line.split(/\s+/u))
.filter(([, remoteRef]) => remoteRef === fullRef);
if (matches.length !== 1) {
throw new Error(`Expected exactly one live benchmark base for ${fullRef}, found ${matches.length}`);
}
return matches[0][0];
}

function readGitFile(commitSha, path) {
const normalizedCommitSha = String(commitSha || '');
if (!/^[a-f0-9]{40}$/.test(normalizedCommitSha)) {
throw new Error(`Invalid benchmark commit SHA: ${normalizedCommitSha || '<missing>'}`);
}

const spec = `${normalizedCommitSha}:${path}`;
try {
return execFileSync('git', ['show', spec], { encoding: 'utf8' });
} catch {
execFileSync('git', ['fetch', '--depth=1', 'origin', normalizedCommitSha], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
return execFileSync('git', ['show', spec], { encoding: 'utf8' });
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function instrumentMetricsSource(source) {
const bootstrapCall = '\nbootstrap();';
const bootstrapIndex = source.lastIndexOf(bootstrapCall);
if (bootstrapIndex === -1) {
throw new Error('Benchmark app source is missing the expected bootstrap call');
}

const withoutBootstrap = `${source.slice(0, bootstrapIndex)}${source.slice(bootstrapIndex + bootstrapCall.length)}`;
return `${withoutBootstrap}\n\nwindow.__scopeweaveMetricsBenchmark = Object.freeze({\n seed(tasks, baseDate) {\n state.tasks = tasks;\n state.baseDate = baseDate;\n },\n compute() {\n return computeTaskMetrics();\n },\n});\n`;
}

function createTask(index) {
const [plannedStartDate, plannedEndDate] = DATE_WINDOWS[index % DATE_WINDOWS.length];
return {
id: `metrics-performance-${index}`,
parentId: null,
depth: 1,
expanded: true,
pendingDelete: false,
isSynthetic: false,
phase: `Phase ${index}`,
activity: '',
task: '',
categoryLarge: '',
categoryMedium: '',
documentName: '',
owner: `owner-${index % 17}`,
supportTeam: '',
plannedStartDate,
plannedEndDate,
actualProgressStatus: '미착수(0%)',
actualStartDate: '',
actualEndDate: '',
predecessors: '',
budget: '',
actualCost: '',
sprint: '',
storyPoints: '',
};
}

async function measureMetrics(browser, { appSource, label }) {
const context = await browser.newContext();
try {
const page = await context.newPage();
const instrumentedSource = instrumentMetricsSource(appSource);

await page.route('**/app.js', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/javascript; charset=utf-8',
body: instrumentedSource,
});
});

await page.goto('/');
const tasks = Array.from({ length: TASK_COUNT }, (_, index) => createTask(index));

const result = await page.evaluate(async ({ seededTasks, baseDate, sampleCount, warmupCount }) => {
const benchmark = window.__scopeweaveMetricsBenchmark;
if (!benchmark) throw new Error('metrics benchmark bridge did not initialize');
benchmark.seed(seededTasks, baseDate);

for (let warmup = 0; warmup < warmupCount; warmup += 1) {
benchmark.compute();
}

const samples = [];
for (let sample = 0; sample < sampleCount; sample += 1) {
const startedAt = performance.now();
benchmark.compute();
samples.push(performance.now() - startedAt);
}
Comment thread
seonghobae marked this conversation as resolved.

const metrics = benchmark.compute();
const entries = Array.from(metrics.byTask, ([taskId, taskMetrics]) => [
taskId,
taskMetrics.durationDays,
taskMetrics.weightRatio,
taskMetrics.plannedProgressRatio,
taskMetrics.actualProgressRatio,
taskMetrics.weightedPlannedRatio,
taskMetrics.weightedActualRatio,
taskMetrics.progressState.label,
taskMetrics.progressState.className,
taskMetrics.plannedDateWarning,
taskMetrics.actualDateWarning,
]);
const snapshot = JSON.stringify({
totalDays: metrics.totalDays,
totalWeightedPlannedRatio: metrics.totalWeightedPlannedRatio,
totalWeightedActualRatio: metrics.totalWeightedActualRatio,
entries,
});
const digestBytes = new Uint8Array(await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(snapshot),
));
const digest = Array.from(digestBytes, (byte) => byte.toString(16).padStart(2, '0')).join('');

return {
samples,
digest,
totalDays: metrics.totalDays,
byTaskSize: metrics.byTask.size,
totalWeightedPlannedRatio: metrics.totalWeightedPlannedRatio,
totalWeightedActualRatio: metrics.totalWeightedActualRatio,
};
}, {
seededTasks: tasks,
baseDate: BASE_DATE,
sampleCount: SAMPLE_COUNT,
warmupCount: WARMUP_COUNT,
});

return { label, ...result };
} finally {
await context.close();
}
}

test('10,000-task metric computation preserves exact semantics without median regression', async ({ browser }) => {
test.setTimeout(240_000);

const event = githubEvent();
const resolveCurrentBaseSha = () => resolveVerifiedBenchmarkBaseSha({
override: process.env.SCOPEWEAVE_BENCHMARK_BASE_SHA,
event,
readLiveBaseSha: readOriginBranchTip,
});
const baseSha = resolveCurrentBaseSha();
const candidateSha = resolveBenchmarkCandidateSha({
override: process.env.SCOPEWEAVE_BENCHMARK_HEAD_SHA,
event,
});
const sourceByLabel = new Map([
['protected-base', readGitFile(baseSha, 'app.js')],
['candidate', readGitFile(candidateSha, 'app.js')],
]);
const measurementOrder = counterbalancedBenchmarkRounds();
const measurements = [];

for (const round of measurementOrder) {
for (const label of round) {
measurements.push(await measureMetrics(browser, {
appSource: sourceByLabel.get(label),
label,
}));
}
}

const semanticReference = measurements[0];
for (const measurement of measurements) {
expect(measurement.byTaskSize).toBe(TASK_COUNT);
expect(measurement.samples).toHaveLength(SAMPLE_COUNT);
expect(measurement.samples.every((duration) => duration > 0)).toBe(true);
expect(measurement.digest).toBe(semanticReference.digest);
expect(measurement.totalDays).toBe(semanticReference.totalDays);
expect(measurement.totalWeightedPlannedRatio).toBe(semanticReference.totalWeightedPlannedRatio);
expect(measurement.totalWeightedActualRatio).toBe(semanticReference.totalWeightedActualRatio);
}

const summary = summarizeCounterbalancedSamples(measurements);
expect(summary.baselineMedianDurationMs).toBeGreaterThan(0);
expect(summary.candidateMedianDurationMs).toBeGreaterThan(0);
// Wall-clock performance is runner-dependent; keep the 15% target as reported
// evidence while failing only on a measured median regression.
expect(
summary.improvementPercent,
`expected no counterbalanced median computeTaskMetrics regression for exact head ${candidateSha} over ${baseSha}, got ${summary.improvementPercent.toFixed(2)}%`,
).toBeGreaterThanOrEqual(0);

const completionBaseSha = resolveCurrentBaseSha();
expect(completionBaseSha).toBe(baseSha);

const sharedSemanticEvidence = {
digest: semanticReference.digest,
totalDays: semanticReference.totalDays,
byTaskSize: semanticReference.byTaskSize,
totalWeightedPlannedRatio: semanticReference.totalWeightedPlannedRatio,
totalWeightedActualRatio: semanticReference.totalWeightedActualRatio,
};
console.log(`SCOPEWEAVE_METRICS_BENCHMARK ${JSON.stringify({
taskCount: TASK_COUNT,
sampleCountPerMeasurement: SAMPLE_COUNT,
warmupCountPerMeasurement: WARMUP_COUNT,
measurementOrder,
protectedBaseSha: baseSha,
exactContributorHeadSha: candidateSha,
protectedBaselineAvailable: true,
targetImprovementPercent: TARGET_IMPROVEMENT_PERCENT,
optimizationDeltaPercent: summary.improvementPercent,
baseline: {
samples: summary.baselineSamples,
medianDurationMs: summary.baselineMedianDurationMs,
...sharedSemanticEvidence,
},
optimized: {
samples: summary.candidateSamples,
medianDurationMs: summary.candidateMedianDurationMs,
...sharedSemanticEvidence,
},
})}`);
});
Loading
Loading