Black Lives Matter. Support the Equal Justice Initiative.

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

Documentation: cmd/vendor/github.com/google/pprof/internal/driver

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

View as plain text