带筛选(级别、关键词、类型)时原查询要对全部引擎运行算标题子查询与 search 拼接, 3 万条运行上超时。改为每个来源两段:第一段只用原始列与一次性算好的业务引用筛选并 order + limit,第二段只对取出的行算标题 / 对象标签;关键词先在业务对象小表里查出 命中 id 集合;查询内关掉 JIT。新增 3 万条运行的耗时测试与改写前口径对照测试。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
106 lines
4.2 KiB
TypeScript
106 lines
4.2 KiB
TypeScript
import { LogPage } from "@umt/contracts";
|
|
import { sql } from "@umt/db";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { createLogStreamPoller, createLogsModule } from "../../src/modules/logs/index.ts";
|
|
import { logQueries } from "../../src/modules/logs/query.ts";
|
|
import { bearer, createHarness, type Harness, pgAvailable } from "../auth-settings/harness.ts";
|
|
import { DEV_KEY, json } from "../helpers.ts";
|
|
import { seedLogVolume, VOLUME_RUNS } from "./volume-seed.ts";
|
|
|
|
// 日志接口在真实数据量上的耗时(约 30,000 条引擎运行 + 定时任务运行 + 审计)。
|
|
// 改写前这几种查询在部署库上超时(> 60s);每个来源先按原始列筛选取前 n 条再算标题后,应在 1 秒内(阈值放宽到 1.5s 防抖动)。
|
|
|
|
const hasPg = await pgAvailable();
|
|
const LIMIT_MS = 1500;
|
|
|
|
describe.skipIf(!hasPg)("日志查询耗时(约 3 万条引擎运行)", () => {
|
|
let h: Harness;
|
|
let admin: ReturnType<typeof bearer>;
|
|
|
|
beforeAll(async () => {
|
|
h = await createHarness({ engine: true });
|
|
const { createApp } = await import("../../src/http/app.ts");
|
|
const { createAuthSettingsModule } = await import("../../src/modules/auth-settings/index.ts");
|
|
h.app = createApp({
|
|
config: h.ctx.config,
|
|
logger: h.ctx.logger,
|
|
bus: h.bus,
|
|
db: h.db,
|
|
role: "all",
|
|
version: "0.0.0-test",
|
|
modules: [
|
|
createAuthSettingsModule({ setupTokenOnStart: false, engineSchema: h.engineSchema }),
|
|
createLogsModule({ engineSchema: h.engineSchema }),
|
|
],
|
|
ctx: h.ctx,
|
|
}) as Harness["app"];
|
|
admin = bearer(h.app, DEV_KEY);
|
|
await seedLogVolume(h.db, h.engineSchema);
|
|
const n = await sql<{
|
|
n: number;
|
|
}>`select count(*)::int as n from ${sql.raw(`"${h.engineSchema}".workflow_runs`)}`.execute(h.db);
|
|
expect(n.rows[0]!.n).toBe(VOLUME_RUNS);
|
|
// 预热连接
|
|
expect((await admin("GET", "/logs?limit=1")).status).toBe(200);
|
|
}, 120_000);
|
|
afterAll(async () => {
|
|
await h?.close();
|
|
});
|
|
|
|
const since24h = () => encodeURIComponent(new Date(Date.now() - 24 * 3600_000).toISOString());
|
|
const cases: [string, () => string, (items: LogPage["items"]) => void][] = [
|
|
[
|
|
"level=error&since=24h",
|
|
() => `?level=error&from=${since24h()}&limit=50`,
|
|
(items) => {
|
|
expect(items.length).toBeGreaterThan(0);
|
|
expect(items.every((e) => e.level === "error")).toBe(true);
|
|
},
|
|
],
|
|
[
|
|
"system=1&q=feeds",
|
|
() => "?system=1&q=feeds&limit=50",
|
|
(items) => {
|
|
// 业务标题(RSS 源「my feeds 汇总」)命中的任务
|
|
expect(items.some((e) => e.source === "engine_run" && e.title === "my feeds 汇总")).toBe(true);
|
|
},
|
|
],
|
|
[
|
|
"type=task&level=error",
|
|
() => "?type=task&level=error&limit=50",
|
|
(items) => {
|
|
expect(items).toHaveLength(50);
|
|
expect(items.every((e) => e.type === "task" && e.level === "error")).toBe(true);
|
|
},
|
|
],
|
|
["type=task", () => "?type=task&limit=50", (items) => expect(items).toHaveLength(50)],
|
|
["默认", () => "?limit=50", (items) => expect(items).toHaveLength(50)],
|
|
// 关键词一条都不命中:引擎来源要扫完全部运行,是最坏情况
|
|
["system=1&q=<不命中>", () => "?system=1&q=zz-nomatch&limit=50", (items) => expect(items).toEqual([])],
|
|
];
|
|
|
|
for (const [name, query, check] of cases) {
|
|
it(`${name} < ${LIMIT_MS}ms`, async () => {
|
|
const t = performance.now();
|
|
const res = await admin("GET", `/logs${query()}`);
|
|
const ms = performance.now() - t;
|
|
expect(res.status).toBe(200);
|
|
const items = LogPage.parse((await json(res)).data).items;
|
|
console.log(`[logs-volume] ${name}: ${ms.toFixed(0)}ms(${items.length} 条)`);
|
|
check(items);
|
|
expect(ms).toBeLessThan(LIMIT_MS);
|
|
});
|
|
}
|
|
|
|
it(`流的一轮轮询 < ${LIMIT_MS}ms`, async () => {
|
|
const poller = createLogStreamPoller(logQueries(h.db, { engineSchema: h.engineSchema }), { system: "1" });
|
|
for (let i = 0; i < 2; i++) {
|
|
const t = performance.now();
|
|
await poller.poll();
|
|
const ms = performance.now() - t;
|
|
console.log(`[logs-volume] 流轮询 #${i + 1}: ${ms.toFixed(0)}ms`);
|
|
expect(ms).toBeLessThan(LIMIT_MS);
|
|
}
|
|
});
|
|
});
|