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