Skip to content

Commit

Permalink
cmd/trace: integrate perfetto ui (experimental)
Browse files Browse the repository at this point in the history
The current catapult trace viewer is bit rotting and struggles with
large traces. Perfetto UI [1] could offer better performance and UX.

This patch gives users the ability to view their traces in both viewers.
If perfetto works well, catapult can be removed in the future.

For golang#57315

[1] https://ui.perfetto.dev/

Change-Id: I3fe248b8b7f1820f0847af5a4a234314fef3b36f
  • Loading branch information
felixge committed Jan 18, 2023
1 parent 7026a8a commit 01e7359
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 10 deletions.
13 changes: 9 additions & 4 deletions src/cmd/trace/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,7 @@ var templUserTaskType = template.Must(template.New("userTask").Funcs(template.Fu
margin-top: 5em;
}
</style>
` + perfettoScript + `
<body>
<h2>User Task: {{.Name}}</h2>
Expand All @@ -1064,6 +1065,8 @@ Search log text: <form onsubmit="window.location.search+='&logtext='+window.logt
<input name="logtext" id="logtextinput" type="text"><input type="submit">
</form><br>
` + perfettoCheckbox + `
<table id="reqs">
<tr><th>When</th><th>Elapsed</th><th>Goroutine ID</th><th>Events</th></tr>
{{range $el := $.Entry}}
Expand All @@ -1072,8 +1075,8 @@ Search log text: <form onsubmit="window.location.search+='&logtext='+window.logt
<td class="elapsed">{{$el.Duration}}</td>
<td></td>
<td>
<a href="/trace?focustask={{$el.ID}}#{{asMillisecond $el.Start}}:{{asMillisecond $el.End}}">Task {{$el.ID}}</a>
<a href="/trace?taskid={{$el.ID}}#{{asMillisecond $el.Start}}:{{asMillisecond $el.End}}">(goroutine view)</a>
<span class="viewtrace"><a href="/trace?focustask={{$el.ID}}#{{asMillisecond $el.Start}}:{{asMillisecond $el.End}}" data-href-json="/jsontrace?focustask={{$el.ID}}">Task {{$el.ID}}</a></span>
(<span class="viewtrace"><a href="/trace?taskid={{$el.ID}}#{{asMillisecond $el.Start}}:{{asMillisecond $el.End}}" data-href-json="/jsontrace?taskid={{$el.ID}}">goroutine view</a></span>)
({{if .Complete}}complete{{else}}incomplete{{end}})</td>
</tr>
{{range $el.Events}}
Expand Down Expand Up @@ -1254,6 +1257,7 @@ function reloadTable(key, value) {
window.location.search = params.toString();
}
</script>
` + perfettoScript + `
<h2>{{.Name}}</h2>
Expand All @@ -1266,6 +1270,7 @@ function reloadTable(key, value) {
</table>
{{ end }}
<p>
` + perfettoCheckbox + `
<table class="details">
<tr>
<th> Goroutine </th>
Expand All @@ -1282,8 +1287,8 @@ function reloadTable(key, value) {
</tr>
{{range .Data}}
<tr>
<td> <a href="/trace?goid={{.G}}">{{.G}}</a> </td>
<td> {{if .TaskID}}<a href="/trace?focustask={{.TaskID}}">{{.TaskID}}</a>{{end}} </td>
<td class="viewtrace"> <a href="/trace?goid={{.G}}" data-href-json="/jsontrace?goid={{.G}}">{{.G}}</a> </td>
<td class="viewtrace"> {{if .TaskID}}<a href="/trace?focustask={{.TaskID}}" data-href-json="/jsontrace?focustask={{.TaskID}}">{{.TaskID}}</a>{{end}} </td>
<td> {{prettyDuration .TotalTime}} </td>
<td>
<div class="stacked-bar-graph">
Expand Down
4 changes: 3 additions & 1 deletion src/cmd/trace/goroutines.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ function reloadTable(key, value) {
window.location.search = params.toString();
}
</script>
` + perfettoScript + `
<table class="summary">
<tr><td>Goroutine Name:</td><td>{{.Name}}</td></tr>
Expand All @@ -262,6 +263,7 @@ function reloadTable(key, value) {
<tr><td>Scheduler Wait Time:</td><td> <a href="/sched?id={{.PC}}">graph</a><a href="/sched?id={{.PC}}&raw=1" download="sched.profile">(download)</a></td></tr>
</table>
<p>
` + perfettoCheckbox + `
<table class="details">
<tr>
<th> Goroutine</th>
Expand All @@ -277,7 +279,7 @@ function reloadTable(key, value) {
</tr>
{{range .GList}}
<tr>
<td> <a href="/trace?goid={{.ID}}">{{.ID}}</a> </td>
<td class="viewtrace"> <a href="/trace?goid={{.ID}}" data-href-json="/jsontrace?goid={{.ID}}">{{.ID}}</a> </td>
<td> {{prettyDuration .TotalTime}} </td>
<td>
<div class="stacked-bar-graph">
Expand Down
109 changes: 105 additions & 4 deletions src/cmd/trace/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,105 @@ func httpMain(w http.ResponseWriter, r *http.Request) {
}
}

const perfettoCheckbox = `<label><input id="perfetto" type="checkbox" name="checkbox" value="value">Use Perfetto UI (experimental)</label>`

const perfettoScript = `
<script>
window.addEventListener('load', e => {
document
.querySelectorAll('.viewtrace > a')
.forEach(el => {
el.dataset.href = el.href;
el.addEventListener('click', onClickViewTrace);
});
const perfettoCheckbox = document.getElementById('perfetto');
perfettoCheckbox.addEventListener('click', el => {
// Restore trace links and indicator state depending on which viewer is
// selected by the user.
document
.querySelectorAll('.viewtrace .indicator')
.forEach(el => el.hidden = !perfettoCheckbox.checked);
document
.querySelectorAll('.viewtrace > a')
.forEach(link => {
const href = perfettoCheckbox.checked ? link.dataset.hrefJson : link.dataset.href;
if (!href) {
link.removeAttribute('href');
} else {
link.setAttribute('href', href);
}
})
});
});
function onClickViewTrace(e) {
const perfettoCheckbox = document.querySelector('#perfetto');
if (!perfettoCheckbox.checked) {
return;
}
e.preventDefault();
const link = this;
const traceURL = link.href;
let indicator = link.parentNode.querySelector('.indicator');
if (!indicator) {
delete link.dataset.hrefJson;
indicator = document.createElement('span');
indicator.className = 'indicator';
indicator.innerText = ' (loading ... this might take a bit)';
link.parentNode.appendChild(indicator);
}
link.removeAttribute('href'); // avoid user clicking more than once
fetch(traceURL)
.then(response => response.blob())
.then(blob => blob.arrayBuffer())
.then(arrayBuf => {
const openLink = document.createElement('a');
openLink.setAttribute('href', '#');
openLink.innerText = 'open';
openLink.addEventListener('click', onClickOpenHandler(arrayBuf));
indicator.innerHTML = '';
indicator.appendChild(document.createTextNode(' ('));
indicator.appendChild(openLink);
indicator.appendChild(document.createTextNode(')'));
// Try to automatically open Perfetto UI. If fetch() takes more than a
// few seconds, browsers may block this as an unwanted "popup". In this
// case users will have to manually click the link.
// See https://groups.google.com/g/perfetto-dev/c/Au39ZVrySgk
openLink.click();
});
}
function onClickOpenHandler(arrayBuf) {
return e => {
// See https://perfetto.dev/docs/visualization/deep-linking-to-perfetto-ui
// for how the code below works.
const origin = 'https://ui.perfetto.dev';
const handle = window.open(origin);
const timer = setInterval(() => handle.postMessage('PING', origin), 50);
const onMessageHandler = (evt) => {
if (evt.data !== 'PONG') return;
window.clearInterval(timer);
window.removeEventListener('message', onMessageHandler);
handle.postMessage({
perfetto: {
buffer: arrayBuf,
title: 'go tool trace', // TODO: use traceFile name?
}}, origin);
};
window.addEventListener('message', onMessageHandler);
};
}
</script>
`

var templMain = template.Must(template.New("").Parse(`
<html>
<style>
Expand All @@ -203,6 +302,7 @@ h1,h2 {
}
p { color: grey85; font-size:85%; }
</style>
` + perfettoScript + `
<body>
<h1>cmd/trace: the Go trace event viewer</h1>
<p>
Expand All @@ -211,19 +311,20 @@ p { color: grey85; font-size:85%; }
</p>
<h2>Event timelines for running goroutines</h2>
` + perfettoCheckbox + `
{{if $}}
<p>
Large traces are split into multiple sections of equal data size
(not duration) to avoid overwhelming the visualizer.
</p>
<ul>
{{range $e := $}}
<li><a href="{{$e.URL}}">View trace ({{$e.Name}})</a></li>
{{end}}
{{range $e := $}}
<li class="viewtrace"><a href="{{$e.URL}}" data-href-json="{{$e.JSONURL}}">View trace ({{$e.Name}})</a></li>
{{end}}
</ul>
{{else}}
<ul>
<li><a href="/trace">View trace</a></li>
<li class="viewtrace"><a href="/trace" data-href-json="/jsontrace">View trace</a></li>
</ul>
{{end}}
<p>
Expand Down
6 changes: 5 additions & 1 deletion src/cmd/trace/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ func (r Range) URL() string {
return fmt.Sprintf("/trace?start=%d&end=%d", r.Start, r.End)
}

func (r Range) JSONURL() string {
return fmt.Sprintf("/jsontrace?start=%d&end=%d", r.Start, r.End)
}

// splitTrace splits the trace into a number of ranges,
// each resulting in approx 100MB of json output
// (trace viewer can hardly handle more).
Expand Down Expand Up @@ -338,7 +342,7 @@ func stackFrameEncodedSize(id uint, f traceviewer.Frame) int {
// The parent is omitted if 0. The trailing comma is omitted from the
// last entry, but we don't need that much precision.
const (
baseSize = len(`"`) + len (`":{"name":"`) + len(`"},`)
baseSize = len(`"`) + len(`":{"name":"`) + len(`"},`)

// Don't count the trailing quote on the name, as that is
// counted in baseSize.
Expand Down

0 comments on commit 01e7359

Please sign in to comment.