1// Copyright 2017 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package driver
16
17import (
18	"html/template"
19
20	"github.com/google/pprof/third_party/d3"
21	"github.com/google/pprof/third_party/d3flamegraph"
22)
23
24// addTemplates adds a set of template definitions to templates.
25func addTemplates(templates *template.Template) {
26	template.Must(templates.Parse(`{{define "d3script"}}` + d3.JSSource + `{{end}}`))
27	template.Must(templates.Parse(`{{define "d3flamegraphscript"}}` + d3flamegraph.JSSource + `{{end}}`))
28	template.Must(templates.Parse(`{{define "d3flamegraphcss"}}` + d3flamegraph.CSSSource + `{{end}}`))
29	template.Must(templates.Parse(`
30{{define "css"}}
31<style type="text/css">
32* {
33  margin: 0;
34  padding: 0;
35  box-sizing: border-box;
36}
37html, body {
38  height: 100%;
39}
40body {
41  font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
42  font-size: 13px;
43  line-height: 1.4;
44  display: flex;
45  flex-direction: column;
46}
47a {
48  color: #2a66d9;
49}
50.header {
51  display: flex;
52  align-items: center;
53  height: 44px;
54  min-height: 44px;
55  background-color: #eee;
56  color: #212121;
57  padding: 0 1rem;
58}
59.header > div {
60  margin: 0 0.125em;
61}
62.header .title h1 {
63  font-size: 1.75em;
64  margin-right: 1rem;
65  margin-bottom: 4px;
66}
67.header .title a {
68  color: #212121;
69  text-decoration: none;
70}
71.header .title a:hover {
72  text-decoration: underline;
73}
74.header .description {
75  width: 100%;
76  text-align: right;
77  white-space: nowrap;
78}
79@media screen and (max-width: 799px) {
80  .header input {
81    display: none;
82  }
83}
84#detailsbox {
85  display: none;
86  z-index: 1;
87  position: fixed;
88  top: 40px;
89  right: 20px;
90  background-color: #ffffff;
91  box-shadow: 0 1px 5px rgba(0,0,0,.3);
92  line-height: 24px;
93  padding: 1em;
94  text-align: left;
95}
96.header input {
97  background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' style='pointer-events:none;display:block;width:100%25;height:100%25;fill:%23757575'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61.0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E") no-repeat 4px center/20px 20px;
98  border: 1px solid #d1d2d3;
99  border-radius: 2px 0 0 2px;
100  padding: 0.25em;
101  padding-left: 28px;
102  margin-left: 1em;
103  font-family: 'Roboto', 'Noto', sans-serif;
104  font-size: 1em;
105  line-height: 24px;
106  color: #212121;
107}
108.downArrow {
109  border-top: .36em solid #ccc;
110  border-left: .36em solid transparent;
111  border-right: .36em solid transparent;
112  margin-bottom: .05em;
113  margin-left: .5em;
114  transition: border-top-color 200ms;
115}
116.menu-item {
117  height: 100%;
118  text-transform: uppercase;
119  font-family: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
120  position: relative;
121}
122.menu-item .menu-name:hover {
123  opacity: 0.75;
124}
125.menu-item .menu-name:hover .downArrow {
126  border-top-color: #666;
127}
128.menu-name {
129  height: 100%;
130  padding: 0 0.5em;
131  display: flex;
132  align-items: center;
133  justify-content: center;
134}
135.menu-name a {
136  text-decoration: none;
137  color: #212121;
138}
139.submenu {
140  display: none;
141  z-index: 1;
142  margin-top: -4px;
143  min-width: 10em;
144  position: absolute;
145  left: 0px;
146  background-color: white;
147  box-shadow: 0 1px 5px rgba(0,0,0,.3);
148  font-size: 100%;
149  text-transform: none;
150}
151.menu-item, .submenu {
152  user-select: none;
153  -moz-user-select: none;
154  -ms-user-select: none;
155  -webkit-user-select: none;
156}
157.submenu hr {
158  border: 0;
159  border-top: 2px solid #eee;
160}
161.submenu a {
162  display: block;
163  padding: .5em 1em;
164  text-decoration: none;
165}
166.submenu a:hover, .submenu a.active {
167  color: white;
168  background-color: #6b82d6;
169}
170.submenu a.disabled {
171  color: gray;
172  pointer-events: none;
173}
174.menu-check-mark {
175  position: absolute;
176  left: 2px;
177}
178.menu-delete-btn {
179  position: absolute;
180  right: 2px;
181}
182
183{{/* Used to disable events when a modal dialog is displayed */}}
184#dialog-overlay {
185  display: none;
186  position: fixed;
187  left: 0px;
188  top: 0px;
189  width: 100%;
190  height: 100%;
191  background-color: rgba(1,1,1,0.1);
192}
193
194.dialog {
195  {{/* Displayed centered horizontally near the top */}}
196  display: none;
197  position: fixed;
198  margin: 0px;
199  top: 60px;
200  left: 50%;
201  transform: translateX(-50%);
202
203  z-index: 3;
204  font-size: 125%;
205  background-color: #ffffff;
206  box-shadow: 0 1px 5px rgba(0,0,0,.3);
207}
208.dialog-header {
209  font-size: 120%;
210  border-bottom: 1px solid #CCCCCC;
211  width: 100%;
212  text-align: center;
213  background: #EEEEEE;
214  user-select: none;
215}
216.dialog-footer {
217  border-top: 1px solid #CCCCCC;
218  width: 100%;
219  text-align: right;
220  padding: 10px;
221}
222.dialog-error {
223  margin: 10px;
224  color: red;
225}
226.dialog input {
227  margin: 10px;
228  font-size: inherit;
229}
230.dialog button {
231  margin-left: 10px;
232  font-size: inherit;
233}
234#save-dialog, #delete-dialog {
235  width: 50%;
236  max-width: 20em;
237}
238#delete-prompt {
239  padding: 10px;
240}
241
242#content {
243  overflow-y: scroll;
244  padding: 1em;
245}
246#top {
247  overflow-y: scroll;
248}
249#graph {
250  overflow: hidden;
251}
252#graph svg {
253  width: 100%;
254  height: auto;
255  padding: 10px;
256}
257#content.source .filename {
258  margin-top: 0;
259  margin-bottom: 1em;
260  font-size: 120%;
261}
262#content.source pre {
263  margin-bottom: 3em;
264}
265table {
266  border-spacing: 0px;
267  width: 100%;
268  padding-bottom: 1em;
269  white-space: nowrap;
270}
271table thead {
272  font-family: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
273}
274table tr th {
275  position: sticky;
276  top: 0;
277  background-color: #ddd;
278  text-align: right;
279  padding: .3em .5em;
280}
281table tr td {
282  padding: .3em .5em;
283  text-align: right;
284}
285#top table tr th:nth-child(6),
286#top table tr th:nth-child(7),
287#top table tr td:nth-child(6),
288#top table tr td:nth-child(7) {
289  text-align: left;
290}
291#top table tr td:nth-child(6) {
292  width: 100%;
293  text-overflow: ellipsis;
294  overflow: hidden;
295  white-space: nowrap;
296}
297#flathdr1, #flathdr2, #cumhdr1, #cumhdr2, #namehdr {
298  cursor: ns-resize;
299}
300.hilite {
301  background-color: #ebf5fb;
302  font-weight: bold;
303}
304</style>
305{{end}}
306
307{{define "header"}}
308<div class="header">
309  <div class="title">
310    <h1><a href="./">pprof</a></h1>
311  </div>
312
313  <div id="view" class="menu-item">
314    <div class="menu-name">
315      View
316      <i class="downArrow"></i>
317    </div>
318    <div class="submenu">
319      <a title="{{.Help.top}}"  href="./top" id="topbtn">Top</a>
320      <a title="{{.Help.graph}}" href="./" id="graphbtn">Graph</a>
321      <a title="{{.Help.flamegraph}}" href="./flamegraph" id="flamegraph">Flame Graph</a>
322      <a title="{{.Help.peek}}" href="./peek" id="peek">Peek</a>
323      <a title="{{.Help.list}}" href="./source" id="list">Source</a>
324      <a title="{{.Help.disasm}}" href="./disasm" id="disasm">Disassemble</a>
325    </div>
326  </div>
327
328  {{$sampleLen := len .SampleTypes}}
329  {{if gt $sampleLen 1}}
330  <div id="sample" class="menu-item">
331    <div class="menu-name">
332      Sample
333      <i class="downArrow"></i>
334    </div>
335    <div class="submenu">
336      {{range .SampleTypes}}
337      <a href="?si={{.}}" id="{{.}}">{{.}}</a>
338      {{end}}
339    </div>
340  </div>
341  {{end}}
342
343  <div id="refine" class="menu-item">
344    <div class="menu-name">
345      Refine
346      <i class="downArrow"></i>
347    </div>
348    <div class="submenu">
349      <a title="{{.Help.focus}}" href="?" id="focus">Focus</a>
350      <a title="{{.Help.ignore}}" href="?" id="ignore">Ignore</a>
351      <a title="{{.Help.hide}}" href="?" id="hide">Hide</a>
352      <a title="{{.Help.show}}" href="?" id="show">Show</a>
353      <a title="{{.Help.show_from}}" href="?" id="show-from">Show from</a>
354      <hr>
355      <a title="{{.Help.reset}}" href="?">Reset</a>
356    </div>
357  </div>
358
359  <div id="config" class="menu-item">
360    <div class="menu-name">
361      Config
362      <i class="downArrow"></i>
363    </div>
364    <div class="submenu">
365      <a title="{{.Help.save_config}}" id="save-config">Save as ...</a>
366      <hr>
367      {{range .Configs}}
368        <a href="{{.URL}}">
369          {{if .Current}}<span class="menu-check-mark">✓</span>{{end}}
370          {{.Name}}
371          {{if .UserConfig}}<span class="menu-delete-btn" data-config={{.Name}}>��</span>{{end}}
372        </a>
373      {{end}}
374    </div>
375  </div>
376
377  <div id="download" class="menu-item">
378    <div class="menu-name">
379      <a href="./download">Download</a>
380    </div>
381  </div>
382
383  <div>
384    <input id="search" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
385  </div>
386
387  <div class="description">
388    <a title="{{.Help.details}}" href="#" id="details">{{.Title}}</a>
389    <div id="detailsbox">
390      {{range .Legend}}<div>{{.}}</div>{{end}}
391    </div>
392  </div>
393</div>
394
395<div id="dialog-overlay"></div>
396
397<div class="dialog" id="save-dialog">
398  <div class="dialog-header">Save options as</div>
399  <datalist id="config-list">
400    {{range .Configs}}{{if .UserConfig}}<option value="{{.Name}}" />{{end}}{{end}}
401  </datalist>
402  <input id="save-name" type="text" list="config-list" placeholder="New config" />
403  <div class="dialog-footer">
404    <span class="dialog-error" id="save-error"></span>
405    <button id="save-cancel">Cancel</button>
406    <button id="save-confirm">Save</button>
407  </div>
408</div>
409
410<div class="dialog" id="delete-dialog">
411  <div class="dialog-header" id="delete-dialog-title">Delete config</div>
412  <div id="delete-prompt"></div>
413  <div class="dialog-footer">
414    <span class="dialog-error" id="delete-error"></span>
415    <button id="delete-cancel">Cancel</button>
416    <button id="delete-confirm">Delete</button>
417  </div>
418</div>
419
420<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
421{{end}}
422
423{{define "graph" -}}
424<!DOCTYPE html>
425<html>
426<head>
427  <meta charset="utf-8">
428  <title>{{.Title}}</title>
429  {{template "css" .}}
430</head>
431<body>
432  {{template "header" .}}
433  <div id="graph">
434    {{.HTMLBody}}
435  </div>
436  {{template "script" .}}
437  <script>viewer(new URL(window.location.href), {{.Nodes}});</script>
438</body>
439</html>
440{{end}}
441
442{{define "script"}}
443<script>
444// Make svg pannable and zoomable.
445// Call clickHandler(t) if a click event is caught by the pan event handlers.
446function initPanAndZoom(svg, clickHandler) {
447  'use strict';
448
449  // Current mouse/touch handling mode
450  const IDLE = 0;
451  const MOUSEPAN = 1;
452  const TOUCHPAN = 2;
453  const TOUCHZOOM = 3;
454  let mode = IDLE;
455
456  // State needed to implement zooming.
457  let currentScale = 1.0;
458  const initWidth = svg.viewBox.baseVal.width;
459  const initHeight = svg.viewBox.baseVal.height;
460
461  // State needed to implement panning.
462  let panLastX = 0;      // Last event X coordinate
463  let panLastY = 0;      // Last event Y coordinate
464  let moved = false;     // Have we seen significant movement
465  let touchid = null;    // Current touch identifier
466
467  // State needed for pinch zooming
468  let touchid2 = null;     // Second id for pinch zooming
469  let initGap = 1.0;       // Starting gap between two touches
470  let initScale = 1.0;     // currentScale when pinch zoom started
471  let centerPoint = null;  // Center point for scaling
472
473  // Convert event coordinates to svg coordinates.
474  function toSvg(x, y) {
475    const p = svg.createSVGPoint();
476    p.x = x;
477    p.y = y;
478    let m = svg.getCTM();
479    if (m == null) m = svg.getScreenCTM(); // Firefox workaround.
480    return p.matrixTransform(m.inverse());
481  }
482
483  // Change the scaling for the svg to s, keeping the point denoted
484  // by u (in svg coordinates]) fixed at the same screen location.
485  function rescale(s, u) {
486    // Limit to a good range.
487    if (s < 0.2) s = 0.2;
488    if (s > 10.0) s = 10.0;
489
490    currentScale = s;
491
492    // svg.viewBox defines the visible portion of the user coordinate
493    // system.  So to magnify by s, divide the visible portion by s,
494    // which will then be stretched to fit the viewport.
495    const vb = svg.viewBox;
496    const w1 = vb.baseVal.width;
497    const w2 = initWidth / s;
498    const h1 = vb.baseVal.height;
499    const h2 = initHeight / s;
500    vb.baseVal.width = w2;
501    vb.baseVal.height = h2;
502
503    // We also want to adjust vb.baseVal.x so that u.x remains at same
504    // screen X coordinate.  In other words, want to change it from x1 to x2
505    // so that:
506    //     (u.x - x1) / w1 = (u.x - x2) / w2
507    // Simplifying that, we get
508    //     (u.x - x1) * (w2 / w1) = u.x - x2
509    //     x2 = u.x - (u.x - x1) * (w2 / w1)
510    vb.baseVal.x = u.x - (u.x - vb.baseVal.x) * (w2 / w1);
511    vb.baseVal.y = u.y - (u.y - vb.baseVal.y) * (h2 / h1);
512  }
513
514  function handleWheel(e) {
515    if (e.deltaY == 0) return;
516    // Change scale factor by 1.1 or 1/1.1
517    rescale(currentScale * (e.deltaY < 0 ? 1.1 : (1/1.1)),
518            toSvg(e.offsetX, e.offsetY));
519  }
520
521  function setMode(m) {
522    mode = m;
523    touchid = null;
524    touchid2 = null;
525  }
526
527  function panStart(x, y) {
528    moved = false;
529    panLastX = x;
530    panLastY = y;
531  }
532
533  function panMove(x, y) {
534    let dx = x - panLastX;
535    let dy = y - panLastY;
536    if (Math.abs(dx) <= 2 && Math.abs(dy) <= 2) return; // Ignore tiny moves
537
538    moved = true;
539    panLastX = x;
540    panLastY = y;
541
542    // Firefox workaround: get dimensions from parentNode.
543    const swidth = svg.clientWidth || svg.parentNode.clientWidth;
544    const sheight = svg.clientHeight || svg.parentNode.clientHeight;
545
546    // Convert deltas from screen space to svg space.
547    dx *= (svg.viewBox.baseVal.width / swidth);
548    dy *= (svg.viewBox.baseVal.height / sheight);
549
550    svg.viewBox.baseVal.x -= dx;
551    svg.viewBox.baseVal.y -= dy;
552  }
553
554  function handleScanStart(e) {
555    if (e.button != 0) return; // Do not catch right-clicks etc.
556    setMode(MOUSEPAN);
557    panStart(e.clientX, e.clientY);
558    e.preventDefault();
559    svg.addEventListener('mousemove', handleScanMove);
560  }
561
562  function handleScanMove(e) {
563    if (e.buttons == 0) {
564      // Missed an end event, perhaps because mouse moved outside window.
565      setMode(IDLE);
566      svg.removeEventListener('mousemove', handleScanMove);
567      return;
568    }
569    if (mode == MOUSEPAN) panMove(e.clientX, e.clientY);
570  }
571
572  function handleScanEnd(e) {
573    if (mode == MOUSEPAN) panMove(e.clientX, e.clientY);
574    setMode(IDLE);
575    svg.removeEventListener('mousemove', handleScanMove);
576    if (!moved) clickHandler(e.target);
577  }
578
579  // Find touch object with specified identifier.
580  function findTouch(tlist, id) {
581    for (const t of tlist) {
582      if (t.identifier == id) return t;
583    }
584    return null;
585  }
586
587  // Return distance between two touch points
588  function touchGap(t1, t2) {
589    const dx = t1.clientX - t2.clientX;
590    const dy = t1.clientY - t2.clientY;
591    return Math.hypot(dx, dy);
592  }
593
594  function handleTouchStart(e) {
595    if (mode == IDLE && e.changedTouches.length == 1) {
596      // Start touch based panning
597      const t = e.changedTouches[0];
598      setMode(TOUCHPAN);
599      touchid = t.identifier;
600      panStart(t.clientX, t.clientY);
601      e.preventDefault();
602    } else if (mode == TOUCHPAN && e.touches.length == 2) {
603      // Start pinch zooming
604      setMode(TOUCHZOOM);
605      const t1 = e.touches[0];
606      const t2 = e.touches[1];
607      touchid = t1.identifier;
608      touchid2 = t2.identifier;
609      initScale = currentScale;
610      initGap = touchGap(t1, t2);
611      centerPoint = toSvg((t1.clientX + t2.clientX) / 2,
612                          (t1.clientY + t2.clientY) / 2);
613      e.preventDefault();
614    }
615  }
616
617  function handleTouchMove(e) {
618    if (mode == TOUCHPAN) {
619      const t = findTouch(e.changedTouches, touchid);
620      if (t == null) return;
621      if (e.touches.length != 1) {
622        setMode(IDLE);
623        return;
624      }
625      panMove(t.clientX, t.clientY);
626      e.preventDefault();
627    } else if (mode == TOUCHZOOM) {
628      // Get two touches; new gap; rescale to ratio.
629      const t1 = findTouch(e.touches, touchid);
630      const t2 = findTouch(e.touches, touchid2);
631      if (t1 == null || t2 == null) return;
632      const gap = touchGap(t1, t2);
633      rescale(initScale * gap / initGap, centerPoint);
634      e.preventDefault();
635    }
636  }
637
638  function handleTouchEnd(e) {
639    if (mode == TOUCHPAN) {
640      const t = findTouch(e.changedTouches, touchid);
641      if (t == null) return;
642      panMove(t.clientX, t.clientY);
643      setMode(IDLE);
644      e.preventDefault();
645      if (!moved) clickHandler(t.target);
646    } else if (mode == TOUCHZOOM) {
647      setMode(IDLE);
648      e.preventDefault();
649    }
650  }
651
652  svg.addEventListener('mousedown', handleScanStart);
653  svg.addEventListener('mouseup', handleScanEnd);
654  svg.addEventListener('touchstart', handleTouchStart);
655  svg.addEventListener('touchmove', handleTouchMove);
656  svg.addEventListener('touchend', handleTouchEnd);
657  svg.addEventListener('wheel', handleWheel, true);
658}
659
660function initMenus() {
661  'use strict';
662
663  let activeMenu = null;
664  let activeMenuHdr = null;
665
666  function cancelActiveMenu() {
667    if (activeMenu == null) return;
668    activeMenu.style.display = 'none';
669    activeMenu = null;
670    activeMenuHdr = null;
671  }
672
673  // Set click handlers on every menu header.
674  for (const menu of document.getElementsByClassName('submenu')) {
675    const hdr = menu.parentElement;
676    if (hdr == null) return;
677    if (hdr.classList.contains('disabled')) return;
678    function showMenu(e) {
679      // menu is a child of hdr, so this event can fire for clicks
680      // inside menu. Ignore such clicks.
681      if (e.target.parentElement != hdr) return;
682      activeMenu = menu;
683      activeMenuHdr = hdr;
684      menu.style.display = 'block';
685    }
686    hdr.addEventListener('mousedown', showMenu);
687    hdr.addEventListener('touchstart', showMenu);
688  }
689
690  // If there is an active menu and a down event outside, retract the menu.
691  for (const t of ['mousedown', 'touchstart']) {
692    document.addEventListener(t, (e) => {
693      // Note: to avoid unnecessary flicker, if the down event is inside
694      // the active menu header, do not retract the menu.
695      if (activeMenuHdr != e.target.closest('.menu-item')) {
696        cancelActiveMenu();
697      }
698    }, { passive: true, capture: true });
699  }
700
701  // If there is an active menu and an up event inside, retract the menu.
702  document.addEventListener('mouseup', (e) => {
703    if (activeMenu == e.target.closest('.submenu')) {
704      cancelActiveMenu();
705    }
706  }, { passive: true, capture: true });
707}
708
709function sendURL(method, url, done) {
710  fetch(url.toString(), {method: method})
711      .then((response) => { done(response.ok); })
712      .catch((error) => { done(false); });
713}
714
715// Initialize handlers for saving/loading configurations.
716function initConfigManager() {
717  'use strict';
718
719  // Initialize various elements.
720  function elem(id) {
721    const result = document.getElementById(id);
722    if (!result) console.warn('element ' + id + ' not found');
723    return result;
724  }
725  const overlay = elem('dialog-overlay');
726  const saveDialog = elem('save-dialog');
727  const saveInput = elem('save-name');
728  const saveError = elem('save-error');
729  const delDialog = elem('delete-dialog');
730  const delPrompt = elem('delete-prompt');
731  const delError = elem('delete-error');
732
733  let currentDialog = null;
734  let currentDeleteTarget = null;
735
736  function showDialog(dialog) {
737    if (currentDialog != null) {
738      overlay.style.display = 'none';
739      currentDialog.style.display = 'none';
740    }
741    currentDialog = dialog;
742    if (dialog != null) {
743      overlay.style.display = 'block';
744      dialog.style.display = 'block';
745    }
746  }
747
748  function cancelDialog(e) {
749    showDialog(null);
750  }
751
752  // Show dialog for saving the current config.
753  function showSaveDialog(e) {
754    saveError.innerText = '';
755    showDialog(saveDialog);
756    saveInput.focus();
757  }
758
759  // Commit save config.
760  function commitSave(e) {
761    const name = saveInput.value;
762    const url = new URL(document.URL);
763    // Set path relative to existing path.
764    url.pathname = new URL('./saveconfig', document.URL).pathname;
765    url.searchParams.set('config', name);
766    saveError.innerText = '';
767    sendURL('POST', url, (ok) => {
768      if (!ok) {
769        saveError.innerText = 'Save failed';
770      } else {
771        showDialog(null);
772        location.reload();  // Reload to show updated config menu
773      }
774    });
775  }
776
777  function handleSaveInputKey(e) {
778    if (e.key === 'Enter') commitSave(e);
779  }
780
781  function deleteConfig(e, elem) {
782    e.preventDefault();
783    const config = elem.dataset.config;
784    delPrompt.innerText = 'Delete ' + config + '?';
785    currentDeleteTarget = elem;
786    showDialog(delDialog);
787  }
788
789  function commitDelete(e, elem) {
790    if (!currentDeleteTarget) return;
791    const config = currentDeleteTarget.dataset.config;
792    const url = new URL('./deleteconfig', document.URL);
793    url.searchParams.set('config', config);
794    delError.innerText = '';
795    sendURL('DELETE', url, (ok) => {
796      if (!ok) {
797        delError.innerText = 'Delete failed';
798        return;
799      }
800      showDialog(null);
801      // Remove menu entry for this config.
802      if (currentDeleteTarget && currentDeleteTarget.parentElement) {
803        currentDeleteTarget.parentElement.remove();
804      }
805    });
806  }
807
808  // Bind event on elem to fn.
809  function bind(event, elem, fn) {
810    if (elem == null) return;
811    elem.addEventListener(event, fn);
812    if (event == 'click') {
813      // Also enable via touch.
814      elem.addEventListener('touchstart', fn);
815    }
816  }
817
818  bind('click', elem('save-config'), showSaveDialog);
819  bind('click', elem('save-cancel'), cancelDialog);
820  bind('click', elem('save-confirm'), commitSave);
821  bind('keydown', saveInput, handleSaveInputKey);
822
823  bind('click', elem('delete-cancel'), cancelDialog);
824  bind('click', elem('delete-confirm'), commitDelete);
825
826  // Activate deletion button for all config entries in menu.
827  for (const del of Array.from(document.getElementsByClassName('menu-delete-btn'))) {
828    bind('click', del, (e) => {
829      deleteConfig(e, del);
830    });
831  }
832}
833
834function viewer(baseUrl, nodes) {
835  'use strict';
836
837  // Elements
838  const search = document.getElementById('search');
839  const graph0 = document.getElementById('graph0');
840  const svg = (graph0 == null ? null : graph0.parentElement);
841  const toptable = document.getElementById('toptable');
842
843  let regexpActive = false;
844  let selected = new Map();
845  let origFill = new Map();
846  let searchAlarm = null;
847  let buttonsEnabled = true;
848
849  function handleDetails(e) {
850    e.preventDefault();
851    const detailsText = document.getElementById('detailsbox');
852    if (detailsText != null) {
853      if (detailsText.style.display === 'block') {
854        detailsText.style.display = 'none';
855      } else {
856        detailsText.style.display = 'block';
857      }
858    }
859  }
860
861  function handleKey(e) {
862    if (e.keyCode != 13) return;
863    setHrefParams(window.location, function (params) {
864      params.set('f', search.value);
865    });
866    e.preventDefault();
867  }
868
869  function handleSearch() {
870    // Delay expensive processing so a flurry of key strokes is handled once.
871    if (searchAlarm != null) {
872      clearTimeout(searchAlarm);
873    }
874    searchAlarm = setTimeout(selectMatching, 300);
875
876    regexpActive = true;
877    updateButtons();
878  }
879
880  function selectMatching() {
881    searchAlarm = null;
882    let re = null;
883    if (search.value != '') {
884      try {
885        re = new RegExp(search.value);
886      } catch (e) {
887        // TODO: Display error state in search box
888        return;
889      }
890    }
891
892    function match(text) {
893      return re != null && re.test(text);
894    }
895
896    // drop currently selected items that do not match re.
897    selected.forEach(function(v, n) {
898      if (!match(nodes[n])) {
899        unselect(n, document.getElementById('node' + n));
900      }
901    })
902
903    // add matching items that are not currently selected.
904    if (nodes) {
905      for (let n = 0; n < nodes.length; n++) {
906        if (!selected.has(n) && match(nodes[n])) {
907          select(n, document.getElementById('node' + n));
908        }
909      }
910    }
911
912    updateButtons();
913  }
914
915  function toggleSvgSelect(elem) {
916    // Walk up to immediate child of graph0
917    while (elem != null && elem.parentElement != graph0) {
918      elem = elem.parentElement;
919    }
920    if (!elem) return;
921
922    // Disable regexp mode.
923    regexpActive = false;
924
925    const n = nodeId(elem);
926    if (n < 0) return;
927    if (selected.has(n)) {
928      unselect(n, elem);
929    } else {
930      select(n, elem);
931    }
932    updateButtons();
933  }
934
935  function unselect(n, elem) {
936    if (elem == null) return;
937    selected.delete(n);
938    setBackground(elem, false);
939  }
940
941  function select(n, elem) {
942    if (elem == null) return;
943    selected.set(n, true);
944    setBackground(elem, true);
945  }
946
947  function nodeId(elem) {
948    const id = elem.id;
949    if (!id) return -1;
950    if (!id.startsWith('node')) return -1;
951    const n = parseInt(id.slice(4), 10);
952    if (isNaN(n)) return -1;
953    if (n < 0 || n >= nodes.length) return -1;
954    return n;
955  }
956
957  function setBackground(elem, set) {
958    // Handle table row highlighting.
959    if (elem.nodeName == 'TR') {
960      elem.classList.toggle('hilite', set);
961      return;
962    }
963
964    // Handle svg element highlighting.
965    const p = findPolygon(elem);
966    if (p != null) {
967      if (set) {
968        origFill.set(p, p.style.fill);
969        p.style.fill = '#ccccff';
970      } else if (origFill.has(p)) {
971        p.style.fill = origFill.get(p);
972      }
973    }
974  }
975
976  function findPolygon(elem) {
977    if (elem.localName == 'polygon') return elem;
978    for (const c of elem.children) {
979      const p = findPolygon(c);
980      if (p != null) return p;
981    }
982    return null;
983  }
984
985  // convert a string to a regexp that matches that string.
986  function quotemeta(str) {
987    return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1');
988  }
989
990  function setSampleIndexLink(id) {
991    const elem = document.getElementById(id);
992    if (elem != null) {
993      setHrefParams(elem, function (params) {
994        params.set("si", id);
995      });
996    }
997  }
998
999  // Update id's href to reflect current selection whenever it is
1000  // liable to be followed.
1001  function makeSearchLinkDynamic(id) {
1002    const elem = document.getElementById(id);
1003    if (elem == null) return;
1004
1005    // Most links copy current selection into the 'f' parameter,
1006    // but Refine menu links are different.
1007    let param = 'f';
1008    if (id == 'ignore') param = 'i';
1009    if (id == 'hide') param = 'h';
1010    if (id == 'show') param = 's';
1011    if (id == 'show-from') param = 'sf';
1012
1013    // We update on mouseenter so middle-click/right-click work properly.
1014    elem.addEventListener('mouseenter', updater);
1015    elem.addEventListener('touchstart', updater);
1016
1017    function updater() {
1018      // The selection can be in one of two modes: regexp-based or
1019      // list-based.  Construct regular expression depending on mode.
1020      let re = regexpActive
1021        ? search.value
1022        : Array.from(selected.keys()).map(key => quotemeta(nodes[key])).join('|');
1023
1024      setHrefParams(elem, function (params) {
1025        if (re != '') {
1026          // For focus/show/show-from, forget old parameter. For others, add to re.
1027          if (param != 'f' && param != 's' && param != 'sf' && params.has(param)) {
1028            const old = params.get(param);
1029            if (old != '') {
1030              re += '|' + old;
1031            }
1032          }
1033          params.set(param, re);
1034        } else {
1035          params.delete(param);
1036        }
1037      });
1038    }
1039  }
1040
1041  function setHrefParams(elem, paramSetter) {
1042    let url = new URL(elem.href);
1043    url.hash = '';
1044
1045    // Copy params from this page's URL.
1046    const params = url.searchParams;
1047    for (const p of new URLSearchParams(window.location.search)) {
1048      params.set(p[0], p[1]);
1049    }
1050
1051    // Give the params to the setter to modify.
1052    paramSetter(params);
1053
1054    elem.href = url.toString();
1055  }
1056
1057  function handleTopClick(e) {
1058    // Walk back until we find TR and then get the Name column (index 5)
1059    let elem = e.target;
1060    while (elem != null && elem.nodeName != 'TR') {
1061      elem = elem.parentElement;
1062    }
1063    if (elem == null || elem.children.length < 6) return;
1064
1065    e.preventDefault();
1066    const tr = elem;
1067    const td = elem.children[5];
1068    if (td.nodeName != 'TD') return;
1069    const name = td.innerText;
1070    const index = nodes.indexOf(name);
1071    if (index < 0) return;
1072
1073    // Disable regexp mode.
1074    regexpActive = false;
1075
1076    if (selected.has(index)) {
1077      unselect(index, elem);
1078    } else {
1079      select(index, elem);
1080    }
1081    updateButtons();
1082  }
1083
1084  function updateButtons() {
1085    const enable = (search.value != '' || selected.size != 0);
1086    if (buttonsEnabled == enable) return;
1087    buttonsEnabled = enable;
1088    for (const id of ['focus', 'ignore', 'hide', 'show', 'show-from']) {
1089      const link = document.getElementById(id);
1090      if (link != null) {
1091        link.classList.toggle('disabled', !enable);
1092      }
1093    }
1094  }
1095
1096  // Initialize button states
1097  updateButtons();
1098
1099  // Setup event handlers
1100  initMenus();
1101  if (svg != null) {
1102    initPanAndZoom(svg, toggleSvgSelect);
1103  }
1104  if (toptable != null) {
1105    toptable.addEventListener('mousedown', handleTopClick);
1106    toptable.addEventListener('touchstart', handleTopClick);
1107  }
1108
1109  const ids = ['topbtn', 'graphbtn', 'flamegraph', 'peek', 'list', 'disasm',
1110               'focus', 'ignore', 'hide', 'show', 'show-from'];
1111  ids.forEach(makeSearchLinkDynamic);
1112
1113  const sampleIDs = [{{range .SampleTypes}}'{{.}}', {{end}}];
1114  sampleIDs.forEach(setSampleIndexLink);
1115
1116  // Bind action to button with specified id.
1117  function addAction(id, action) {
1118    const btn = document.getElementById(id);
1119    if (btn != null) {
1120      btn.addEventListener('click', action);
1121      btn.addEventListener('touchstart', action);
1122    }
1123  }
1124
1125  addAction('details', handleDetails);
1126  initConfigManager();
1127
1128  search.addEventListener('input', handleSearch);
1129  search.addEventListener('keydown', handleKey);
1130
1131  // Give initial focus to main container so it can be scrolled using keys.
1132  const main = document.getElementById('bodycontainer');
1133  if (main) {
1134    main.focus();
1135  }
1136}
1137</script>
1138{{end}}
1139
1140{{define "top" -}}
1141<!DOCTYPE html>
1142<html>
1143<head>
1144  <meta charset="utf-8">
1145  <title>{{.Title}}</title>
1146  {{template "css" .}}
1147  <style type="text/css">
1148  </style>
1149</head>
1150<body>
1151  {{template "header" .}}
1152  <div id="top">
1153    <table id="toptable">
1154      <thead>
1155        <tr>
1156          <th id="flathdr1">Flat</th>
1157          <th id="flathdr2">Flat%</th>
1158          <th>Sum%</th>
1159          <th id="cumhdr1">Cum</th>
1160          <th id="cumhdr2">Cum%</th>
1161          <th id="namehdr">Name</th>
1162          <th>Inlined?</th>
1163        </tr>
1164      </thead>
1165      <tbody id="rows"></tbody>
1166    </table>
1167  </div>
1168  {{template "script" .}}
1169  <script>
1170    function makeTopTable(total, entries) {
1171      const rows = document.getElementById('rows');
1172      if (rows == null) return;
1173
1174      // Store initial index in each entry so we have stable node ids for selection.
1175      for (let i = 0; i < entries.length; i++) {
1176        entries[i].Id = 'node' + i;
1177      }
1178
1179      // Which column are we currently sorted by and in what order?
1180      let currentColumn = '';
1181      let descending = false;
1182      sortBy('Flat');
1183
1184      function sortBy(column) {
1185        // Update sort criteria
1186        if (column == currentColumn) {
1187          descending = !descending; // Reverse order
1188        } else {
1189          currentColumn = column;
1190          descending = (column != 'Name');
1191        }
1192
1193        // Sort according to current criteria.
1194        function cmp(a, b) {
1195          const av = a[currentColumn];
1196          const bv = b[currentColumn];
1197          if (av < bv) return -1;
1198          if (av > bv) return +1;
1199          return 0;
1200        }
1201        entries.sort(cmp);
1202        if (descending) entries.reverse();
1203
1204        function addCell(tr, val) {
1205          const td = document.createElement('td');
1206          td.textContent = val;
1207          tr.appendChild(td);
1208        }
1209
1210        function percent(v) {
1211          return (v * 100.0 / total).toFixed(2) + '%';
1212        }
1213
1214        // Generate rows
1215        const fragment = document.createDocumentFragment();
1216        let sum = 0;
1217        for (const row of entries) {
1218          const tr = document.createElement('tr');
1219          tr.id = row.Id;
1220          sum += row.Flat;
1221          addCell(tr, row.FlatFormat);
1222          addCell(tr, percent(row.Flat));
1223          addCell(tr, percent(sum));
1224          addCell(tr, row.CumFormat);
1225          addCell(tr, percent(row.Cum));
1226          addCell(tr, row.Name);
1227          addCell(tr, row.InlineLabel);
1228          fragment.appendChild(tr);
1229        }
1230
1231        rows.textContent = ''; // Remove old rows
1232        rows.appendChild(fragment);
1233      }
1234
1235      // Make different column headers trigger sorting.
1236      function bindSort(id, column) {
1237        const hdr = document.getElementById(id);
1238        if (hdr == null) return;
1239        const fn = function() { sortBy(column) };
1240        hdr.addEventListener('click', fn);
1241        hdr.addEventListener('touch', fn);
1242      }
1243      bindSort('flathdr1', 'Flat');
1244      bindSort('flathdr2', 'Flat');
1245      bindSort('cumhdr1', 'Cum');
1246      bindSort('cumhdr2', 'Cum');
1247      bindSort('namehdr', 'Name');
1248    }
1249
1250    viewer(new URL(window.location.href), {{.Nodes}});
1251    makeTopTable({{.Total}}, {{.Top}});
1252  </script>
1253</body>
1254</html>
1255{{end}}
1256
1257{{define "sourcelisting" -}}
1258<!DOCTYPE html>
1259<html>
1260<head>
1261  <meta charset="utf-8">
1262  <title>{{.Title}}</title>
1263  {{template "css" .}}
1264  {{template "weblistcss" .}}
1265  {{template "weblistjs" .}}
1266</head>
1267<body>
1268  {{template "header" .}}
1269  <div id="content" class="source">
1270    {{.HTMLBody}}
1271  </div>
1272  {{template "script" .}}
1273  <script>viewer(new URL(window.location.href), null);</script>
1274</body>
1275</html>
1276{{end}}
1277
1278{{define "plaintext" -}}
1279<!DOCTYPE html>
1280<html>
1281<head>
1282  <meta charset="utf-8">
1283  <title>{{.Title}}</title>
1284  {{template "css" .}}
1285</head>
1286<body>
1287  {{template "header" .}}
1288  <div id="content">
1289    <pre>
1290      {{.TextBody}}
1291    </pre>
1292  </div>
1293  {{template "script" .}}
1294  <script>viewer(new URL(window.location.href), null);</script>
1295</body>
1296</html>
1297{{end}}
1298
1299{{define "flamegraph" -}}
1300<!DOCTYPE html>
1301<html>
1302<head>
1303  <meta charset="utf-8">
1304  <title>{{.Title}}</title>
1305  {{template "css" .}}
1306  <style type="text/css">{{template "d3flamegraphcss" .}}</style>
1307  <style type="text/css">
1308    .flamegraph-content {
1309      width: 90%;
1310      min-width: 80%;
1311      margin-left: 5%;
1312    }
1313    .flamegraph-details {
1314      height: 1.2em;
1315      width: 90%;
1316      min-width: 90%;
1317      margin-left: 5%;
1318      padding: 15px 0 35px;
1319    }
1320  </style>
1321</head>
1322<body>
1323  {{template "header" .}}
1324  <div id="bodycontainer">
1325    <div id="flamegraphdetails" class="flamegraph-details"></div>
1326    <div class="flamegraph-content">
1327      <div id="chart"></div>
1328    </div>
1329  </div>
1330  {{template "script" .}}
1331  <script>viewer(new URL(window.location.href), {{.Nodes}});</script>
1332  <script>{{template "d3script" .}}</script>
1333  <script>{{template "d3flamegraphscript" .}}</script>
1334  <script>
1335    var data = {{.FlameGraph}};
1336
1337    var width = document.getElementById('chart').clientWidth;
1338
1339    var flameGraph = d3.flamegraph()
1340      .width(width)
1341      .cellHeight(18)
1342      .minFrameSize(1)
1343      .transitionDuration(750)
1344      .transitionEase(d3.easeCubic)
1345      .inverted(true)
1346      .sort(true)
1347      .title('')
1348      .tooltip(false)
1349      .details(document.getElementById('flamegraphdetails'));
1350
1351    // <full name> (percentage, value)
1352    flameGraph.label((d) => d.data.f + ' (' + d.data.p + ', ' + d.data.l + ')');
1353
1354    (function(flameGraph) {
1355      var oldColorMapper = flameGraph.color();
1356      function colorMapper(d) {
1357        // Hack to force default color mapper to use 'warm' color scheme by not passing libtype
1358        const { data, highlight } = d;
1359        return oldColorMapper({ data: { n: data.n }, highlight });
1360      }
1361
1362      flameGraph.color(colorMapper);
1363    }(flameGraph));
1364
1365    d3.select('#chart')
1366      .datum(data)
1367      .call(flameGraph);
1368
1369    function clear() {
1370      flameGraph.clear();
1371    }
1372
1373    function resetZoom() {
1374      flameGraph.resetZoom();
1375    }
1376
1377    window.addEventListener('resize', function() {
1378      var width = document.getElementById('chart').clientWidth;
1379      var graphs = document.getElementsByClassName('d3-flame-graph');
1380      if (graphs.length > 0) {
1381        graphs[0].setAttribute('width', width);
1382      }
1383      flameGraph.width(width);
1384      flameGraph.resetZoom();
1385    }, true);
1386
1387    var search = document.getElementById('search');
1388    var searchAlarm = null;
1389
1390    function selectMatching() {
1391      searchAlarm = null;
1392
1393      if (search.value != '') {
1394        flameGraph.search(search.value);
1395      } else {
1396        flameGraph.clear();
1397      }
1398    }
1399
1400    function handleSearch() {
1401      // Delay expensive processing so a flurry of key strokes is handled once.
1402      if (searchAlarm != null) {
1403        clearTimeout(searchAlarm);
1404      }
1405      searchAlarm = setTimeout(selectMatching, 300);
1406    }
1407
1408    search.addEventListener('input', handleSearch);
1409  </script>
1410</body>
1411</html>
1412{{end}}
1413`))
1414}
1415