diff options
| author | Yulong Zhang <yulongzhang@google.com> | 2026-02-08 00:36:35 +0000 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2026-02-12 18:09:12 +0000 |
| commit | 6a673c5037dce5b85634cac4fabcc3fa5d33bb43 (patch) | |
| tree | 9d0c3f1d8d0f260dfe96f1ffd9d76e453435587c | |
| parent | 504cb1bfd362b0bb3b5d58f3c0abc7cc8c8ad026 (diff) | |
dashboard/app: add llm/tool stats/graphs
| -rw-r--r-- | dashboard/app/ai.go | 23 | ||||
| -rw-r--r-- | dashboard/app/templates/ai_job.html | 171 |
2 files changed, 184 insertions, 10 deletions
diff --git a/dashboard/app/ai.go b/dashboard/app/ai.go index 053ce35de..166473494 100644 --- a/dashboard/app/ai.go +++ b/dashboard/app/ai.go @@ -39,10 +39,11 @@ type uiAIJobPage struct { Header *uiHeader Job *uiAIJob // The slice contains the same single Job, just for HTML templates convenience. - Jobs []*uiAIJob - CrashReport template.HTML - Trajectory []*uiAITrajectorySpan - History []*uiJobReviewHistory + Jobs []*uiAIJob + CrashReport template.HTML + Trajectory []*uiAITrajectorySpan + History []*uiJobReviewHistory + TrajectoryJSON template.JS } type uiJobReviewHistory struct { @@ -194,13 +195,15 @@ func handleAIJobPage(ctx context.Context, w http.ResponseWriter, r *http.Request crashReport = linkifyReport(report, args["KernelRepo"].(string), args["KernelCommit"].(string)) } uiJob := makeUIAIJob(job) + trajectoryJSON, _ := json.Marshal(makeUIAITrajectory(trajectory)) page := &uiAIJobPage{ - Header: hdr, - Job: uiJob, - Jobs: []*uiAIJob{uiJob}, - CrashReport: crashReport, - Trajectory: makeUIAITrajectory(trajectory), - History: makeUIJobReviewHistory(history), + Header: hdr, + Job: uiJob, + Jobs: []*uiAIJob{uiJob}, + CrashReport: crashReport, + Trajectory: makeUIAITrajectory(trajectory), + History: makeUIJobReviewHistory(history), + TrajectoryJSON: template.JS(trajectoryJSON), } return serveTemplate(w, "ai_job.html", page) } diff --git a/dashboard/app/templates/ai_job.html b/dashboard/app/templates/ai_job.html index 09b2e5370..ca3c10131 100644 --- a/dashboard/app/templates/ai_job.html +++ b/dashboard/app/templates/ai_job.html @@ -10,6 +10,144 @@ Detailed info on a single AI job execution. <head> {{template "head" .Header}} <title>syzbot</title> + <script type="text/javascript" src="https://www.google.com/jsapi"></script> + <script type="text/javascript"> + google.load("visualization", "1", {packages:["corechart"]}); + google.setOnLoadCallback(drawCharts); + + function drawCharts() { + const rawData = {{.TrajectoryJSON}}; + if (!rawData || rawData.length === 0) return; + + drawSummaryTables(rawData); + drawDurationChart(rawData); + drawTokenConsumptionChart(rawData); + } + + function drawSummaryTables(rawData) { + const stats = { + llm: { count: 0, tokens: 0, duration: 0 }, + tool: { count: 0, duration: 0 } + }; + + rawData.forEach(s => { + const dur = s.Duration / 1000000000; // Convert ns to s + if (s.Type === "llm") { + stats.llm.count++; + stats.llm.tokens += (s.InputTokens + s.OutputTokens + s.OutputThoughtsTokens); + stats.llm.duration += dur; + } else if (s.Type === "tool") { + stats.tool.count++; + stats.tool.duration += dur; + } + }); + + const avgLlmDur = stats.llm.count > 0 ? (stats.llm.duration / stats.llm.count).toFixed(2) : 0; + const avgLlmToken = stats.llm.count > 0 ? (stats.llm.tokens / stats.llm.count).toFixed(2) : 0; + const avgToolDur = stats.tool.count > 0 ? (stats.tool.duration / stats.tool.count).toFixed(2) : 0; + + const llmSumaryCells = [stats.llm.count, stats.llm.tokens, avgLlmToken, stats.llm.duration, avgLlmDur]; + const toolSummaryCells = [stats.tool.count, stats.tool.duration, avgToolDur]; + + const llmSummaryBody = document.getElementById('llm_summary_stats_body'); + llmSummaryBody.innerHTML = ""; + const llmTr = document.createElement('tr'); + llmSumaryCells.forEach(cell => { + const td = document.createElement('td'); + td.textContent = cell; + llmTr.appendChild(td); + }); + llmSummaryBody.appendChild(llmTr); + + const toolSummaryBody = document.getElementById('tool_summary_stats_body'); + toolSummaryBody.innerHTML = ""; + const toolTr = document.createElement('tr'); + toolSummaryCells.forEach(cell => { + const td = document.createElement('td'); + td.textContent = cell; + toolTr.appendChild(td); + }); + toolSummaryBody.appendChild(toolTr); + } + + function drawDurationChart(rawData) { + const filteredSteps = rawData.filter(d => ["llm", "tool"].includes(d.Type)); + const data = new google.visualization.DataTable(); + data.addColumn('string', 'Step'); + data.addColumn('number', 'Duration'); + data.addColumn({type: 'string', role: 'tooltip'}); // Hover info + + filteredSteps.forEach(s => { + // Convert Go duration (nanoseconds) to seconds + const dur = s.Duration / 1000000000; + data.addRow([`${s.Seq}`, dur, s.Name]); + }); + + const options = { + title: 'Time Consumption of LLM/Tool Calls', + hAxis: { + title: 'LLM/Tool Steps', + }, + vAxis: { title: 'Seconds' }, + legend: { position: 'none' }, + bar: { groupWidth: '75%' }, + chartArea: { width: '85%', height: '70%' }, + height: 400 + }; + + const chart = new google.visualization.ColumnChart(document.getElementById('duration_chart_div')); + chart.draw(data, options); + } + + function drawTokenConsumptionChart(rawData) { + // Filter only LLM steps + const llmSteps = rawData.filter(d => d.Type === "llm"); + if (llmSteps.length === 0) return; + + const data = new google.visualization.DataTable(); + data.addColumn('string', 'Step'); // X-Axis (Label) + data.addColumn('number', 'Tokens'); // Y-Axis (Value) + data.addColumn({type: 'string', role: 'style'}); // Bar Color + data.addColumn({type: 'string', role: 'tooltip'}); // Hover info + + // Define a palette or a dynamic color generator + const agentColors = {}; + const palette = ['#4285F4', '#DB4437', '#F4B400', '#0F9D58', '#AB47BC', '#00ACC1']; + let colorIdx = 0; + // Tracks count per agent + const agentLLMCounts = {}; + llmSteps.forEach(s => { + // Assign color to agent if not already assigned + if (!agentColors[s.Name]) { + agentColors[s.Name] = palette[colorIdx % palette.length]; + colorIdx++; + } + + agentLLMCounts[s.Name] = (agentLLMCounts[s.Name] || 0) + 1; + const label = `${s.Name}-${agentLLMCounts[s.Name]}`; + const totalTokens = s.InputTokens + s.OutputTokens + s.OutputThoughtsTokens; + const style = `color: ${agentColors[s.Name]}`; + const tooltip = `Total Tokens: ${totalTokens}\n(In: ${s.InputTokens}, Out: ${s.OutputTokens}, Thoughts: ${s.OutputThoughtsTokens})`; + + data.addRow([label, totalTokens, style, tooltip]); + }); + + const options = { + title: 'Token Consumption by LLM Calls', + hAxis: { + title: 'LLM Call Sequence', + }, + vAxis: { title: 'Consumed Tokens' }, + legend: { position: 'none' }, + bar: { groupWidth: '75%' }, + chartArea: { width: '85%', height: '70%' }, + height: 400 + }; + + const chart = new google.visualization.ColumnChart(document.getElementById('token_chart_div')); + chart.draw(data, options); + } + </script> </head> <body> {{template "header" .Header}} @@ -125,5 +263,38 @@ Detailed info on a single AI job execution. {{end}} </tbody> </table> + + <div id="llm_summary_table_div" style="margin: 20px 0;"> + <table class="list_table"> + <caption>LLM Calls Summary:</caption> + <thead> + <tr> + <th>Total Calls</th> + <th>Total Tokens</th> + <th>Avg Tokens</th> + <th>Total Duration (Seconds)</th> + <th>Avg Duration (Seconds)</th> + </tr> + </thead> + <tbody id="llm_summary_stats_body"></tbody> + </table> + </div> + + <div id="tool_summary_table_div" style="margin: 20px 0;"> + <table class="list_table"> + <caption>Tool Calls Summary:</caption> + <thead> + <tr> + <th>Total Calls</th> + <th>Total Duration (Seconds)</th> + <th>Avg Duration (Seconds)</th> + </tr> + </thead> + <tbody id="tool_summary_stats_body"></tbody> + </table> + </div> + + <div id="duration_chart_div" style="width: 100%; margin: 20px 0; border: 1px;"></div> + <div id="token_chart_div" style="width: 100%; margin: 20px 0; border: 1px;"></div> </body> </html> |
