Skip to content

Commit 029cad8

Browse files
10x performance improvement by optimizing zod parsing
1 parent 06c84f0 commit 029cad8

5 files changed

Lines changed: 85 additions & 21 deletions

File tree

packages/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"@codemirror/search": "^6.5.6",
5252
"@codemirror/state": "^6.4.1",
5353
"@codemirror/view": "^6.33.0",
54+
"@duplojs/zod-accelerator": "^2.6.2",
5455
"@floating-ui/react": "^0.27.2",
5556
"@hookform/resolvers": "^3.9.0",
5657
"@iconify/react": "^5.1.0",

packages/web/src/app/[domain]/search/components/searchResultsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { FilterPanel } from "./filterPanel";
3535
import { useFilteredMatches } from "./filterPanel/useFilterMatches";
3636
import { SearchResultsPanel } from "./searchResultsPanel";
3737

38-
const DEFAULT_MAX_MATCH_COUNT = 5000;
38+
const DEFAULT_MAX_MATCH_COUNT = 100_000;
3939

4040
interface SearchResultsPageProps {
4141
searchQuery: string;

packages/web/src/features/search/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export const searchResponseSchema = z.object({
141141
repositoryInfo: z.array(repositoryInfoSchema),
142142
isBranchFilteringEnabled: z.boolean(),
143143
isSearchExhaustive: z.boolean(),
144+
__debug_timings: z.record(z.string(), z.number()).optional(),
144145
});
145146

146147
export const fileSourceRequestSchema = z.object({

packages/web/src/features/search/searchApi.ts

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
'use server';
22

3+
import { sew } from "@/actions";
4+
import { withOptionalAuthV2 } from "@/withAuthV2";
5+
import { ZodAccelerator } from "@duplojs/zod-accelerator";
6+
import { PrismaClient, Repo } from "@sourcebot/db";
7+
import { base64Decode, createLogger } from "@sourcebot/shared";
8+
import { StatusCodes } from "http-status-codes";
9+
import z from "zod";
10+
import { ErrorCode } from "../../lib/errorCodes";
311
import { invalidZoektResponse, ServiceError } from "../../lib/serviceError";
4-
import { isServiceError } from "../../lib/utils";
12+
import { isServiceError, measure } from "../../lib/utils";
13+
import { SearchRequest, SearchResponse, SourceRange } from "./types";
514
import { zoektFetch } from "./zoektClient";
6-
import { ErrorCode } from "../../lib/errorCodes";
7-
import { StatusCodes } from "http-status-codes";
815
import { zoektSearchResponseSchema } from "./zoektSchema";
9-
import { SearchRequest, SearchResponse, SourceRange } from "./types";
10-
import { PrismaClient, Repo } from "@sourcebot/db";
11-
import { sew } from "@/actions";
12-
import { base64Decode } from "@sourcebot/shared";
13-
import { withOptionalAuthV2 } from "@/withAuthV2";
16+
17+
const acceleratedZoektSearchResponseSchema = ZodAccelerator.build(zoektSearchResponseSchema);
18+
const logger = createLogger("searchApi");
1419

1520
// List of supported query prefixes in zoekt.
1621
// @see : https://github.com/sourcebot-dev/zoekt/blob/main/query/parse.go#L417
@@ -126,7 +131,7 @@ const getFileWebUrl = (template: string, branch: string, fileName: string): stri
126131
return encodeURI(url + optionalQueryParams);
127132
}
128133

129-
export const search = async ({ query, matches, contextLines, whole }: SearchRequest) => sew(() =>
134+
export const search = async ({ query, matches, contextLines, whole }: SearchRequest): Promise<SearchResponse | ServiceError> => sew(() =>
130135
withOptionalAuthV2(async ({ org, prisma }) => {
131136
const transformedQuery = await transformZoektQuery(query, org.id, prisma);
132137
if (isServiceError(transformedQuery)) {
@@ -200,20 +205,22 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
200205
"X-Tenant-ID": org.id.toString()
201206
};
202207

203-
const searchResponse = await zoektFetch({
204-
path: "/api/search",
205-
body,
206-
header,
207-
method: "POST",
208-
});
208+
const { data: searchResponse, durationMs: fetchDurationMs } = await measure(
209+
() => zoektFetch({
210+
path: "/api/search",
211+
body,
212+
header,
213+
method: "POST",
214+
}),
215+
"zoekt_fetch",
216+
false
217+
);
209218

210219
if (!searchResponse.ok) {
211220
return invalidZoektResponse(searchResponse);
212221
}
213222

214-
const searchBody = await searchResponse.json();
215-
216-
const parser = zoektSearchResponseSchema.transform(async ({ Result }) => {
223+
const transformZoektSearchResponse = async ({ Result }: z.infer<typeof zoektSearchResponseSchema>) => {
217224
// @note (2025-05-12): in zoekt, repositories are identified by the `RepositoryID` field
218225
// which corresponds to the `id` in the Repo table. In order to efficiently fetch repository
219226
// metadata when transforming (potentially thousands) of file matches, we aggregate a unique
@@ -379,7 +386,52 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
379386
flushReason: Result.FlushReason,
380387
}
381388
} satisfies SearchResponse;
382-
});
389+
}
390+
391+
const { data: rawZoektResponse, durationMs: parseJsonDurationMs } = await measure(
392+
() => searchResponse.json(),
393+
"parse_json",
394+
false
395+
);
383396

384-
return parser.parseAsync(searchBody);
397+
const { data: zoektResponse, durationMs: parseZoektResponseDurationMs } = await measure(
398+
() => acceleratedZoektSearchResponseSchema.parseAsync(rawZoektResponse),
399+
"parse_zoekt_response",
400+
false
401+
);
402+
403+
const { data: response, durationMs: transformZoektResponseDurationMs } = await measure(
404+
() => transformZoektSearchResponse(zoektResponse),
405+
"transform_zoekt_response",
406+
false
407+
);
408+
409+
const totalDurationMs = fetchDurationMs + parseJsonDurationMs + parseZoektResponseDurationMs + transformZoektResponseDurationMs;
410+
411+
// Debug log: timing breakdown
412+
const timings = [
413+
{ name: "zoekt_fetch", duration: fetchDurationMs },
414+
{ name: "parse_json", duration: parseJsonDurationMs },
415+
{ name: "parse_zoekt_response", duration: parseZoektResponseDurationMs },
416+
{ name: "transform_zoekt_response", duration: transformZoektResponseDurationMs },
417+
];
418+
419+
logger.debug(`Search timing breakdown (query: "${query}"):`);
420+
timings.forEach(({ name, duration }) => {
421+
const percentage = ((duration / totalDurationMs) * 100).toFixed(1);
422+
const durationStr = duration.toFixed(2).padStart(8);
423+
const percentageStr = percentage.padStart(5);
424+
logger.debug(` ${name.padEnd(25)} ${durationStr}ms (${percentageStr}%)`);
425+
});
426+
logger.debug(` ${"TOTAL".padEnd(25)} ${totalDurationMs.toFixed(2).padStart(8)}ms (100.0%)`);
427+
428+
return {
429+
...response,
430+
__debug_timings: {
431+
zoekt_fetch: fetchDurationMs,
432+
parse_json: parseJsonDurationMs,
433+
parse_zoekt_response: parseZoektResponseDurationMs,
434+
transform_zoekt_response: transformZoektResponseDurationMs,
435+
}
436+
} satisfies SearchResponse;
385437
}));

yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,15 @@ __metadata:
16301630
languageName: node
16311631
linkType: hard
16321632

1633+
"@duplojs/zod-accelerator@npm:^2.6.2":
1634+
version: 2.6.2
1635+
resolution: "@duplojs/zod-accelerator@npm:2.6.2"
1636+
peerDependencies:
1637+
zod: ">=3.0.0 <4.0.0"
1638+
checksum: 10c0/9b8a1dd6cc7c79df16d6e82b34a3f9f76242f513bb272265adbf159f565785f7c4be14d9cf492053fbacd806d0865b20ce1e9adff28f92f7ff9730121688e2b5
1639+
languageName: node
1640+
linkType: hard
1641+
16331642
"@emnapi/core@npm:^1.3.1":
16341643
version: 1.3.1
16351644
resolution: "@emnapi/core@npm:1.3.1"
@@ -8058,6 +8067,7 @@ __metadata:
80588067
"@codemirror/search": "npm:^6.5.6"
80598068
"@codemirror/state": "npm:^6.4.1"
80608069
"@codemirror/view": "npm:^6.33.0"
8070+
"@duplojs/zod-accelerator": "npm:^2.6.2"
80618071
"@eslint/eslintrc": "npm:^3"
80628072
"@floating-ui/react": "npm:^0.27.2"
80638073
"@hookform/resolvers": "npm:^3.9.0"

0 commit comments

Comments
 (0)