...

Source file src/pkg/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go

     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	
    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	// addTemplates adds a set of template definitions to templates.
    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