aboutsummaryrefslogtreecommitdiff
path: root/src/cmd/vendor/github.com/google/pprof/internal/driver
diff options
context:
space:
mode:
authorCherry Mui <cherryyz@google.com>2022-11-22 12:39:05 -0500
committerCherry Mui <cherryyz@google.com>2022-11-22 18:58:12 +0000
commitbb917bd1b212dc8fff3852fa164667cd06b9f653 (patch)
tree36785e58dd168eca8d7001b48dbbed10bdadfe91 /src/cmd/vendor/github.com/google/pprof/internal/driver
parent21015cf6baed45a1e7c3d1a0dfe34c778140344f (diff)
downloadgo-bb917bd1b212dc8fff3852fa164667cd06b9f653.tar.xz
cmd/vendor: update vendored github.com/google/pprof for Go 1.20 release
The Go 1.20 code freeze has recently started. This is a time to update the vendored copy. Done by cd GOROOT/src/cmd go get -d github.com/google/pprof@latest go mod tidy go mod vendor For #36905. Change-Id: Iaec604c66ea8f4b7638a31bdb77d6dd56966e38a Reviewed-on: https://go-review.googlesource.com/c/go/+/452815 Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Run-TryBot: Cherry Mui <cherryyz@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Diffstat (limited to 'src/cmd/vendor/github.com/google/pprof/internal/driver')
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/cli.go3
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go25
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go21
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/html/common.css1
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/html/common.js57
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/html/header.html1
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.css80
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.html32
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.js524
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go3
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/settings.go5
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/stacks.go58
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/tagroot.go4
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go3
-rw-r--r--src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go13
15 files changed, 797 insertions, 33 deletions
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/cli.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/cli.go
index 237cc33233..a9cae92d1b 100644
--- a/src/cmd/vendor/github.com/google/pprof/internal/driver/cli.go
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/cli.go
@@ -363,5 +363,6 @@ var usageMsgVars = "\n\n" +
" PPROF_TOOLS Search path for object-level tools\n" +
" PPROF_BINARY_PATH Search path for local binary files\n" +
" default: $HOME/pprof/binaries\n" +
- " searches $name, $path, $buildid/$name, $path/$buildid\n" +
+ " searches $buildid/$name, $buildid/*, $path/$buildid,\n" +
+ " ${buildid:0:2}/${buildid:2}.debug, $name, $path\n" +
" * On Windows, %USERPROFILE% is used instead of $HOME"
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go
index 6a1e64c600..27681c540f 100644
--- a/src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go
@@ -59,9 +59,8 @@ func PProf(eo *plugin.Options) error {
return interactive(p, o)
}
+// generateRawReport is allowed to modify p.
func generateRawReport(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) (*command, *report.Report, error) {
- p = p.Copy() // Prevent modification to the incoming profile.
-
// Identify units of numeric tags in profile.
numLabelUnits := identifyNumLabelUnits(p, o.UI)
@@ -110,6 +109,7 @@ func generateRawReport(p *profile.Profile, cmd []string, cfg config, o *plugin.O
return c, rpt, nil
}
+// generateReport is allowed to modify p.
func generateReport(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) error {
c, rpt, err := generateRawReport(p, cmd, cfg, o)
if err != nil {
@@ -201,7 +201,6 @@ func applyCommandOverrides(cmd string, outputFormat int, cfg config) config {
case report.Proto, report.Raw, report.Callgrind:
trim = false
cfg.Granularity = "addresses"
- cfg.NoInlines = false
}
if !trim {
@@ -365,3 +364,23 @@ func valueExtractor(ix int) sampleValueFunc {
return v[ix]
}
}
+
+// profileCopier can be used to obtain a fresh copy of a profile.
+// It is useful since reporting code may mutate the profile handed to it.
+type profileCopier []byte
+
+func makeProfileCopier(src *profile.Profile) profileCopier {
+ // Pre-serialize the profile. We will deserialize every time a fresh copy is needed.
+ var buf bytes.Buffer
+ src.WriteUncompressed(&buf)
+ return profileCopier(buf.Bytes())
+}
+
+// newCopy returns a new copy of the profile.
+func (c profileCopier) newCopy() *profile.Profile {
+ p, err := profile.ParseUncompressed([]byte(c))
+ if err != nil {
+ panic(err)
+ }
+ return p
+}
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go
index 0b361651bc..5ddee33610 100644
--- a/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go
@@ -18,7 +18,6 @@ import (
"bytes"
"fmt"
"io"
- "io/ioutil"
"net/http"
"net/url"
"os"
@@ -167,7 +166,7 @@ func grabSourcesAndBases(sources, bases []profileSource, fetch plugin.Fetcher, o
// a single profile. It fetches a chunk of profiles concurrently, with a maximum
// chunk size to limit its memory usage.
func chunkedGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) {
- const chunkSize = 64
+ const chunkSize = 128
var p *profile.Profile
var msrc plugin.MappingSources
@@ -242,10 +241,22 @@ func concurrentGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.Ob
func combineProfiles(profiles []*profile.Profile, msrcs []plugin.MappingSources) (*profile.Profile, plugin.MappingSources, error) {
// Merge profiles.
+ //
+ // The merge call below only treats exactly matching sample type lists as
+ // compatible and will fail otherwise. Make the profiles' sample types
+ // compatible for the merge, see CompatibilizeSampleTypes() doc for details.
+ if err := profile.CompatibilizeSampleTypes(profiles); err != nil {
+ return nil, nil, err
+ }
if err := measurement.ScaleProfiles(profiles); err != nil {
return nil, nil, err
}
+ // Avoid expensive work for the common case of a single profile/src.
+ if len(profiles) == 1 && len(msrcs) == 1 {
+ return profiles[0], msrcs[0], nil
+ }
+
p, err := profile.Merge(profiles)
if err != nil {
return nil, nil, err
@@ -410,6 +421,10 @@ mapping:
fileNames = append(fileNames, matches...)
}
fileNames = append(fileNames, filepath.Join(path, m.File, m.BuildID)) // perf path format
+ // Llvm buildid protocol: the first two characters of the build id
+ // are used as directory, and the remaining part is in the filename.
+ // e.g. `/ab/cdef0123456.debug`
+ fileNames = append(fileNames, filepath.Join(path, m.BuildID[:2], m.BuildID[2:]+".debug"))
}
if m.File != "" {
// Try both the basename and the full path, to support the same directory
@@ -507,7 +522,7 @@ func fetchURL(source string, timeout time.Duration, tr http.RoundTripper) (io.Re
func statusCodeError(resp *http.Response) error {
if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") {
// error is from pprof endpoint
- if body, err := ioutil.ReadAll(resp.Body); err == nil {
+ if body, err := io.ReadAll(resp.Body); err == nil {
return fmt.Errorf("server response: %s - %s", resp.Status, body)
}
}
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/html/common.css b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/common.css
index 03755abc0e..e0de53c1e1 100644
--- a/src/cmd/vendor/github.com/google/pprof/internal/driver/html/common.css
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/common.css
@@ -116,6 +116,7 @@ a {
box-shadow: 0 1px 5px rgba(0,0,0,.3);
font-size: 100%;
text-transform: none;
+ white-space: nowrap;
}
.menu-item, .submenu {
user-select: none;
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/html/common.js b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/common.js
index 4fe3caa442..5282c1b363 100644
--- a/src/cmd/vendor/github.com/google/pprof/internal/driver/html/common.js
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/common.js
@@ -388,7 +388,12 @@ function initConfigManager() {
}
}
-function viewer(baseUrl, nodes) {
+// options if present can contain:
+// hiliter: function(Number, Boolean): Boolean
+// Overridable mechanism for highlighting/unhighlighting specified node.
+// current: function() Map[Number,Boolean]
+// Overridable mechanism for fetching set of currently selected nodes.
+function viewer(baseUrl, nodes, options) {
'use strict';
// Elements
@@ -403,6 +408,16 @@ function viewer(baseUrl, nodes) {
let searchAlarm = null;
let buttonsEnabled = true;
+ // Return current selection.
+ function getSelection() {
+ if (selected.size > 0) {
+ return selected;
+ } else if (options && options.current) {
+ return options.current();
+ }
+ return new Map();
+ }
+
function handleDetails(e) {
e.preventDefault();
const detailsText = document.getElementById('detailsbox');
@@ -453,7 +468,7 @@ function viewer(baseUrl, nodes) {
// drop currently selected items that do not match re.
selected.forEach(function(v, n) {
if (!match(nodes[n])) {
- unselect(n, document.getElementById('node' + n));
+ unselect(n);
}
})
@@ -461,7 +476,7 @@ function viewer(baseUrl, nodes) {
if (nodes) {
for (let n = 0; n < nodes.length; n++) {
if (!selected.has(n) && match(nodes[n])) {
- select(n, document.getElementById('node' + n));
+ select(n);
}
}
}
@@ -482,23 +497,19 @@ function viewer(baseUrl, nodes) {
const n = nodeId(elem);
if (n < 0) return;
if (selected.has(n)) {
- unselect(n, elem);
+ unselect(n);
} else {
- select(n, elem);
+ select(n);
}
updateButtons();
}
- function unselect(n, elem) {
- if (elem == null) return;
- selected.delete(n);
- setBackground(elem, false);
+ function unselect(n) {
+ if (setNodeHighlight(n, false)) selected.delete(n);
}
function select(n, elem) {
- if (elem == null) return;
- selected.set(n, true);
- setBackground(elem, true);
+ if (setNodeHighlight(n, true)) selected.set(n, true);
}
function nodeId(elem) {
@@ -511,11 +522,17 @@ function viewer(baseUrl, nodes) {
return n;
}
- function setBackground(elem, set) {
+ // Change highlighting of node (returns true if node was found).
+ function setNodeHighlight(n, set) {
+ if (options && options.hiliter) return options.hiliter(n, set);
+
+ const elem = document.getElementById('node' + n);
+ if (!elem) return false;
+
// Handle table row highlighting.
if (elem.nodeName == 'TR') {
elem.classList.toggle('hilite', set);
- return;
+ return true;
}
// Handle svg element highlighting.
@@ -528,6 +545,8 @@ function viewer(baseUrl, nodes) {
p.style.fill = origFill.get(p);
}
}
+
+ return true;
}
function findPolygon(elem) {
@@ -575,8 +594,8 @@ function viewer(baseUrl, nodes) {
// The selection can be in one of two modes: regexp-based or
// list-based. Construct regular expression depending on mode.
let re = regexpActive
- ? search.value
- : Array.from(selected.keys()).map(key => quotemeta(nodes[key])).join('|');
+ ? search.value
+ : Array.from(getSelection().keys()).map(key => quotemeta(nodes[key])).join('|');
setHrefParams(elem, function (params) {
if (re != '') {
@@ -639,7 +658,7 @@ function viewer(baseUrl, nodes) {
}
function updateButtons() {
- const enable = (search.value != '' || selected.size != 0);
+ const enable = (search.value != '' || getSelection().size != 0);
if (buttonsEnabled == enable) return;
buttonsEnabled = enable;
for (const id of ['focus', 'ignore', 'hide', 'show', 'show-from']) {
@@ -663,8 +682,8 @@ function viewer(baseUrl, nodes) {
toptable.addEventListener('touchstart', handleTopClick);
}
- const ids = ['topbtn', 'graphbtn', 'flamegraph', 'peek', 'list', 'disasm',
- 'focus', 'ignore', 'hide', 'show', 'show-from'];
+ const ids = ['topbtn', 'graphbtn', 'flamegraph', 'flamegraph2', 'peek', 'list',
+ 'disasm', 'focus', 'ignore', 'hide', 'show', 'show-from'];
ids.forEach(makeSearchLinkDynamic);
const sampleIDs = [{{range .SampleTypes}}'{{.}}', {{end}}];
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/html/header.html b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/header.html
index 66cabbbaa4..39cb55a1d1 100644
--- a/src/cmd/vendor/github.com/google/pprof/internal/driver/html/header.html
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/header.html
@@ -12,6 +12,7 @@
<a title="{{.Help.top}}" href="./top" id="topbtn">Top</a>
<a title="{{.Help.graph}}" href="./" id="graphbtn">Graph</a>
<a title="{{.Help.flamegraph}}" href="./flamegraph" id="flamegraph">Flame Graph</a>
+ <a title="{{.Help.flamegraph2}}" href="./flamegraph2" id="flamegraph2">Flame Graph (new)</a>
<a title="{{.Help.peek}}" href="./peek" id="peek">Peek</a>
<a title="{{.Help.list}}" href="./source" id="list">Source</a>
<a title="{{.Help.disasm}}" href="./disasm" id="disasm">Disassemble</a>
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.css b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.css
new file mode 100644
index 0000000000..d142aa789c
--- /dev/null
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.css
@@ -0,0 +1,80 @@
+body {
+ overflow: hidden; /* Want scrollbar not here, but in #stack-holder */
+}
+/* Scrollable container for flame graph */
+#stack-holder {
+ width: 100%;
+ flex-grow: 1;
+ overflow-y: auto;
+ background: #eee; /* Light grey gives better contrast with boxes */
+ position: relative; /* Allows absolute positioning of child boxes */
+}
+/* Flame graph */
+#stack-chart {
+ width: 100%;
+ position: relative; /* Allows absolute positioning of child boxes */
+}
+/* Shows details of frame that is under the mouse */
+#current-details {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ z-index: 2;
+ font-size: 12pt;
+}
+/* Background of a single flame-graph frame */
+.boxbg {
+ border-width: 0px;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+/* Not-inlined frames are visually separated from their caller. */
+.not-inlined {
+ border-top: 1px solid black;
+}
+/* Function name */
+.boxtext {
+ position: absolute;
+ width: 100%;
+ padding-left: 2px;
+ line-height: 18px;
+ cursor: default;
+ font-family: "Google Sans", Arial, sans-serif;
+ font-size: 12pt;
+ z-index: 2;
+}
+/* Box highlighting via shadows to avoid size changes */
+.hilite { box-shadow: 0px 0px 0px 2px #000; z-index: 1; }
+.hilite2 { box-shadow: 0px 0px 0px 2px #000; z-index: 1; }
+/* Self-cost region inside a box */
+.self {
+ position: absolute;
+ background: rgba(0,0,0,0.25); /* Darker hue */
+}
+/* Gap left between callers and callees */
+.separator {
+ position: absolute;
+ text-align: center;
+ font-size: 12pt;
+ font-weight: bold;
+}
+/* Ensure that pprof menu is above boxes */
+.submenu { z-index: 3; }
+/* Right-click menu */
+#action-menu {
+ max-width: 15em;
+}
+/* Right-click menu title */
+#action-title {
+ display: block;
+ padding: 0.5em 1em;
+ background: #888;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+/* Internal canvas used to measure text size when picking fonts */
+#textsizer {
+ position: absolute;
+ bottom: -100px;
+}
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.html b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.html
new file mode 100644
index 0000000000..1ddb7a3a1c
--- /dev/null
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>{{.Title}}</title>
+ {{template "css" .}}
+ {{template "stacks_css"}}
+</head>
+<body>
+ {{template "header" .}}
+ <div id="stack-holder">
+ <div id="stack-chart"></div>
+ <div id="current-details"></div>
+ </div>
+ <div id="action-menu" class="submenu">
+ <span id="action-title"></span>
+ <hr>
+ <a title="{{.Help.list}}" id="action-source" href="./source">Show source code</a>
+ <a title="{{.Help.list}}" id="action-source-tab" href="./source" target="_blank">Show source in new tab</a>
+ <hr>
+ <a title="{{.Help.focus}}" id="action-focus" href="?">Focus</a>
+ <a title="{{.Help.ignore}}" id="action-ignore" href="?">Ignore</a>
+ <a title="{{.Help.hide}}" id="action-hide" href="?">Hide</a>
+ <a title="{{.Help.show_from}}" id="action-showfrom" href="?">Show from</a>
+ </div>
+ {{template "script" .}}
+ {{template "stacks_js"}}
+ <script>
+ stackViewer({{.Stacks}}, {{.Nodes}});
+ </script>
+</body>
+</html>
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.js b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.js
new file mode 100644
index 0000000000..64229a000a
--- /dev/null
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.js
@@ -0,0 +1,524 @@
+// stackViewer displays a flame-graph like view (extended to show callers).
+// stacks - report.StackSet
+// nodes - List of names for each source in report.StackSet
+function stackViewer(stacks, nodes) {
+ 'use strict';
+
+ // Constants used in rendering.
+ const ROW = 20;
+ const PADDING = 2;
+ const MIN_WIDTH = 4;
+ const MIN_TEXT_WIDTH = 16;
+ const TEXT_MARGIN = 2;
+ const FONT_SIZE = 12;
+ const MIN_FONT_SIZE = 8;
+
+ // Mapping from unit to a list of display scales/labels.
+ // List should be ordered by increasing unit size.
+ const UNITS = new Map([
+ ['B', [
+ ['B', 1],
+ ['kB', Math.pow(2, 10)],
+ ['MB', Math.pow(2, 20)],
+ ['GB', Math.pow(2, 30)],
+ ['TB', Math.pow(2, 40)],
+ ['PB', Math.pow(2, 50)]]],
+ ['s', [
+ ['ns', 1e-9],
+ ['µs', 1e-6],
+ ['ms', 1e-3],
+ ['s', 1],
+ ['hrs', 60*60]]]]);
+
+ // Fields
+ let shownTotal = 0; // Total value of all stacks
+ let pivots = []; // Indices of currently selected data.Sources entries.
+ let matches = new Set(); // Indices of sources that match search
+ let elems = new Map(); // Mapping from source index to display elements
+ let displayList = []; // List of boxes to display.
+ let actionMenuOn = false; // Is action menu visible?
+ let actionTarget = null; // Box on which action menu is operating.
+
+ // Setup to allow measuring text width.
+ const textSizer = document.createElement('canvas');
+ textSizer.id = 'textsizer';
+ const textContext = textSizer.getContext('2d');
+
+ // Get DOM elements.
+ const chart = find('stack-chart');
+ const search = find('search');
+ const actions = find('action-menu');
+ const actionTitle = find('action-title');
+ const detailBox = find('current-details');
+
+ window.addEventListener('resize', render);
+ window.addEventListener('popstate', render);
+ search.addEventListener('keydown', handleSearchKey);
+
+ // Withdraw action menu when clicking outside, or when item selected.
+ document.addEventListener('mousedown', (e) => {
+ if (!actions.contains(e.target)) {
+ hideActionMenu();
+ }
+ });
+ actions.addEventListener('click', hideActionMenu);
+
+ // Initialize menus and other general UI elements.
+ viewer(new URL(window.location.href), nodes, {
+ hiliter: (n, on) => { return hilite(n, on); },
+ current: () => {
+ let r = new Map();
+ for (let p of pivots) {
+ r.set(p, true);
+ }
+ return r;
+ }});
+
+ render();
+
+ // Helper functions follow:
+
+ // hilite changes the highlighting of elements corresponding to specified src.
+ function hilite(src, on) {
+ if (on) {
+ matches.add(src);
+ } else {
+ matches.delete(src);
+ }
+ toggleClass(src, 'hilite', on);
+ return true;
+ }
+
+ // Display action menu (triggered by right-click on a frame)
+ function showActionMenu(e, box) {
+ if (box.src == 0) return; // No action menu for root
+ e.preventDefault(); // Disable browser context menu
+ const src = stacks.Sources[box.src];
+ actionTitle.innerText = src.Display[src.Display.length-1];
+ const menu = actions;
+ menu.style.display = 'block';
+ // Compute position so menu stays visible and near the mouse.
+ const x = Math.min(e.clientX - 10, document.body.clientWidth - menu.clientWidth);
+ const y = Math.min(e.clientY - 10, document.body.clientHeight - menu.clientHeight);
+ menu.style.left = x + 'px';
+ menu.style.top = y + 'px';
+ // Set menu links to operate on clicked box.
+ setHrefParam('action-source', 'f', box.src);
+ setHrefParam('action-source-tab', 'f', box.src);
+ setHrefParam('action-focus', 'f', box.src);
+ setHrefParam('action-ignore', 'i', box.src);
+ setHrefParam('action-hide', 'h', box.src);
+ setHrefParam('action-showfrom', 'sf', box.src);
+ toggleClass(box.src, 'hilite2', true);
+ actionTarget = box;
+ actionMenuOn = true;
+ }
+
+ function hideActionMenu() {
+ actions.style.display = 'none';
+ actionMenuOn = false;
+ if (actionTarget != null) {
+ toggleClass(actionTarget.src, 'hilite2', false);
+ }
+ }
+
+ // setHrefParam updates the specified parameter in the href of an <a>
+ // element to make it operate on the specified src.
+ function setHrefParam(id, param, src) {
+ const elem = document.getElementById(id);
+ if (!elem) return;
+
+ let url = new URL(elem.href);
+ url.hash = '';
+
+ // Copy params from this page's URL.
+ const params = url.searchParams;
+ for (const p of new URLSearchParams(window.location.search)) {
+ params.set(p[0], p[1]);
+ }
+
+ // Update params to include src.
+ let v = stacks.Sources[src].RE;
+ if (param != 'f' && param != 'sf') { // old f,sf values are overwritten
+ // Add new source to current parameter value.
+ const old = params.get(param);
+ if (old && old != '') {
+ v += '|' + old;
+ }
+ }
+ params.set(param, v);
+
+ elem.href = url.toString();
+ }
+
+ // Capture Enter key in the search box to make it pivot instead of focus.
+ function handleSearchKey(e) {
+ if (e.key != 'Enter') return;
+ e.stopImmediatePropagation(); // Disable normal enter key handling
+ const val = search.value;
+ try {
+ new RegExp(search.value);
+ } catch (error) {
+ return; // TODO: Display error state in search box
+ }
+ switchPivots(val);
+ }
+
+ function switchPivots(regexp) {
+ // Switch URL without hitting the server.
+ const url = new URL(document.URL);
+ url.searchParams.set('p', regexp);
+ history.pushState('', '', url.toString()); // Makes back-button work
+ matches = new Set();
+ search.value = '';
+ render();
+ }
+
+ function handleEnter(box, div) {
+ if (actionMenuOn) return;
+ const src = stacks.Sources[box.src];
+ const d = details(box);
+ div.title = d + ' ' + src.FullName + (src.Inlined ? "\n(inlined)" : "");
+ detailBox.innerText = d;
+ // Highlight all boxes that have the same source as box.
+ toggleClass(box.src, 'hilite2', true);
+ }
+
+ function handleLeave(box) {
+ if (actionMenuOn) return;
+ detailBox.innerText = '';
+ toggleClass(box.src, 'hilite2', false);
+ }
+
+ // Return list of sources that match the regexp given by the 'p' URL parameter.
+ function urlPivots() {
+ const pivots = [];
+ const params = (new URL(document.URL)).searchParams;
+ const val = params.get('p');
+ if (val !== null && val != '') {
+ try {
+ const re = new RegExp(val);
+ for (let i = 0; i < stacks.Sources.length; i++) {
+ const src = stacks.Sources[i];
+ if (re.test(src.UniqueName) || re.test(src.FileName)) {
+ pivots.push(i);
+ }
+ }
+ } catch (error) {}
+ }
+ if (pivots.length == 0) {
+ pivots.push(0);
+ }
+ return pivots;
+ }
+
+ // render re-generates the stack display.
+ function render() {
+ pivots = urlPivots();
+
+ // Get places where pivots occur.
+ let places = [];
+ for (let pivot of pivots) {
+ const src = stacks.Sources[pivot];
+ for (let p of src.Places) {
+ places.push(p);
+ }
+ }
+
+ const width = chart.clientWidth;
+ elems.clear();
+ actionTarget = null;
+ const total = totalValue(places);
+ const xscale = (width-2*PADDING) / total; // Converts from profile value to X pixels
+ const x = PADDING;
+ const y = 0;
+ shownTotal = total;
+
+ displayList.length = 0;
+ renderStacks(0, xscale, x, y, places, +1); // Callees
+ renderStacks(0, xscale, x, y-ROW, places, -1); // Callers (ROW left for separator)
+ display(displayList);
+ }
+
+ // renderStacks creates boxes with top-left at x,y with children drawn as
+ // nested stacks (below or above based on the sign of direction).
+ // Returns the largest y coordinate filled.
+ function renderStacks(depth, xscale, x, y, places, direction) {
+ // Example: suppose we are drawing the following stacks:
+ // a->b->c
+ // a->b->d
+ // a->e->f
+ // After rendering a, we will call renderStacks, with places pointing to
+ // the preceding stacks.
+ //
+ // We first group all places with the same leading entry. In this example
+ // we get [b->c, b->d] and [e->f]. We render the two groups side-by-side.
+ const groups = partitionPlaces(places);
+ for (const g of groups) {
+ renderGroup(depth, xscale, x, y, g, direction);
+ x += xscale*g.sum;
+ }
+ }
+
+ function renderGroup(depth, xscale, x, y, g, direction) {
+ // Skip if not wide enough.
+ const width = xscale * g.sum;
+ if (width < MIN_WIDTH) return;
+
+ // Draw the box for g.src (except for selected element in upwards direction
+ // since that duplicates the box we added in downwards direction).
+ if (depth != 0 || direction > 0) {
+ const box = {
+ x: x,
+ y: y,
+ src: g.src,
+ sum: g.sum,
+ selfValue: g.self,
+ width: xscale*g.sum,
+ selfWidth: (direction > 0) ? xscale*g.self : 0,
+ };
+ displayList.push(box);
+ x += box.selfWidth;
+ }
+ y += direction * ROW;
+
+ // Find child or parent stacks.
+ const next = [];
+ for (const place of g.places) {
+ const stack = stacks.Stacks[place.Stack];
+ const nextSlot = place.Pos + direction;
+ if (nextSlot >= 0 && nextSlot < stack.Sources.length) {
+ next.push({Stack: place.Stack, Pos: nextSlot});
+ }
+ }
+ renderStacks(depth+1, xscale, x, y, next, direction);
+ }
+
+ // partitionPlaces partitions a set of places into groups where each group
+ // contains places with the same source. If a stack occurs multiple times
+ // in places, only the outer-most occurrence is kept.
+ function partitionPlaces(places) {
+ // Find outer-most slot per stack (used later to elide duplicate stacks).
+ const stackMap = new Map(); // Map from stack index to outer-most slot#
+ for (const place of places) {
+ const prevSlot = stackMap.get(place.Stack);
+ if (prevSlot && prevSlot <= place.Pos) {
+ // We already have a higher slot in this stack.
+ } else {
+ stackMap.set(place.Stack, place.Pos);
+ }
+ }
+
+ // Now partition the stacks.
+ const groups = []; // Array of Group {name, src, sum, self, places}
+ const groupMap = new Map(); // Map from Source to Group
+ for (const place of places) {
+ if (stackMap.get(place.Stack) != place.Pos) {
+ continue;
+ }
+
+ const stack = stacks.Stacks[place.Stack];
+ const src = stack.Sources[place.Pos];
+ let group = groupMap.get(src);
+ if (!group) {
+ const name = stacks.Sources[src].FullName;
+ group = {name: name, src: src, sum: 0, self: 0, places: []};
+ groupMap.set(src, group);
+ groups.push(group);
+ }
+ group.sum += stack.Value;
+ group.self += (place.Pos == stack.Sources.length-1) ? stack.Value : 0;
+ group.places.push(place);
+ }
+
+ // Order by decreasing cost (makes it easier to spot heavy functions).
+ // Though alphabetical ordering is a potential alternative that will make
+ // profile comparisons easier.
+ groups.sort(function(a, b) { return b.sum - a.sum; });
+
+ return groups;
+ }
+
+ function display(list) {
+ // Sort boxes so that text selection follows a predictable order.
+ list.sort(function(a, b) {
+ if (a.y != b.y) return a.y - b.y;
+ return a.x - b.x;
+ });
+
+ // Adjust Y coordinates so that zero is at top.
+ let adjust = (list.length > 0) ? list[0].y : 0;
+ adjust -= ROW + 2*PADDING; // Room for details
+
+ const divs = [];
+ for (const box of list) {
+ box.y -= adjust;
+ divs.push(drawBox(box));
+ }
+ divs.push(drawSep(-adjust));
+
+ const h = (list.length > 0 ? list[list.length-1].y : 0) + 4*ROW;
+ chart.style.height = h+'px';
+ chart.replaceChildren(...divs);
+ }
+
+ function drawBox(box) {
+ const srcIndex = box.src;
+ const src = stacks.Sources[srcIndex];
+
+ // Background
+ const w = box.width - 1; // Leave 1px gap
+ const r = document.createElement('div');
+ r.style.left = box.x + 'px';
+ r.style.top = box.y + 'px';
+ r.style.width = w + 'px';
+ r.style.height = ROW + 'px';
+ r.classList.add('boxbg');
+ r.style.background = makeColor(src.Color);
+ addElem(srcIndex, r);
+ if (!src.Inlined) {
+ r.classList.add('not-inlined');
+ }
+
+ // Box that shows time spent in self
+ if (box.selfWidth >= MIN_WIDTH) {
+ const s = document.createElement('div');
+ s.style.width = Math.min(box.selfWidth, w)+'px';
+ s.style.height = (ROW-1)+'px';
+ s.classList.add('self');
+ r.appendChild(s);
+ }
+
+ // Label
+ if (box.width >= MIN_TEXT_WIDTH) {
+ const t = document.createElement('div');
+ t.classList.add('boxtext');
+ fitText(t, box.width-2*TEXT_MARGIN, src.Display);
+ r.appendChild(t);
+ }
+
+ r.addEventListener('click', () => { switchPivots(src.RE); });
+ r.addEventListener('mouseenter', () => { handleEnter(box, r); });
+ r.addEventListener('mouseleave', () => { handleLeave(box); });
+ r.addEventListener('contextmenu', (e) => { showActionMenu(e, box); });
+ return r;
+ }
+
+ function drawSep(y) {
+ const m = document.createElement('div');
+ m.innerText = percent(shownTotal, stacks.Total) +
+ '\xa0\xa0\xa0\xa0' + // Some non-breaking spaces
+ valueString(shownTotal);
+ m.style.top = (y-ROW) + 'px';
+ m.style.left = PADDING + 'px';
+ m.style.width = (chart.clientWidth - PADDING*2) + 'px';
+ m.classList.add('separator');
+ return m;
+ }
+
+ // addElem registers an element that belongs to the specified src.
+ function addElem(src, elem) {
+ let list = elems.get(src);
+ if (!list) {
+ list = [];
+ elems.set(src, list);
+ }
+ list.push(elem);
+ elem.classList.toggle('hilite', matches.has(src));
+ }
+
+ // Adds or removes cl from classList of all elements for the specified source.
+ function toggleClass(src, cl, value) {
+ const list = elems.get(src);
+ if (list) {
+ for (const elem of list) {
+ elem.classList.toggle(cl, value);
+ }
+ }
+ }
+
+ // fitText sets text and font-size clipped to the specified width w.
+ function fitText(t, avail, textList) {
+ // Find first entry in textList that fits.
+ let width = avail;
+ textContext.font = FONT_SIZE + 'pt Arial';
+ for (let i = 0; i < textList.length; i++) {
+ let text = textList[i];
+ width = textContext.measureText(text).width;
+ if (width <= avail) {
+ t.innerText = text;
+ return;
+ }
+ }
+
+ // Try to fit by dropping font size.
+ let text = textList[textList.length-1];
+ const fs = Math.max(MIN_FONT_SIZE, FONT_SIZE * (avail / width));
+ t.style.fontSize = fs + 'pt';
+ t.innerText = text;
+ }
+
+ // totalValue returns the combined sum of the stacks listed in places.
+ function totalValue(places) {
+ const seen = new Set();
+ let result = 0;
+ for (const place of places) {
+ if (seen.has(place.Stack)) continue; // Do not double-count stacks
+ seen.add(place.Stack);
+ const stack = stacks.Stacks[place.Stack];
+ result += stack.Value;
+ }
+ return result;
+ }
+
+ function details(box) {
+ // E.g., 10% 7s
+ // or 10% 7s (3s self
+ let result = percent(box.sum, stacks.Total) + ' ' + valueString(box.sum);
+ if (box.selfValue > 0) {
+ result += ` (${valueString(box.selfValue)} self)`;
+ }
+ return result;
+ }
+
+ function percent(v, total) {
+ return Number(((100.0 * v) / total).toFixed(1)) + '%';
+ }
+
+ // valueString returns a formatted string to display for value.
+ function valueString(value) {
+ let v = value * stacks.Scale;
+ // Rescale to appropriate display unit.
+ let unit = stacks.Unit;
+ const list = UNITS.get(unit);
+ if (list) {
+ // Find first entry in list that is not too small.
+ for (const [name, scale] of list) {
+ if (v <= 100*scale) {
+ v /= scale;
+ unit = name;
+ break;
+ }
+ }
+ }
+ return Number(v.toFixed(2)) + unit;
+ }
+
+ function find(name) {
+ const elem = document.getElementById(name);
+ if (!elem) {
+ throw 'element not found: ' + name
+ }
+ return elem;
+ }
+
+ function makeColor(index) {
+ // Rotate hue around a circle. Multiple by phi to spread things
+ // out better. Use 50% saturation to make subdued colors, and
+ // 80% lightness to have good contrast with black foreground text.
+ const PHI = 1.618033988;
+ const hue = (index+1) * PHI * 2 * Math.PI; // +1 to avoid 0
+ const hsl = `hsl(${hue}rad 50% 80%)`;
+ return hsl;
+ }
+}
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go
index 777fb90bfb..e6e865f385 100644
--- a/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go
@@ -42,6 +42,7 @@ func interactive(p *profile.Profile, o *plugin.Options) error {
interactiveMode = true
shortcuts := profileShortcuts(p)
+ copier := makeProfileCopier(p)
greetings(p, o.UI)
for {
input, err := o.UI.ReadLine("(pprof) ")
@@ -110,7 +111,7 @@ func interactive(p *profile.Profile, o *plugin.Options) error {
args, cfg, err := parseCommandLine(tokens)
if err == nil {
- err = generateReportWrapper(p, args, cfg, o)
+ err = generateReportWrapper(copier.newCopy(), args, cfg, o)
}
if err != nil {
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/settings.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/settings.go
index 1e9154c5f5..b784618aca 100644
--- a/src/cmd/vendor/github.com/google/pprof/internal/driver/settings.go
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/settings.go
@@ -3,7 +3,6 @@ package driver
import (
"encoding/json"
"fmt"
- "io/ioutil"
"net/url"
"os"
"path/filepath"
@@ -33,7 +32,7 @@ func settingsFileName() (string, error) {
// readSettings reads settings from fname.
func readSettings(fname string) (*settings, error) {
- data, err := ioutil.ReadFile(fname)
+ data, err := os.ReadFile(fname)
if err != nil {
if os.IsNotExist(err) {
return &settings{}, nil
@@ -64,7 +63,7 @@ func writeSettings(fname string, settings *settings) error {
return fmt.Errorf("failed to create settings directory: %w", err)
}
- if err := ioutil.WriteFile(fname, data, 0644); err != nil {
+ if err := os.WriteFile(fname, data, 0644); err != nil {
return fmt.Errorf("failed to write settings: %w", err)
}
return nil
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/stacks.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/stacks.go
new file mode 100644
index 0000000000..249dfe0742
--- /dev/null
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/stacks.go
@@ -0,0 +1,58 @@
+// Copyright 2022 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package driver
+
+import (
+ "encoding/json"
+ "html/template"
+ "net/http"
+
+ "github.com/google/pprof/internal/report"
+)
+
+// stackView generates the new flamegraph view.
+func (ui *webInterface) stackView(w http.ResponseWriter, req *http.Request) {
+ // Get all data in a report.
+ rpt, errList := ui.makeReport(w, req, []string{"svg"}, func(cfg *config) {
+ cfg.CallTree = true
+ cfg.Trim = false
+ cfg.Granularity = "filefunctions"
+ })
+ if rpt == nil {
+ return // error already reported
+ }
+
+ // Make stack data and generate corresponding JSON.
+ stacks := rpt.Stacks()
+ b, err := json.Marshal(stacks)
+ if err != nil {
+ http.Error(w, "error serializing stacks for flame graph",
+ http.StatusInternalServerError)
+ ui.options.UI.PrintErr(err)
+ return
+ }
+
+ nodes := make([]string, len(stacks.Sources))
+ for i, src := range stacks.Sources {
+ nodes[i] = src.FullName
+ }
+ nodes[0] = "" // root is not a real node
+
+ _, legend := report.TextItems(rpt)
+ ui.render(w, req, "stacks", rpt, errList, legend, webArgs{
+ Stacks: template.JS(b),
+ Nodes: nodes,
+ })
+}
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/tagroot.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/tagroot.go
index c43d599982..76a594d9f7 100644
--- a/src/cmd/vendor/github.com/google/pprof/internal/driver/tagroot.go
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/tagroot.go
@@ -97,6 +97,10 @@ func addLabelNodes(p *profile.Profile, rootKeys, leafKeys []string, outputUnit s
leafm = true
}
+ if len(leavesToAdd)+len(rootsToAdd) == 0 {
+ continue
+ }
+
var newLocs []*profile.Location
newLocs = append(newLocs, leavesToAdd...)
newLocs = append(newLocs, s.Location...)
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go
index 94f32e3755..55973ffb9f 100644
--- a/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go
@@ -65,4 +65,7 @@ func addTemplates(templates *template.Template) {
def("sourcelisting", loadFile("html/source.html"))
def("plaintext", loadFile("html/plaintext.html"))
def("flamegraph", loadFile("html/flamegraph.html"))
+ def("stacks", loadFile("html/stacks.html"))
+ def("stacks_css", loadCSS("html/stacks.css"))
+ def("stacks_js", loadJS("html/stacks.js"))
}
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go
index 0f3e8bf93c..8881e39eb2 100644
--- a/src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go
+++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go
@@ -36,13 +36,14 @@ import (
// webInterface holds the state needed for serving a browser based interface.
type webInterface struct {
prof *profile.Profile
+ copier profileCopier
options *plugin.Options
help map[string]string
templates *template.Template
settingsFile string
}
-func makeWebInterface(p *profile.Profile, opt *plugin.Options) (*webInterface, error) {
+func makeWebInterface(p *profile.Profile, copier profileCopier, opt *plugin.Options) (*webInterface, error) {
settingsFile, err := settingsFileName()
if err != nil {
return nil, err
@@ -52,6 +53,7 @@ func makeWebInterface(p *profile.Profile, opt *plugin.Options) (*webInterface, e
report.AddSourceTemplates(templates)
return &webInterface{
prof: p,
+ copier: copier,
options: opt,
help: make(map[string]string),
templates: templates,
@@ -86,6 +88,7 @@ type webArgs struct {
TextBody string
Top []report.TextItem
FlameGraph template.JS
+ Stacks template.JS
Configs []configMenuEntry
}
@@ -95,7 +98,8 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, d
return err
}
interactiveMode = true
- ui, err := makeWebInterface(p, o)
+ copier := makeProfileCopier(p)
+ ui, err := makeWebInterface(p, copier, o)
if err != nil {
return err
}
@@ -107,6 +111,8 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, d
}
ui.help["details"] = "Show information about the profile and this view"
ui.help["graph"] = "Display profile as a directed graph"
+ ui.help["flamegraph"] = "Display profile as a flame graph"
+ ui.help["flamegraph2"] = "Display profile as a flame graph (experimental version that can display caller info on selection)"
ui.help["reset"] = "Show the entire profile"
ui.help["save_config"] = "Save current settings"
@@ -125,6 +131,7 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, d
"/source": http.HandlerFunc(ui.source),
"/peek": http.HandlerFunc(ui.peek),
"/flamegraph": http.HandlerFunc(ui.flamegraph),
+ "/flamegraph2": http.HandlerFunc(ui.stackView), // Experimental
"/saveconfig": http.HandlerFunc(ui.saveConfig),
"/deleteconfig": http.HandlerFunc(ui.deleteConfig),
"/download": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -262,7 +269,7 @@ func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request,
catcher := &errorCatcher{UI: ui.options.UI}
options := *ui.options
options.UI = catcher
- _, rpt, err := generateRawReport(ui.prof, cmd, cfg, &options)
+ _, rpt, err := generateRawReport(ui.copier.newCopy(), cmd, cfg, &options)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
ui.options.UI.PrintErr(err)