1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package driver
16
17 import (
18 "html/template"
19
20 "github.com/google/pprof/third_party/d3"
21 "github.com/google/pprof/third_party/d3flamegraph"
22 )
23
24
25 func 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 }
37 html, body {
38 height: 100%;
39 }
40 body {
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 }
47 a {
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 }
265 table {
266 border-spacing: 0px;
267 width: 100%;
268 padding-bottom: 1em;
269 white-space: nowrap;
270 }
271 table 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 }
274 table tr th {
275 position: sticky;
276 top: 0;
277 background-color: #ddd;
278 text-align: right;
279 padding: .3em .5em;
280 }
281 table 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.
446 function 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
660 function 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
709 function 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.
716 function 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
834 function 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
View as plain text