Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
117 changes: 117 additions & 0 deletions bench/buildDisplayLines.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { Difference } from "@aws-sdk/client-codecommit";
import { bench, describe } from "vitest";
import type { CommentThread } from "../src/services/codecommit.js";
import { buildDisplayLines, type DisplayLine } from "../src/utils/displayLines.js";

function makeLines(count: number, prefix: string): string {
return Array.from({ length: count }, (_, i) => `${prefix} line ${i} with some content`).join(
"\n",
);
}

function makeFixture(fileCount: number, linesPerFile: number) {
const differences: Difference[] = [];
const diffTexts = new Map<string, { before: string; after: string }>();
const diffTextStatus = new Map<string, "loading" | "loaded" | "error">();

for (let f = 0; f < fileCount; f++) {
const beforeBlobId = `before-${f}`;
const afterBlobId = `after-${f}`;
differences.push({
beforeBlob: { blobId: beforeBlobId, path: `src/file-${f}.ts` },
afterBlob: { blobId: afterBlobId, path: `src/file-${f}.ts` },
changeType: "M",
});
const key = `${beforeBlobId}:${afterBlobId}`;
const before = makeLines(linesPerFile, `f${f}-old`);
const after = makeLines(linesPerFile, `f${f}-new`);
diffTexts.set(key, { before, after });
diffTextStatus.set(key, "loaded");
}

return { differences, diffTexts, diffTextStatus };
}

function makeInlineThreads(fileCount: number, threadsPerFile: number): CommentThread[] {
const threads: CommentThread[] = [];
for (let f = 0; f < fileCount; f++) {
for (let t = 0; t < threadsPerFile; t++) {
threads.push({
location: {
filePath: `src/file-${f}.ts`,
filePosition: t * 10 + 1,
relativeFileVersion: "AFTER",
},
comments: [
{
commentId: `c-${f}-${t}`,
content: `comment ${t} on file ${f}`,
authorArn: "arn:aws:iam::123456789012:user/reviewer",
},
],
});
}
}
return threads;
}

const small = makeFixture(5, 100);
const large = makeFixture(20, 400);
const withComments = makeFixture(10, 200);
const inlineThreads = makeInlineThreads(10, 5);

describe("buildDisplayLines", () => {
bench("5 files x 100 lines, no comments, cold cache", () => {
buildDisplayLines(
small.differences,
small.diffTexts,
small.diffTextStatus,
new Map(),
[],
new Map(),
new Map(),
new Map(),
);
});

bench("20 files x 400 lines, no comments, cold cache", () => {
buildDisplayLines(
large.differences,
large.diffTexts,
large.diffTextStatus,
new Map(),
[],
new Map(),
new Map(),
new Map(),
);
});

const warmCache = new Map<string, DisplayLine[]>();
bench("20 files x 400 lines, no comments, warm cache", () => {
buildDisplayLines(
large.differences,
large.diffTexts,
large.diffTextStatus,
new Map(),
[],
new Map(),
new Map(),
warmCache,
);
});

const warmCacheComments = new Map<string, DisplayLine[]>();
bench("10 files x 200 lines, 50 inline threads, warm cache", () => {
buildDisplayLines(
withComments.differences,
withComments.diffTexts,
withComments.diffTextStatus,
new Map(),
inlineThreads,
new Map(),
new Map(),
warmCacheComments,
);
});
});
55 changes: 55 additions & 0 deletions bench/computeSimpleDiff.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { bench, describe } from "vitest";
import { computeSimpleDiff } from "../src/utils/formatDiff.js";

function makeLines(count: number, prefix: string): string[] {
return Array.from({ length: count }, (_, i) => `${prefix} line ${i} with some content`);
}

// Identical files: pure context path
const identical2k = makeLines(2000, "same");

// Fully rewritten file: every before-line is deleted, every after-line is added.
// Worst case for unbounded lookahead scans.
const rewrittenBefore2k = makeLines(2000, "old");
const rewrittenAfter2k = makeLines(2000, "new");

// Realistic edit: large file with several small change blocks
const editedBefore = makeLines(3000, "ctx");
const editedAfter = (() => {
const lines = [...editedBefore];
for (let block = 0; block < 10; block++) {
const at = 250 + block * 250;
lines.splice(at, 5, ...makeLines(8, `edit${block}`));
}
return lines;
})();

// Reordered blocks: triggers the 5-line lookahead match path
const reorderedBefore = makeLines(1000, "blk");
const reorderedAfter = (() => {
const lines = [...reorderedBefore];
for (let i = 0; i < lines.length - 3; i += 7) {
const tmp = lines[i]!;
lines[i] = lines[i + 3]!;
lines[i + 3] = tmp;
}
return lines;
})();

describe("computeSimpleDiff", () => {
bench("identical 2000-line files", () => {
computeSimpleDiff(identical2k, identical2k);
});

bench("fully rewritten 2000-line file", () => {
computeSimpleDiff(rewrittenBefore2k, rewrittenAfter2k);
});

bench("3000-line file with 10 edit blocks", () => {
computeSimpleDiff(editedBefore, editedAfter);
});

bench("1000-line file with reordered blocks", () => {
computeSimpleDiff(reorderedBefore, reorderedAfter);
});
});
147 changes: 147 additions & 0 deletions bench/detailRender.bench.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { Difference } from "@aws-sdk/client-codecommit";
import { render } from "ink-testing-library";
import React from "react";
import { bench, describe } from "vitest";
import { PullRequestDetail } from "../src/components/PullRequestDetail.js";

