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