Skip to content

Commit a5abb6f

Browse files
authored
Fetch summary report (#7275)
Setup an API to fetch regression summary report. Currently only return suspicious and regression level data Demo Api resp: https://torchci-git-fetchsummaryreport-fbopensource.vercel.app/api/benchmark/list_regression_summary_reports?report_id=compiler_regression&&limit=2
1 parent d422175 commit a5abb6f

File tree

2 files changed

+232
-0
lines changed

2 files changed

+232
-0
lines changed

torchci/lib/benchmark/api_helper/compilers/helpers/common.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,32 @@ function deepDiff(obj1: any, obj2: any) {
7575

7676
return diffs;
7777
}
78+
79+
export function parseTimestampToken(
80+
token: string | number | null | undefined
81+
): number | null {
82+
if (token == null) return null;
83+
84+
if (typeof token === "string") {
85+
const ms = Date.parse(token.trim()); // parses ISO, RFC2822, etc.
86+
return isNaN(ms) ? null : ms; // return epoch ms
87+
}
88+
89+
if (typeof token === "number") {
90+
const d = new Date(token);
91+
return isNaN(d.getTime()) ? null : d.getTime();
92+
}
93+
94+
return null;
95+
}
96+
97+
export function parseTimestampTokenSeconds(
98+
token: string | number | null | undefined
99+
): number | null {
100+
const ms = parseTimestampToken(token);
101+
return ms == null ? null : Math.floor(ms / 1000);
102+
}
103+
104+
export function badRequest(message: string, res: any) {
105+
return res.json({ error: message }, { status: 400 });
106+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import {
2+
badRequest,
3+
parseTimestampTokenSeconds,
4+
} from "lib/benchmark/api_helper/compilers/helpers/common";
5+
import { readApiGetParams } from "lib/benchmark/api_helper/utils";
6+
import { queryClickhouse } from "lib/clickhouse";
7+
import { NextApiRequest, NextApiResponse } from "next";
8+
9+
const DEFAULT_QUERY_LIMIT = 25;
10+
const MAX_QUERY_LIMIT = 200;
11+
12+
const REPORT_TABLE = "fortesting.benchmark_regression_report";
13+
14+
export default async function handler(
15+
req: NextApiRequest,
16+
res: NextApiResponse
17+
) {
18+
if (req.method !== "GET" && req.method !== "POST") {
19+
res.setHeader("Allow", "GET, POST");
20+
return res.status(405).json({ error: "Only GET and POST allowed" });
21+
}
22+
23+
const params = readApiGetParams(req);
24+
console.log(
25+
"[API]list regression summmary report, received request:",
26+
params
27+
);
28+
29+
// validate params
30+
if (!params || !params.report_id) {
31+
return badRequest("Missing required params report_id", res);
32+
}
33+
34+
// list regression summary report for a given type. ex compiler_regression
35+
const { report_id, limit, last_ts_token } = params;
36+
try {
37+
const query_limit = getQueryLimit(limit);
38+
const last_ts = parseTimestampTokenSeconds(last_ts_token);
39+
// validate last_ts_token only if it is provided
40+
if (last_ts_token && !last_ts) {
41+
return badRequest(
42+
`invalid input params last_ts_token "${last_ts_token}"`,
43+
res
44+
);
45+
}
46+
47+
console.log(
48+
"[API][DB]list regression summary report with query_limit",
49+
query_limit
50+
);
51+
const results = await queryFromDb({
52+
report_id,
53+
limit: query_limit,
54+
ts_token: last_ts,
55+
});
56+
57+
const resp = toApiFormat(results);
58+
59+
return res.status(200).json(resp);
60+
} catch (err: any) {
61+
return res.status(400).json({ error: err.message });
62+
}
63+
}
64+
65+
async function queryFromDb({
66+
report_id,
67+
limit,
68+
ts_token,
69+
}: {
70+
report_id: string;
71+
limit: number;
72+
ts_token?: number | null; // epoch seconds or null
73+
}) {
74+
const { query, params } = buildQuery({
75+
table: REPORT_TABLE,
76+
report_id,
77+
limit,
78+
ts_token,
79+
});
80+
81+
console.log("[API][DB]list regression summary reporr with params", params);
82+
const results = await queryClickhouse(query, params);
83+
return results;
84+
}
85+
86+
function buildQuery({
87+
table,
88+
report_id,
89+
limit,
90+
ts_token,
91+
}: {
92+
table: string;
93+
report_id: string;
94+
limit: number;
95+
ts_token?: number | null; // epoch seconds or null
96+
}) {
97+
const where: string[] = ["report_id = {report_id:String}"];
98+
if (ts_token != null) {
99+
where.push("last_record_ts < toDateTime({ts_token:UInt32})");
100+
}
101+
const whereSql = where.join(" AND ");
102+
103+
const query = `
104+
SELECT
105+
*
106+
FROM ${table}
107+
WHERE ${whereSql}
108+
ORDER BY last_record_ts DESC
109+
LIMIT {limit:UInt32}
110+
`;
111+
112+
const params: Record<string, string | number> = {
113+
report_id,
114+
limit,
115+
...(ts_token != null ? { ts_token } : {}),
116+
};
117+
118+
return { query, params };
119+
}
120+
121+
function toApiFormat(dbResult: any[]) {
122+
const items = mapReportField(dbResult, "report");
123+
const next_cursor = items.length
124+
? items[items.length - 1].last_record_ts
125+
: null;
126+
const miniReports = [];
127+
128+
for (const item of items) {
129+
const { report, ...rest } = item;
130+
131+
const otherFields = rest;
132+
133+
if (!report) {
134+
return {
135+
...otherFields,
136+
};
137+
}
138+
const policy = report.policy;
139+
const r = report?.report;
140+
const startInfo = r?.baseline_meta_data?.start;
141+
const endInfo = r?.baseline_meta_data?.end;
142+
const buckets = transformReportRows(r?.results ?? []);
143+
miniReports.push({
144+
...otherFields,
145+
policy,
146+
start: startInfo,
147+
end: endInfo,
148+
details: buckets,
149+
});
150+
}
151+
152+
return {
153+
reports: miniReports,
154+
next_cursor,
155+
};
156+
}
157+
158+
function safeJsonParse<T = unknown>(s: unknown): T | null {
159+
if (s == null) return null;
160+
if (typeof s !== "string") return s as T; // already parsed or not a string
161+
try {
162+
return JSON.parse(s) as T;
163+
} catch {
164+
return null; // or throw if you prefer strictness
165+
}
166+
}
167+
168+
/** Map ClickHouse rows so `report` (JSON string) becomes an object. */
169+
function mapReportField<T = unknown>(
170+
rows: Array<Record<string, any>>,
171+
fieldName: string = "report"
172+
): Array<Record<string, any>> {
173+
return rows.map((r) => ({
174+
...r,
175+
[fieldName]: safeJsonParse<T>(r[fieldName]),
176+
}));
177+
}
178+
179+
function getQueryLimit(limit: any) {
180+
let query_limit = Number(limit ?? DEFAULT_QUERY_LIMIT);
181+
if (!Number.isFinite(query_limit) || query_limit <= 0)
182+
query_limit = DEFAULT_QUERY_LIMIT;
183+
query_limit = Math.min(query_limit, MAX_QUERY_LIMIT);
184+
return query_limit;
185+
}
186+
187+
export function transformReportRows(
188+
results: Array<Record<string, any>>
189+
): Record<"regression" | "suspicious", any[]> {
190+
const resultBuckets: Record<"regression" | "suspicious", any[]> = {
191+
regression: [],
192+
suspicious: [],
193+
};
194+
195+
for (const item of results) {
196+
if (item.label === "regression") {
197+
resultBuckets.regression.push(item);
198+
} else if (item.label === "suspicious") {
199+
resultBuckets.suspicious.push(item);
200+
}
201+
}
202+
return resultBuckets;
203+
}

0 commit comments

Comments
 (0)