const noop = () => {};

function makeLines(count: number, prefix: string): string {
return Array.from({ length: count }, (_, i) => `${prefix} line ${i} with some content`).join(
"\n",
);
}

function makeFixture(fileCount: number, linesPerFile: number) {
const differences: Difference[] = [];
const diffTexts = new Map<string, { before: string; after: string }>();
const diffTextStatus = new Map<string, "loading" | "loaded" | "error">();

for (let f = 0; f < fileCount; f++) {
const beforeBlobId = `before-${f}`;
const afterBlobId = `after-${f}`;
differences.push({
beforeBlob: { blobId: beforeBlobId, path: `src/file-${f}.ts` },
afterBlob: { blobId: afterBlobId, path: `src/file-${f}.ts` },
changeType: "M",
});
const key = `${beforeBlobId}:${afterBlobId}`;
diffTexts.set(key, {
before: makeLines(linesPerFile, `f${f}-old`),
after: makeLines(linesPerFile, `f${f}-new`),
});
diffTextStatus.set(key, "loaded");
}

return { differences, diffTexts, diffTextStatus };
}

const pullRequest = {
pullRequestId: "42",
title: "perf: benchmark fixture",
authorArn: "arn:aws:iam::123456789012:user/watany",
pullRequestStatus: "OPEN",
creationDate: new Date("2026-02-13T10:00:00Z"),
pullRequestTargets: [
{
destinationReference: "refs/heads/main",
sourceReference: "refs/heads/feature/perf",
},
],
};

const asyncActionProps = {
onPost: noop,
isProcessing: false,
error: null,
onClearError: noop,
};

const fixture = makeFixture(10, 200);

function renderDetail() {
return render(
<PullRequestDetail
pullRequest={pullRequest as never}
differences={fixture.differences}
commentThreads={[]}
diffTexts={fixture.diffTexts}
diffTextStatus={fixture.diffTextStatus}
onBack={noop}
onHelp={noop}
onShowActivity={noop}
comment={asyncActionProps}
inlineComment={asyncActionProps}
reply={asyncActionProps}
approval={{
approvals: [],
evaluation: null,
onApprove: noop,
onRevoke: noop,
isProcessing: false,
error: null,
onClearError: noop,
}}
merge={{
onMerge: noop,
onCheckConflicts: () =>
Promise.resolve({ mergeable: true, conflictCount: 0, conflictFiles: [] }),
isProcessing: false,
error: null,
onClearError: noop,
}}
close={{ onClose: noop, isProcessing: false, error: null, onClearError: noop }}
commitView={{
commits: [],
differences: [],
diffTexts: new Map(),
isLoading: false,
onLoad: noop,
commitsAvailable: false,
}}
editComment={{ onUpdate: noop, isProcessing: false, error: null, onClearError: noop }}
deleteComment={{ onDelete: noop, isProcessing: false, error: null, onClearError: noop }}
reaction={{
byComment: new Map(),
onReact: noop,
isProcessing: false,
error: null,
onClearError: noop,
}}
/>,
);
}

const flush = () => new Promise<void>((r) => setTimeout(r, 0));

describe("PullRequestDetail rendering (10 files x 200 lines)", () => {
bench("initial mount + unmount", async () => {
const instance = renderDetail();
await flush();
instance.unmount();
});

// Alternate down/up so the cursor keeps moving (a clamped cursor skips re-render)
const navInstance = renderDetail();
let navDown = true;
bench("j/k keystroke (cursor move + re-render)", async () => {
navInstance.stdin.write(navDown ? "j" : "k");
navDown = !navDown;
await flush();
});

// Unhandled key: measures stdin parsing + flush overhead without a re-render
const noopInstance = renderDetail();
bench("no-op keystroke (no re-render)", async () => {
noopInstance.stdin.write("z");
await flush();
});

const pageInstance = renderDetail();
let pageDown = true;
bench("Ctrl+d/u half-page scroll + re-render", async () => {
pageInstance.stdin.write(pageDown ? "\x04" : "\x15");
pageDown = !pageDown;
await flush();
});
});
4 changes: 4 additions & 0 deletions build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const [cliResult, libResult] = await Promise.all([
minify: true,
naming: "cli.mjs",
plugins: [stubDevtools],
// Inline NODE_ENV so react-reconciler/react bundle their production
// builds instead of deciding at runtime (users rarely set NODE_ENV)
define: { "process.env.NODE_ENV": '"production"' },
}),
Bun.build({
entrypoints: ["./src/index.ts"],
Expand All @@ -29,6 +32,7 @@ const [cliResult, libResult] = await Promise.all([
target: "node",
minify: true,
plugins: [stubDevtools],
define: { "process.env.NODE_ENV": '"production"' },
}),
]);

Expand Down
Loading
Loading