|
1 | 1 | 'use server'; |
2 | 2 |
|
| 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"; |
3 | 11 | 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"; |
5 | 14 | import { zoektFetch } from "./zoektClient"; |
6 | | -import { ErrorCode } from "../../lib/errorCodes"; |
7 | | -import { StatusCodes } from "http-status-codes"; |
8 | 15 | 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"); |
14 | 19 |
|
15 | 20 | // List of supported query prefixes in zoekt. |
16 | 21 | // @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 |
126 | 131 | return encodeURI(url + optionalQueryParams); |
127 | 132 | } |
128 | 133 |
|
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(() => |
130 | 135 | withOptionalAuthV2(async ({ org, prisma }) => { |
131 | 136 | const transformedQuery = await transformZoektQuery(query, org.id, prisma); |
132 | 137 | if (isServiceError(transformedQuery)) { |
@@ -200,20 +205,22 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ |
200 | 205 | "X-Tenant-ID": org.id.toString() |
201 | 206 | }; |
202 | 207 |
|
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 | + ); |
209 | 218 |
|
210 | 219 | if (!searchResponse.ok) { |
211 | 220 | return invalidZoektResponse(searchResponse); |
212 | 221 | } |
213 | 222 |
|
214 | | - const searchBody = await searchResponse.json(); |
215 | | - |
216 | | - const parser = zoektSearchResponseSchema.transform(async ({ Result }) => { |
| 223 | + const transformZoektSearchResponse = async ({ Result }: z.infer<typeof zoektSearchResponseSchema>) => { |
217 | 224 | // @note (2025-05-12): in zoekt, repositories are identified by the `RepositoryID` field |
218 | 225 | // which corresponds to the `id` in the Repo table. In order to efficiently fetch repository |
219 | 226 | // 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 |
379 | 386 | flushReason: Result.FlushReason, |
380 | 387 | } |
381 | 388 | } satisfies SearchResponse; |
382 | | - }); |
| 389 | + } |
| 390 | + |
| 391 | + const { data: rawZoektResponse, durationMs: parseJsonDurationMs } = await measure( |
| 392 | + () => searchResponse.json(), |
| 393 | + "parse_json", |
| 394 | + false |
| 395 | + ); |
383 | 396 |
|
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; |
385 | 437 | })); |
0 commit comments