1// Copyright 2023 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package traceviewer
6
7import (
8	"embed"
9	"fmt"
10	"html/template"
11	"net/http"
12	"strings"
13)
14
15func MainHandler(views []View) http.Handler {
16	return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
17		if err := templMain.Execute(w, views); err != nil {
18			http.Error(w, err.Error(), http.StatusInternalServerError)
19			return
20		}
21	})
22}
23
24const CommonStyle = `
25/* See https://github.com/golang/pkgsite/blob/master/static/shared/typography/typography.css */
26body {
27  font-family:	-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
28  font-size:	1rem;
29  line-height:	normal;
30  max-width:	9in;
31  margin:	1em;
32}
33h1 { font-size: 1.5rem; }
34h2 { font-size: 1.375rem; }
35h1,h2 {
36  font-weight: 600;
37  line-height: 1.25em;
38  word-break: break-word;
39}
40p  { color: grey85; font-size:85%; }
41code,
42pre,
43textarea.code {
44  font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
45  font-size: 0.875rem;
46  line-height: 1.5em;
47}
48
49pre,
50textarea.code {
51  background-color: var(--color-background-accented);
52  border: var(--border);
53  border-radius: var(--border-radius);
54  color: var(--color-text);
55  overflow-x: auto;
56  padding: 0.625rem;
57  tab-size: 4;
58  white-space: pre;
59}
60`
61
62var templMain = template.Must(template.New("").Parse(`
63<html>
64<style>` + CommonStyle + `</style>
65<body>
66<h1>cmd/trace: the Go trace event viewer</h1>
67<p>
68  This web server provides various visualizations of an event log gathered during
69  the execution of a Go program that uses the <a href='https://pkg.go.dev/runtime/trace'>runtime/trace</a> package.
70</p>
71
72<h2>Event timelines for running goroutines</h2>
73{{range $i, $view := $}}
74{{if $view.Ranges}}
75{{if eq $i 0}}
76<p>
77  Large traces are split into multiple sections of equal data size
78  (not duration) to avoid overwhelming the visualizer.
79</p>
80{{end}}
81<ul>
82	{{range $index, $e := $view.Ranges}}
83		<li><a href="{{$view.URL $index}}">View trace by {{$view.Type}} ({{$e.Name}})</a></li>
84	{{end}}
85</ul>
86{{else}}
87<ul>
88	<li><a href="{{$view.URL -1}}">View trace by {{$view.Type}}</a></li>
89</ul>
90{{end}}
91{{end}}
92<p>
93  This view displays a series of timelines for a type of resource.
94  The "by proc" view consists of a timeline for each of the GOMAXPROCS
95  logical processors, showing which goroutine (if any) was running on that
96  logical processor at each moment.
97  The "by thread" view (if available) consists of a similar timeline for each
98  OS thread.
99
100  Each goroutine has an identifying number (e.g. G123), main function,
101  and color.
102
103  A colored bar represents an uninterrupted span of execution.
104
105  Execution of a goroutine may migrate from one logical processor to another,
106  causing a single colored bar to be horizontally continuous but
107  vertically displaced.
108</p>
109<p>
110  Clicking on a span reveals information about it, such as its
111  duration, its causal predecessors and successors, and the stack trace
112  at the final moment when it yielded the logical processor, for example
113  because it made a system call or tried to acquire a mutex.
114
115  Directly underneath each bar, a smaller bar or more commonly a fine
116  vertical line indicates an event occurring during its execution.
117  Some of these are related to garbage collection; most indicate that
118  a goroutine yielded its logical processor but then immediately resumed execution
119  on the same logical processor. Clicking on the event displays the stack trace
120  at the moment it occurred.
121</p>
122<p>
123  The causal relationships between spans of goroutine execution
124  can be displayed by clicking the Flow Events button at the top.
125</p>
126<p>
127  At the top ("STATS"), there are three additional timelines that
128  display statistical information.
129
130  "Goroutines" is a time series of the count of existing goroutines;
131  clicking on it displays their breakdown by state at that moment:
132  running, runnable, or waiting.
133
134  "Heap" is a time series of the amount of heap memory allocated (in orange)
135  and (in green) the allocation limit at which the next GC cycle will begin.
136
137  "Threads" shows the number of kernel threads in existence: there is
138  always one kernel thread per logical processor, and additional threads
139  are created for calls to non-Go code such as a system call or a
140  function written in C.
141</p>
142<p>
143  Above the event trace for the first logical processor are
144  traces for various runtime-internal events.
145
146  The "GC" bar shows when the garbage collector is running, and in which stage.
147  Garbage collection may temporarily affect all the logical processors
148  and the other metrics.
149
150  The "Network", "Timers", and "Syscalls" traces indicate events in
151  the runtime that cause goroutines to wake up.
152</p>
153<p>
154  The visualization allows you to navigate events at scales ranging from several
155  seconds to a handful of nanoseconds.
156
157  Consult the documentation for the Chromium <a href='https://www.chromium.org/developers/how-tos/trace-event-profiling-tool/'>Trace Event Profiling Tool<a/>
158  for help navigating the view.
159</p>
160
161<ul>
162<li><a href="/goroutines">Goroutine analysis</a></li>
163</ul>
164<p>
165  This view displays information about each set of goroutines that
166  shares the same main function.
167
168  Clicking on a main function shows links to the four types of
169  blocking profile (see below) applied to that subset of goroutines.
170
171  It also shows a table of specific goroutine instances, with various
172  execution statistics and a link to the event timeline for each one.
173
174  The timeline displays only the selected goroutine and any others it
175  interacts with via block/unblock events. (The timeline is
176  goroutine-oriented rather than logical processor-oriented.)
177</p>
178
179<h2>Profiles</h2>
180<p>
181  Each link below displays a global profile in zoomable graph form as
182  produced by <a href='https://go.dev/blog/pprof'>pprof</a>'s "web" command.
183
184  In addition there is a link to download the profile for offline
185  analysis with pprof.
186
187  All four profiles represent causes of delay that prevent a goroutine
188  from running on a logical processor: because it was waiting for the network,
189  for a synchronization operation on a mutex or channel, for a system call,
190  or for a logical processor to become available.
191</p>
192<ul>
193<li><a href="/io">Network blocking profile</a> (<a href="/io?raw=1" download="io.profile">⬇</a>)</li>
194<li><a href="/block">Synchronization blocking profile</a> (<a href="/block?raw=1" download="block.profile">⬇</a>)</li>
195<li><a href="/syscall">Syscall profile</a> (<a href="/syscall?raw=1" download="syscall.profile">⬇</a>)</li>
196<li><a href="/sched">Scheduler latency profile</a> (<a href="/sched?raw=1" download="sched.profile">⬇</a>)</li>
197</ul>
198
199<h2>User-defined tasks and regions</h2>
200<p>
201  The trace API allows a target program to annotate a <a
202  href='https://pkg.go.dev/runtime/trace#Region'>region</a> of code
203  within a goroutine, such as a key function, so that its performance
204  can be analyzed.
205
206  <a href='https://pkg.go.dev/runtime/trace#Log'>Log events</a> may be
207  associated with a region to record progress and relevant values.
208
209  The API also allows annotation of higher-level
210  <a href='https://pkg.go.dev/runtime/trace#Task'>tasks</a>,
211  which may involve work across many goroutines.
212</p>
213<p>
214  The links below display, for each region and task, a histogram of its execution times.
215
216  Each histogram bucket contains a sample trace that records the
217  sequence of events such as goroutine creations, log events, and
218  subregion start/end times.
219
220  For each task, you can click through to a logical-processor or
221  goroutine-oriented view showing the tasks and regions on the
222  timeline.
223
224  Such information may help uncover which steps in a region are
225  unexpectedly slow, or reveal relationships between the data values
226  logged in a request and its running time.
227</p>
228<ul>
229<li><a href="/usertasks">User-defined tasks</a></li>
230<li><a href="/userregions">User-defined regions</a></li>
231</ul>
232
233<h2>Garbage collection metrics</h2>
234<ul>
235<li><a href="/mmu">Minimum mutator utilization</a></li>
236</ul>
237<p>
238  This chart indicates the maximum GC pause time (the largest x value
239  for which y is zero), and more generally, the fraction of time that
240  the processors are available to application goroutines ("mutators"),
241  for any time window of a specified size, in the worst case.
242</p>
243</body>
244</html>
245`))
246
247type View struct {
248	Type   ViewType
249	Ranges []Range
250}
251
252type ViewType string
253
254const (
255	ViewProc   ViewType = "proc"
256	ViewThread ViewType = "thread"
257)
258
259func (v View) URL(rangeIdx int) string {
260	if rangeIdx < 0 {
261		return fmt.Sprintf("/trace?view=%s", v.Type)
262	}
263	return v.Ranges[rangeIdx].URL(v.Type)
264}
265
266type Range struct {
267	Name      string
268	Start     int
269	End       int
270	StartTime int64
271	EndTime   int64
272}
273
274func (r Range) URL(viewType ViewType) string {
275	return fmt.Sprintf("/trace?view=%s&start=%d&end=%d", viewType, r.Start, r.End)
276}
277
278func TraceHandler() http.Handler {
279	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
280		if err := r.ParseForm(); err != nil {
281			http.Error(w, err.Error(), http.StatusInternalServerError)
282			return
283		}
284		html := strings.ReplaceAll(templTrace, "{{PARAMS}}", r.Form.Encode())
285		w.Write([]byte(html))
286	})
287}
288
289// https://chromium.googlesource.com/catapult/+/9508452e18f130c98499cb4c4f1e1efaedee8962/tracing/docs/embedding-trace-viewer.md
290// This is almost verbatim copy of https://chromium-review.googlesource.com/c/catapult/+/2062938/2/tracing/bin/index.html
291var templTrace = `
292<html>
293<head>
294<script src="/static/webcomponents.min.js"></script>
295<script>
296'use strict';
297
298function onTraceViewerImportFail() {
299  document.addEventListener('DOMContentLoaded', function() {
300    document.body.textContent =
301    '/static/trace_viewer_full.html is missing. File a bug in https://golang.org/issue';
302  });
303}
304</script>
305
306<link rel="import" href="/static/trace_viewer_full.html"
307      onerror="onTraceViewerImportFail(event)">
308
309<style type="text/css">
310  html, body {
311    box-sizing: border-box;
312    overflow: hidden;
313    margin: 0px;
314    padding: 0;
315    width: 100%;
316    height: 100%;
317  }
318  #trace-viewer {
319    width: 100%;
320    height: 100%;
321  }
322  #trace-viewer:focus {
323    outline: none;
324  }
325</style>
326<script>
327'use strict';
328(function() {
329  var viewer;
330  var url;
331  var model;
332
333  function load() {
334    var req = new XMLHttpRequest();
335    var isBinary = /[.]gz$/.test(url) || /[.]zip$/.test(url);
336    req.overrideMimeType('text/plain; charset=x-user-defined');
337    req.open('GET', url, true);
338    if (isBinary)
339      req.responseType = 'arraybuffer';
340
341    req.onreadystatechange = function(event) {
342      if (req.readyState !== 4)
343        return;
344
345      window.setTimeout(function() {
346        if (req.status === 200)
347          onResult(isBinary ? req.response : req.responseText);
348        else
349          onResultFail(req.status);
350      }, 0);
351    };
352    req.send(null);
353  }
354
355  function onResultFail(err) {
356    var overlay = new tr.ui.b.Overlay();
357    overlay.textContent = err + ': ' + url + ' could not be loaded';
358    overlay.title = 'Failed to fetch data';
359    overlay.visible = true;
360  }
361
362  function onResult(result) {
363    model = new tr.Model();
364    var opts = new tr.importer.ImportOptions();
365    opts.shiftWorldToZero = false;
366    var i = new tr.importer.Import(model, opts);
367    var p = i.importTracesWithProgressDialog([result]);
368    p.then(onModelLoaded, onImportFail);
369  }
370
371  function onModelLoaded() {
372    viewer.model = model;
373    viewer.viewTitle = "trace";
374
375    if (!model || model.bounds.isEmpty)
376      return;
377    var sel = window.location.hash.substr(1);
378    if (sel === '')
379      return;
380    var parts = sel.split(':');
381    var range = new (tr.b.Range || tr.b.math.Range)();
382    range.addValue(parseFloat(parts[0]));
383    range.addValue(parseFloat(parts[1]));
384    viewer.trackView.viewport.interestRange.set(range);
385  }
386
387  function onImportFail(err) {
388    var overlay = new tr.ui.b.Overlay();
389    overlay.textContent = tr.b.normalizeException(err).message;
390    overlay.title = 'Import error';
391    overlay.visible = true;
392  }
393
394  document.addEventListener('WebComponentsReady', function() {
395    var container = document.createElement('track-view-container');
396    container.id = 'track_view_container';
397
398    viewer = document.createElement('tr-ui-timeline-view');
399    viewer.track_view_container = container;
400    Polymer.dom(viewer).appendChild(container);
401
402    viewer.id = 'trace-viewer';
403    viewer.globalMode = true;
404    Polymer.dom(document.body).appendChild(viewer);
405
406    url = '/jsontrace?{{PARAMS}}';
407    load();
408  });
409}());
410</script>
411</head>
412<body>
413</body>
414</html>
415`
416
417//go:embed static/trace_viewer_full.html static/webcomponents.min.js
418var staticContent embed.FS
419
420func StaticHandler() http.Handler {
421	return http.FileServer(http.FS(staticContent))
422}
423