Skip to content

Commit 62d0084

Browse files
authored
Multi-logging sidepanel (#7431)
# Overview Add logging side panel for searching. This fetches all logs belongs to two workflow runs selected each workflow has combined logs view for search # Motivation the benchmark jobs runs on shards, that means models may run in any of the sharding jobs, when it comes to debug, for old UI, user needs to open each log file to search, compiler team oncall constantly open 7 for each suit if any model fails, or have regression. This multilog feature make the log searchable, and list models that are included in the data. It reduce lots of tedious work for oncall # Features 1 . add search label to filter # of logs based on fields 2. enable search log in codeMirror # Demo Link https://torchci-git-addlogging-fbopensource.vercel.app/benchmark/v3/dashboard/compiler_inductor # Demo 1.Filter based on kep labels ![filterout2](https://github.com/user-attachments/assets/d08935a3-4a7d-4b6f-9abb-6b880742b5c4) 2. Search in logging section ![Oct-31-2025 16-21-50](https://github.com/user-attachments/assets/e1ab2791-2960-41b6-9fe5-b7108ea8ec49) 3. Jump to each log's head ![Oct-31-2025 16-22-42](https://github.com/user-attachments/assets/900ecbdd-9e58-49d1-83db-b1a350fdb252)
1 parent dd72a20 commit 62d0084

File tree

9 files changed

+916
-2
lines changed

9 files changed

+916
-2
lines changed
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
import { EditorSelection } from "@codemirror/state";
2+
import { EditorView } from "@codemirror/view";
3+
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
4+
import {
5+
Autocomplete,
6+
Button,
7+
Chip,
8+
Divider,
9+
IconButton,
10+
Link,
11+
TextField,
12+
Tooltip,
13+
Typography,
14+
} from "@mui/material";
15+
import { Box, Stack } from "@mui/system";
16+
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
17+
import { LogSrc } from "./BenchmarkLogViewContent";
18+
export function LogUrlList({
19+
urls,
20+
viewRef,
21+
query,
22+
setQuery,
23+
width = "80vw",
24+
height = "60vh",
25+
}: {
26+
urls: LogSrc[];
27+
viewRef: React.MutableRefObject<EditorView | null>;
28+
query: string;
29+
setQuery: (s: string) => void;
30+
width?: any;
31+
height?: any;
32+
}) {
33+
// chips state (derived from query initially)
34+
const [terms, setTerms] = useState<string[]>(
35+
query
36+
.split(/\s+/)
37+
.map((t) => t.trim())
38+
.filter(Boolean)
39+
);
40+
const [inputValue, setInputValue] = useState("");
41+
42+
// keep external query string loosely in sync
43+
useEffect(() => {
44+
setQuery(terms.join(" "));
45+
}, [terms, setQuery]);
46+
47+
// normalized chips (dedupe + lowercase)
48+
const normTerms = useMemo(
49+
() =>
50+
Array.from(new Set(terms.map((t) => t.toLowerCase()).filter(Boolean))),
51+
[terms]
52+
);
53+
54+
const filtered = useMemo(() => {
55+
if (!normTerms.length) return urls;
56+
return urls.filter((u) => {
57+
const hay = buildHaystack(u);
58+
return normTerms.every((t) => hay.includes(t));
59+
});
60+
}, [urls, normTerms]);
61+
62+
// add a term from the current input
63+
const commitInputAsTerm = useCallback(() => {
64+
const t = inputValue.trim();
65+
if (!t) return;
66+
if (!terms.includes(t)) setTerms((prev) => [...prev, t]);
67+
setInputValue("");
68+
}, [inputValue, terms]);
69+
70+
const removeAt = useCallback(
71+
(idx: number) => setTerms((prev) => prev.filter((_, i) => i !== idx)),
72+
[]
73+
);
74+
75+
return (
76+
<Box width={width}>
77+
<Autocomplete<string, true, false, true>
78+
multiple
79+
freeSolo
80+
options={[] as string[]}
81+
value={terms}
82+
inputValue={inputValue}
83+
onInputChange={(_e, v) => setInputValue(v)}
84+
onChange={(_e, newValue) => setTerms(newValue)}
85+
renderValue={(selected /* string[] */) => (
86+
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5, m: 1 }}>
87+
{selected.map((option, idx) => (
88+
<Chip
89+
key={`${option}-${idx}`}
90+
variant="outlined"
91+
size="small"
92+
label={option}
93+
onDelete={() => removeAt(idx)}
94+
/>
95+
))}
96+
</Box>
97+
)}
98+
renderInput={(params) => (
99+
<TextField
100+
{...params}
101+
size="small"
102+
placeholder="Search label / url / info…"
103+
onKeyDown={(e) => {
104+
if (["Enter", " ", "Comma"].includes(e.key) || e.key === ",") {
105+
e.preventDefault();
106+
commitInputAsTerm();
107+
return;
108+
}
109+
if (e.key === "Backspace" && !inputValue && terms.length > 0) {
110+
e.preventDefault();
111+
removeAt(terms.length - 1);
112+
}
113+
}}
114+
sx={{ m: 1 }}
115+
/>
116+
)}
117+
/>
118+
<SearchTipsTooltip />
119+
<Divider sx={{ my: 1 }} />
120+
<Box
121+
sx={{
122+
height,
123+
minHeight: "10vh",
124+
overflowY: "auto",
125+
pr: 1,
126+
minWidth: 0,
127+
}}
128+
>
129+
{filtered.map((u, i) => (
130+
<Fragment key={`${u.url}-${i}`}>
131+
<Stack>
132+
<Stack direction="row" alignItems="center" spacing={1}>
133+
<Typography
134+
variant="body2"
135+
sx={{ minWidth: 60, fontWeight: 600 }}
136+
>
137+
{highlightChunksMulti(u.label ?? `Log ${i + 1}`, terms)}
138+
</Typography>
139+
<Tooltip
140+
title="Naivgate to the head of the log in log view"
141+
placement="top"
142+
arrow
143+
>
144+
<Button
145+
size="small"
146+
sx={{
147+
textTransform: "none", // keep lowercase
148+
fontSize: "0.7rem", // smaller font
149+
minWidth: 0, // remove default min width
150+
px: 0.5, // tighter horizontal padding
151+
py: 0, // tighter vertical padding
152+
}}
153+
variant="contained"
154+
onClick={() => {
155+
const v = viewRef.current;
156+
if (!v) return;
157+
const fallback = u.url || "";
158+
cmJumpToFirstMatch(v, fallback);
159+
}}
160+
>
161+
jump to head of log
162+
</Button>
163+
</Tooltip>
164+
</Stack>
165+
<Stack direction="row" alignItems="center" spacing={1}>
166+
<Typography variant="caption">Source:</Typography>
167+
<Link
168+
href={u.url}
169+
target="_blank"
170+
rel="noopener"
171+
underline="hover"
172+
sx={{
173+
fontSize: "0.8rem",
174+
wordBreak: "break-all",
175+
color: "text.secondary",
176+
flex: 1,
177+
}}
178+
>
179+
{highlightChunksMulti(u.url, terms)}
180+
</Link>
181+
</Stack>
182+
183+
{u.info && (
184+
<Stack sx={{ mt: 0.5 }}>
185+
<Typography variant="caption" sx={{ fontWeight: 600 }}>
186+
Info:
187+
</Typography>
188+
{Object.entries(u.info).map(([k, v]) => {
189+
const vs = Array.isArray(v) ? v.join(", ") : String(v);
190+
return (
191+
<Box key={k} sx={{ display: "flex", gap: 1, ml: 2 }}>
192+
<Typography variant="caption" sx={{ fontWeight: 500 }}>
193+
{highlightChunksMulti(`${k}:`, terms)}
194+
</Typography>
195+
<Typography
196+
variant="caption"
197+
sx={{ color: "text.secondary" }}
198+
>
199+
{highlightChunksMulti(vs, terms)}
200+
</Typography>
201+
</Box>
202+
);
203+
})}
204+
</Stack>
205+
)}
206+
</Stack>
207+
<Divider sx={{ my: 1 }} />
208+
</Fragment>
209+
))}
210+
</Box>
211+
<Divider sx={{ my: 1 }} />
212+
</Box>
213+
);
214+
}
215+
216+
function escapeRegExp(s: string) {
217+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
218+
}
219+
220+
// Flatten any nested info into plain strings
221+
function flattenInfo(val: unknown, out: string[] = []): string[] {
222+
if (val == null) return out;
223+
if (
224+
typeof val === "string" ||
225+
typeof val === "number" ||
226+
typeof val === "boolean"
227+
) {
228+
out.push(String(val));
229+
} else if (Array.isArray(val)) {
230+
for (const v of val) flattenInfo(v, out);
231+
} else if (typeof val === "object") {
232+
for (const [k, v] of Object.entries(val as Record<string, unknown>)) {
233+
out.push(k); // include keys so "arch", "device" are searchable
234+
flattenInfo(v, out);
235+
}
236+
}
237+
return out;
238+
}
239+
240+
function buildHaystack(u: LogSrc): string {
241+
const parts = [u.label ?? "", u.url ?? "", ...flattenInfo(u.info ?? {})];
242+
return parts.join(" ").toLowerCase();
243+
}
244+
245+
// Highlight ALL chips
246+
function highlightChunksMulti(text: string, terms: string[]): React.ReactNode {
247+
if (!terms?.length) return text;
248+
const pattern = new RegExp(terms.map(escapeRegExp).join("|"), "ig");
249+
const out: React.ReactNode[] = [];
250+
let last = 0,
251+
m: RegExpExecArray | null;
252+
while ((m = pattern.exec(text)) !== null) {
253+
const start = m.index,
254+
end = start + m[0].length;
255+
if (start > last) out.push(text.slice(last, start));
256+
out.push(<mark key={`${start}-${end}`}>{text.slice(start, end)}</mark>);
257+
last = end;
258+
}
259+
if (last < text.length) out.push(text.slice(last));
260+
return out;
261+
}
262+
263+
// Jump helper
264+
export function cmJumpToFirstMatch(
265+
view: EditorView | null,
266+
query: string | RegExp,
267+
opts: { caseSensitive?: boolean } = {}
268+
): boolean {
269+
if (!view) return false;
270+
const text = view.state.doc.toString();
271+
272+
const re =
273+
typeof query === "string"
274+
? new RegExp(escapeRegExp(query), opts.caseSensitive ? "" : "i")
275+
: query;
276+
277+
const m = text.match(re);
278+
if (!m || m.index == null) return false;
279+
280+
const from = m.index;
281+
const to = from + m[0].length;
282+
view.dispatch({
283+
selection: EditorSelection.single(from, to),
284+
scrollIntoView: true,
285+
});
286+
view.focus();
287+
return true;
288+
}
289+
290+
export function SearchTipsTooltip() {
291+
return (
292+
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ ml: 1 }}>
293+
<Typography variant="body2" sx={{ mr: 0.5 }}>
294+
Search Tips:
295+
</Typography>
296+
<Tooltip
297+
title={<SearchTipsContent />}
298+
placement="top"
299+
arrow
300+
slotProps={{
301+
tooltip: {
302+
sx: {
303+
bgcolor: "background.paper",
304+
color: "text.primary",
305+
boxShadow: 3,
306+
p: 1.2,
307+
},
308+
},
309+
}}
310+
>
311+
<IconButton size="small">
312+
<InfoOutlinedIcon fontSize="small" />
313+
</IconButton>
314+
</Tooltip>
315+
</Stack>
316+
);
317+
}
318+
319+
function SearchTipsContent() {
320+
return (
321+
<Box sx={{ mx: 1.5, mt: 0.5, color: "text.secondary" }}>
322+
<Stack direction="row" spacing={1} alignItems="flex-start">
323+
<InfoOutlinedIcon fontSize="small" sx={{ mt: "2px", opacity: 0.8 }} />
324+
<Box>
325+
<Typography variant="caption" sx={{ display: "block" }}>
326+
<b>Search scope:</b> search all values in info section & source
327+
section below.
328+
</Typography>
329+
<Typography variant="caption" sx={{ display: "block", mt: 0.25 }}>
330+
<b>How to filter:</b> type terms and press <b>Enter</b>/<b>Space</b>
331+
/<b>,</b>. Multiple terms are combined with <b>AND</b>.
332+
</Typography>
333+
{/* optional: small examples row */}
334+
<Stack
335+
direction="row"
336+
spacing={0.5}
337+
sx={{ mt: 0.5, flexWrap: "wrap" }}
338+
>
339+
<Typography variant="caption" sx={{ mr: 0.5 }}>
340+
Examples:
341+
</Typography>
342+
<Chip size="small" variant="outlined" label="timm_models" />
343+
<Chip size="small" variant="outlined" label="adv_inception_v3" />
344+
</Stack>
345+
<Typography variant="caption" sx={{ display: "block", mt: 0.25 }}>
346+
<b>Field option coverage:</b> the available fields (e.g.{" "}
347+
<code>model</code>, <code>arch</code>) reflect your <i>main page</i>{" "}
348+
filters and may not be the full list of content in the log.
349+
</Typography>
350+
</Box>
351+
</Stack>
352+
</Box>
353+
);
354+
}

0 commit comments

Comments
 (0)