Skip to content

Commit b7007cb

Browse files
authored
Merge pull request #98 from watany-dev/claude/perf-optimization-benchmarks-kfci5o
2 parents 226d7c8 + 4fe0bdd commit b7007cb

14 files changed

Lines changed: 535 additions & 98 deletions

bench/buildDisplayLines.bench.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type { Difference } from "@aws-sdk/client-codecommit";
2+
import { bench, describe } from "vitest";
3+
import type { CommentThread } from "../src/services/codecommit.js";
4+
import { buildDisplayLines, type DisplayLine } from "../src/utils/displayLines.js";
5+
6+
function makeLines(count: number, prefix: string): string {
7+
return Array.from({ length: count }, (_, i) => `${prefix} line ${i} with some content`).join(
8+
"\n",
9+
);
10+
}
11+
12+
function makeFixture(fileCount: number, linesPerFile: number) {
13+
const differences: Difference[] = [];
14+
const diffTexts = new Map<string, { before: string; after: string }>();
15+
const diffTextStatus = new Map<string, "loading" | "loaded" | "error">();
16+
17+
for (let f = 0; f < fileCount; f++) {
18+
const beforeBlobId = `before-${f}`;
19+
const afterBlobId = `after-${f}`;
20+
differences.push({
21+
beforeBlob: { blobId: beforeBlobId, path: `src/file-${f}.ts` },
22+
afterBlob: { blobId: afterBlobId, path: `src/file-${f}.ts` },
23+
changeType: "M",
24+
});
25+
const key = `${beforeBlobId}:${afterBlobId}`;
26+
const before = makeLines(linesPerFile, `f${f}-old`);
27+
const after = makeLines(linesPerFile, `f${f}-new`);
28+
diffTexts.set(key, { before, after });
29+
diffTextStatus.set(key, "loaded");
30+
}
31+
32+
return { differences, diffTexts, diffTextStatus };
33+
}
34+
35+
function makeInlineThreads(fileCount: number, threadsPerFile: number): CommentThread[] {
36+
const threads: CommentThread[] = [];
37+
for (let f = 0; f < fileCount; f++) {
38+
for (let t = 0; t < threadsPerFile; t++) {
39+
threads.push({
40+
location: {
41+
filePath: `src/file-${f}.ts`,
42+
filePosition: t * 10 + 1,
43+
relativeFileVersion: "AFTER",
44+
},
45+
comments: [
46+
{
47+
commentId: `c-${f}-${t}`,
48+
content: `comment ${t} on file ${f}`,
49+
authorArn: "arn:aws:iam::123456789012:user/reviewer",
50+
},
51+
],
52+
});
53+
}
54+
}
55+
return threads;
56+
}
57+
58+
const small = makeFixture(5, 100);
59+
const large = makeFixture(20, 400);
60+
const withComments = makeFixture(10, 200);
61+
const inlineThreads = makeInlineThreads(10, 5);
62+
63+
describe("buildDisplayLines", () => {
64+
bench("5 files x 100 lines, no comments, cold cache", () => {
65+
buildDisplayLines(
66+
small.differences,
67+
small.diffTexts,
68+
small.diffTextStatus,
69+
new Map(),
70+
[],
71+
new Map(),
72+
new Map(),
73+
new Map(),
74+
);
75+
});
76+
77+
bench("20 files x 400 lines, no comments, cold cache", () => {
78+
buildDisplayLines(
79+
large.differences,
80+
large.diffTexts,
81+
large.diffTextStatus,
82+
new Map(),
83+
[],
84+
new Map(),
85+
new Map(),
86+
new Map(),
87+
);
88+
});
89+
90+
const warmCache = new Map<string, DisplayLine[]>();
91+
bench("20 files x 400 lines, no comments, warm cache", () => {
92+
buildDisplayLines(
93+
large.differences,
94+
large.diffTexts,
95+
large.diffTextStatus,
96+
new Map(),
97+
[],
98+
new Map(),
99+
new Map(),
100+
warmCache,
101+
);
102+
});
103+
104+
const warmCacheComments = new Map<string, DisplayLine[]>();
105+
bench("10 files x 200 lines, 50 inline threads, warm cache", () => {
106+
buildDisplayLines(
107+
withComments.differences,
108+
withComments.diffTexts,
109+
withComments.diffTextStatus,
110+
new Map(),
111+
inlineThreads,
112+
new Map(),
113+
new Map(),
114+
warmCacheComments,
115+
);
116+
});
117+
});

