feat: add code queue services and baidu netdisk
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import { chromium, type BrowserContext, type Request } from "playwright";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { readConfig } from "./config";
|
||||
|
||||
interface CodeQueuePerfOptions {
|
||||
url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
timeoutMs: number;
|
||||
targetMs: number;
|
||||
headless: boolean;
|
||||
json: boolean;
|
||||
}
|
||||
|
||||
interface ApiTiming {
|
||||
url: string;
|
||||
method: string;
|
||||
status: number;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
function argValue(args: string[], name: string): string | null {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) return null;
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function numberArg(args: string[], name: string, fallback: number): number {
|
||||
const raw = argValue(args, name);
|
||||
if (raw === null) return fallback;
|
||||
const value = Number(raw);
|
||||
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number`);
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value: string): string {
|
||||
return value.replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
function readOptions(): CodeQueuePerfOptions {
|
||||
const config = readConfig();
|
||||
const args = process.argv.slice(2);
|
||||
return {
|
||||
url: normalizeBaseUrl(argValue(args, "--url") ?? `http://${config.network.publicHost}:${config.network.frontend.port}`),
|
||||
username: argValue(args, "--username") ?? config.auth.username,
|
||||
password: argValue(args, "--password") ?? config.auth.password,
|
||||
timeoutMs: numberArg(args, "--timeout-ms", 90_000),
|
||||
targetMs: numberArg(args, "--target-ms", 1_000),
|
||||
headless: !args.includes("--headed"),
|
||||
json: args.includes("--json"),
|
||||
};
|
||||
}
|
||||
|
||||
async function authenticateSession(context: BrowserContext, options: CodeQueuePerfOptions): Promise<void> {
|
||||
const response = await fetch(`${options.url}/login`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ username: options.username, password: options.password }),
|
||||
signal: AbortSignal.timeout(options.timeoutMs),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`login failed: HTTP ${response.status} ${await response.text()}`);
|
||||
}
|
||||
const setCookie = response.headers.get("set-cookie");
|
||||
const cookiePair = setCookie?.split(";", 1)[0] ?? "";
|
||||
const separator = cookiePair.indexOf("=");
|
||||
if (separator <= 0) throw new Error("login response did not set a session cookie");
|
||||
await context.addCookies([{
|
||||
name: cookiePair.slice(0, separator),
|
||||
value: cookiePair.slice(separator + 1),
|
||||
url: options.url,
|
||||
}]);
|
||||
}
|
||||
|
||||
function requestPath(request: Request): string {
|
||||
try {
|
||||
const url = new URL(request.url());
|
||||
return `${url.pathname}${url.search}`;
|
||||
} catch {
|
||||
return request.url();
|
||||
}
|
||||
}
|
||||
|
||||
function parseNumberAttr(value: string | null): number | null {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
async function runCodeQueuePerf(options: CodeQueuePerfOptions): Promise<Record<string, unknown>> {
|
||||
const browsersPath = process.env.PLAYWRIGHT_BROWSERS_PATH || ".state/playwright-browsers";
|
||||
const fullChromePath = resolve(browsersPath, "chromium-1217/chrome-linux64/chrome");
|
||||
const launchArgs = [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
];
|
||||
const launchOptions: Parameters<typeof chromium.launch>[0] = { headless: options.headless, args: launchArgs };
|
||||
if (existsSync(fullChromePath)) {
|
||||
launchOptions.executablePath = fullChromePath;
|
||||
}
|
||||
const browser = await chromium.launch(launchOptions);
|
||||
const apiStartedAt = new Map<Request, number>();
|
||||
const apiTimings: ApiTiming[] = [];
|
||||
const consoleErrors: string[] = [];
|
||||
const targetUrl = `${options.url}/app/code-queue/`;
|
||||
const measuredAt = new Date().toISOString();
|
||||
|
||||
try {
|
||||
const context = await browser.newContext();
|
||||
await authenticateSession(context, options);
|
||||
const page = await context.newPage();
|
||||
await page.route("**/favicon.ico", (route) => route.abort());
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") consoleErrors.push(message.text().slice(0, 500));
|
||||
});
|
||||
page.on("request", (request) => {
|
||||
const path = requestPath(request);
|
||||
if (path.startsWith("/api/") || path === "/app.js") apiStartedAt.set(request, performance.now());
|
||||
});
|
||||
page.on("response", (response) => {
|
||||
const request = response.request();
|
||||
const started = apiStartedAt.get(request);
|
||||
if (started === undefined) return;
|
||||
apiStartedAt.delete(request);
|
||||
apiTimings.push({
|
||||
url: requestPath(request),
|
||||
method: request.method(),
|
||||
status: response.status(),
|
||||
durationMs: Math.round((performance.now() - started) * 10) / 10,
|
||||
});
|
||||
});
|
||||
|
||||
const startedAt = performance.now();
|
||||
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: options.timeoutMs });
|
||||
const domContentLoadedMs = Math.round((performance.now() - startedAt) * 10) / 10;
|
||||
await page.waitForSelector('[data-testid="code-queue-page"][data-load-state="complete"]', { timeout: options.timeoutMs });
|
||||
const completeAt = performance.now();
|
||||
const dom = await page.evaluate(() => {
|
||||
const pageElement = document.querySelector('[data-testid="code-queue-page"]') as HTMLElement | null;
|
||||
const output = document.querySelector('[data-testid="codex-output"]') as HTMLElement | null;
|
||||
const tasks = document.querySelectorAll('[data-testid^="codex-task-codex_"]');
|
||||
return {
|
||||
loadState: pageElement?.dataset.loadState ?? "",
|
||||
componentLoadMs: pageElement?.dataset.loadTotalMs ?? "",
|
||||
queueMs: pageElement?.dataset.loadQueueMs ?? "",
|
||||
detailMs: pageElement?.dataset.loadDetailMs ?? "",
|
||||
transcriptRows: pageElement?.dataset.loadTranscriptRows ?? "",
|
||||
taskId: pageElement?.dataset.loadTaskId ?? "",
|
||||
partial: pageElement?.dataset.loadPartial === "true",
|
||||
visibleTaskCount: tasks.length,
|
||||
outputChars: output?.textContent?.length ?? 0,
|
||||
};
|
||||
});
|
||||
let networkIdleReached = true;
|
||||
try {
|
||||
await page.waitForLoadState("networkidle", { timeout: 5_000 });
|
||||
} catch {
|
||||
networkIdleReached = false;
|
||||
}
|
||||
const networkIdleMs = Math.round((performance.now() - startedAt) * 10) / 10;
|
||||
const playwrightObservedMs = Math.round((completeAt - startedAt) * 10) / 10;
|
||||
const browserLoadMs = parseNumberAttr(dom.componentLoadMs);
|
||||
const totalLoadMs = browserLoadMs ?? playwrightObservedMs;
|
||||
|
||||
const slowestApi = apiTimings.slice().sort((left, right) => right.durationMs - left.durationMs).slice(0, 8);
|
||||
const result = {
|
||||
ok: true,
|
||||
measuredAt,
|
||||
url: targetUrl,
|
||||
targetMs: options.targetMs,
|
||||
withinTarget: totalLoadMs <= options.targetMs,
|
||||
status: totalLoadMs <= options.targetMs ? "passed" : "slow",
|
||||
wallMs: totalLoadMs,
|
||||
totalLoadMs,
|
||||
domContentLoadedMs,
|
||||
appCompleteMs: playwrightObservedMs,
|
||||
playwrightObservedMs,
|
||||
networkIdleMs,
|
||||
networkIdleReached,
|
||||
componentLoadMs: browserLoadMs,
|
||||
queueMs: parseNumberAttr(dom.queueMs),
|
||||
detailMs: parseNumberAttr(dom.detailMs),
|
||||
transcriptRows: parseNumberAttr(dom.transcriptRows),
|
||||
partial: dom.partial,
|
||||
selectedTaskId: dom.taskId || null,
|
||||
visibleTaskCount: dom.visibleTaskCount,
|
||||
outputChars: dom.outputChars,
|
||||
apiRequestCount: apiTimings.length,
|
||||
slowestApi,
|
||||
consoleErrors,
|
||||
};
|
||||
await context.close();
|
||||
return result;
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
measuredAt,
|
||||
url: targetUrl,
|
||||
targetMs: options.targetMs,
|
||||
status: "failed",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
apiRequestCount: apiTimings.length,
|
||||
slowestApi: apiTimings.slice().sort((left, right) => right.durationMs - left.durationMs).slice(0, 8),
|
||||
consoleErrors,
|
||||
};
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
const options = readOptions();
|
||||
const result = await runCodeQueuePerf(options);
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(result));
|
||||
} else {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
if (result.ok !== true) process.exitCode = 1;
|
||||
Reference in New Issue
Block a user