Source file src/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 (
    18  	"html/template"
    19  
    20  	"github.com/google/pprof/third_party/d3"
    21  	"github.com/google/pprof/third_party/d3flamegraph"
    22  )
    23  
    24  // addTemplates adds a set of template definitions to templates.
    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