bench/computeSimpleDiff.bench.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { bench, describe } from "vitest";
2+
import { computeSimpleDiff } from "../src/utils/formatDiff.js";
3+
4+
function makeLines(count: number, prefix: string): string[] {
5+
return Array.from({ length: count }, (_, i) => `${prefix} line ${i} with some content`);
6+
}
7+
8+
// Identical files: pure context path
9+
const identical2k = makeLines(2000, "same");
10+
11+
// Fully rewritten file: every before-line is deleted, every after-line is added.
12+
// Worst case for unbounded lookahead scans.
13+
const rewrittenBefore2k = makeLines(2000, "old");
14+
const rewrittenAfter2k = makeLines(2000, "new");
15+
16+
// Realistic edit: large file with several small change blocks
17+
const editedBefore = makeLines(3000, "ctx");
18+
const editedAfter = (() => {
19+
const lines = [...editedBefore];
20+
for (let block = 0; block < 10; block++) {
21+
const at = 250 + block * 250;
22+
lines.splice(at, 5, ...makeLines(8, `edit${block}`));
23+
}
24+
return lines;
25+
})();
26+
27+
// Reordered blocks: triggers the 5-line lookahead match path
28+
const reorderedBefore = makeLines(1000, "blk");
29+
const reorderedAfter = (() => {
30+
const lines = [...reorderedBefore];
31+
for (let i = 0; i < lines.length - 3; i += 7) {
32+
const tmp = lines[i]!;
33+
lines[i] = lines[i + 3]!;
34+
lines[i + 3] = tmp;
35+
}
36+
return lines;
37+
})();
38+
39+
describe("computeSimpleDiff", () => {
40+
bench("identical 2000-line files", () => {
41+
computeSimpleDiff(identical2k, identical2k);
42+
});
43+
44+
bench("fully rewritten 2000-line file", () => {
45+
computeSimpleDiff(rewrittenBefore2k, rewrittenAfter2k);
46+
});
47+
48+
bench("3000-line file with 10 edit blocks", () => {
49+
computeSimpleDiff(editedBefore, editedAfter);
50+
});
51+
52+
bench("1000-line file with reordered blocks", () => {
53+
computeSimpleDiff(reorderedBefore, reorderedAfter);
54+
});
55+
});

bench/detailRender.bench.tsx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import type { Difference } from "@aws-sdk/client-codecommit";
2+
import { render } from "ink-testing-library";
3+
import React from "react";
4+
import { bench, describe } from "vitest";
5+
import { PullRequestDetail } from "../src/components/PullRequestDetail.js";
6+
7+
const noop = () => {};
8+
9+
function makeLines(count: number, prefix: string): string {
10+
return Array.from({ length: count }, (_, i) => `${prefix} line ${i} with some content`).join(
11+
"\n",
12+
);
13+
}
14+
15+
function makeFixture(fileCount: number, linesPerFile: number) {
16+
const differences: Difference[] = [];
17+
const diffTexts = new Map<string, { before: string; after: string }>();
18+
const diffTextStatus = new Map<string, "loading" | "loaded" | "error">();
19+
20+
for (let f = 0; f < fileCount; f++) {
21+
const beforeBlobId = `before-${f}`;
22+
const afterBlobId = `after-${f}`;
23+
differences.push({
24+
beforeBlob: { blobId: beforeBlobId, path: `src/file-${f}.ts` },
25+
afterBlob: { blobId: afterBlobId, path: `src/file-${f}.ts` },
26+
changeType: "M",
27+
});
28+
const key = `${beforeBlobId}:${afterBlobId}`;
29+
diffTexts.set(key, {
30+
before: makeLines(linesPerFile, `f${f}-old`),
31+
after: makeLines(linesPerFile, `f${f}-new`),
32+
});
33+
diffTextStatus.set(key, "loaded");
34+
}
35+
36+
return { differences, diffTexts, diffTextStatus };
37+
}
38+
39+
const pullRequest = {
40+
pullRequestId: "42",
41+
title: "perf: benchmark fixture",
42+
authorArn: "arn:aws:iam::123456789012:user/watany",
43+
pullRequestStatus: "OPEN",
44+
creationDate: new Date("2026-02-13T10:00:00Z"),
45+
pullRequestTargets: [
46+
{
47+
destinationReference: "refs/heads/main",
48+
sourceReference: "refs/heads/feature/perf",
49+
},
50+
],
51+
};
52+
53+
const asyncActionProps = {
54+
onPost: noop,
55+
isProcessing: false,
56+
error: null,
57+
onClearError: noop,
58+
};
59+
60+
const fixture = makeFixture(10, 200);
61+
62+
function renderDetail() {
63+
return render(
64+
<PullRequestDetail
65+
pullRequest={pullRequest as never}
66+
differences={fixture.differences}
67+
commentThreads={[]}
68+
diffTexts={fixture.diffTexts}
69+
diffTextStatus={fixture.diffTextStatus}
70+
onBack={noop}
71+
onHelp={noop}
72+
onShowActivity={noop}
73+
comment={asyncActionProps}
74+
inlineComment={asyncActionProps}
75+
reply={asyncActionProps}
76+
approval={{
77+
approvals: [],
78+
evaluation: null,
79+
onApprove: noop,
80+
onRevoke: noop,
81+
isProcessing: false,
82+
error: null,
83+
onClearError: noop,
84+
}}
85+
merge={{
86+
onMerge: noop,
87+
onCheckConflicts: () =>
88+
Promise.resolve({ mergeable: true, conflictCount: 0, conflictFiles: [] }),
89+
isProcessing: false,
90+
error: null,
91+
onClearError: noop,
92+
}}
93+
close={{ onClose: noop, isProcessing: false, error: null, onClearError: noop }}
94+
commitView={{
95+
commits: [],
96+
differences: [],
97+
diffTexts: new Map(),
98+
isLoading: false,
99+
onLoad: noop,
100+
commitsAvailable: false,
101+
}}
102+
editComment={{ onUpdate: noop, isProcessing: false, error: null, onClearError: noop }}
103+
deleteComment={{ onDelete: noop, isProcessing: false, error: null, onClearError: noop }}
104+
reaction={{
105+
byComment: new Map(),
106+
onReact: noop,
107+
isProcessing: false,
108+
error: null,
109+
onClearError: noop,
110+
}}
111+
/>,
112+
);
113+
}
114+
115+
const flush = () => new Promise<void>((r) => setTimeout(r, 0));
116+
117+
describe("PullRequestDetail rendering (10 files x 200 lines)", () => {
118+
bench("initial mount + unmount", async () => {
119+
const instance = renderDetail();
120+
await flush();
121+
instance.unmount();
122+
});
123+
124+
// Alternate down/up so the cursor keeps moving (a clamped cursor skips re-render)
125+
const navInstance = renderDetail();
126+
let navDown = true;
127+
bench("j/k keystroke (cursor move + re-render)", async () => {
128+
navInstance.stdin.write(navDown ? "j" : "k");
129+
navDown = !navDown;
130+
await flush();
131+
});
132+
133+
// Unhandled key: measures stdin parsing + flush overhead without a re-render
134+
const noopInstance = renderDetail();
135+
bench("no-op keystroke (no re-render)", async () => {
136+
noopInstance.stdin.write("z");
137+
await flush();
138+
});
139+
140+
const pageInstance = renderDetail();
141+
let pageDown = true;
142+
bench("Ctrl+d/u half-page scroll + re-render", async () => {
143+
pageInstance.stdin.write(pageDown ? "\x04" : "\x15");
144+
pageDown = !pageDown;
145+
await flush();
146+
});
147+
});

build.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const [cliResult, libResult] = await Promise.all([
2121
minify: true,
2222
naming: "cli.mjs",
2323
plugins: [stubDevtools],
24+
// Inline NODE_ENV so react-reconciler/react bundle their production
25+
// builds instead of deciding at runtime (users rarely set NODE_ENV)
26+
define: { "process.env.NODE_ENV": '"production"' },
2427
}),
2528
Bun.build({
2629
entrypoints: ["./src/index.ts"],
@@ -29,6 +32,7 @@ const [cliResult, libResult] = await Promise.all([
2932
target: "node",
3033
minify: true,
3134
plugins: [stubDevtools],
35+
define: { "process.env.NODE_ENV": '"production"' },
3236
}),
3337
]);
3438

0 commit comments

Comments
 (0)