go 1.20
require (
- github.com/google/pprof v0.0.0-20220729232143-a41b82acbcb1
+ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26
golang.org/x/arch v0.1.1-0.20221116201807-1bb480fc256a
golang.org/x/mod v0.7.0
golang.org/x/sync v0.1.0
-github.com/google/pprof v0.0.0-20220729232143-a41b82acbcb1 h1:8pyqKJvrJqUYaKS851Ule26pwWvey6IDMiczaBLDKLQ=
-github.com/google/pprof v0.0.0-20220729232143-a41b82acbcb1/go.mod h1:gSuNB+gJaOiQKLEZ+q+PK9Mq3SOzhRcw2GsGS/FhYDk=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2 h1:rcanfLhLDA8nozr/K289V1zcntHr3V+SHlXwzz1ZI2g=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
golang.org/x/arch v0.1.1-0.20221116201807-1bb480fc256a h1:TpDpIG2bYSheFxm9xw8NNrBKrurU1ZJ59ZMXnpQwPLQ=
" 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"
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)
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 {
case report.Proto, report.Raw, report.Callgrind:
trim = false
cfg.Granularity = "addresses"
- cfg.NoInlines = false
}
if !trim {
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
+}
"bytes"
"fmt"
"io"
- "io/ioutil"
"net/http"
"net/url"
"os"
// 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
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
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
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)
}
}
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;
}
}
-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
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');
// 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);
}
})
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);
}
}
}
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) {
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.
p.style.fill = origFill.get(p);
}
}
+
+ return true;
}
function findPolygon(elem) {
// 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 != '') {
}
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']) {
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}}];
<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>
--- /dev/null
+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;
+}
--- /dev/null
+<!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>
--- /dev/null
+// 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;
+ }
+}
interactiveMode = true
shortcuts := profileShortcuts(p)
+ copier := makeProfileCopier(p)
greetings(p, o.UI)
for {
input, err := o.UI.ReadLine("(pprof) ")
args, cfg, err := parseCommandLine(tokens)
if err == nil {
- err = generateReportWrapper(p, args, cfg, o)
+ err = generateReportWrapper(copier.newCopy(), args, cfg, o)
}
if err != nil {
import (
"encoding/json"
"fmt"
- "io/ioutil"
"net/url"
"os"
"path/filepath"
// 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
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
--- /dev/null
+// 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,
+ })
+}
leafm = true
}
+ if len(leavesToAdd)+len(rootsToAdd) == 0 {
+ continue
+ }
+
var newLocs []*profile.Location
newLocs = append(newLocs, leavesToAdd...)
newLocs = append(newLocs, s.Location...)
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"))
}
// 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
report.AddSourceTemplates(templates)
return &webInterface{
prof: p,
+ copier: copier,
options: opt,
help: make(map[string]string),
templates: templates,
TextBody string
Top []report.TextItem
FlameGraph template.JS
+ Stacks template.JS
Configs []configMenuEntry
}
return err
}
interactiveMode = true
- ui, err := makeWebInterface(p, o)
+ copier := makeProfileCopier(p)
+ ui, err := makeWebInterface(p, copier, o)
if err != nil {
return err
}
}
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"
"/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) {
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)
--- /dev/null
+package report
+
+import "regexp"
+
+// pkgRE extracts package name, It looks for the first "." or "::" that occurs
+// after the last "/". (Searching after the last / allows us to correctly handle
+// names that look like "some.url.com/foo.bar".
+var pkgRE = regexp.MustCompile(`^((.*/)?[\w\d_]+)(\.|::)([^/]*)$`)
+
+// packageName returns the package name of the named symbol, or "" if not found.
+func packageName(name string) string {
+ m := pkgRE.FindStringSubmatch(name)
+ if m == nil {
+ return ""
+ }
+ return m[1]
+}
--- /dev/null
+// 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 report
+
+import (
+ "regexp"
+
+ "github.com/google/pprof/internal/graph"
+)
+
+var sepRE = regexp.MustCompile(`::|\.`)
+
+// shortNameList returns a non-empty sequence of shortened names
+// (in decreasing preference) that can be used to represent name.
+func shortNameList(name string) []string {
+ name = graph.ShortenFunctionName(name)
+ seps := sepRE.FindAllStringIndex(name, -1)
+ result := make([]string, 0, len(seps)+1)
+ result = append(result, name)
+ for _, sep := range seps {
+ // Suffix starting just after sep
+ if sep[1] < len(name) {
+ result = append(result, name[sep[1]:])
+ }
+ }
+ return result
+}
--- /dev/null
+// 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 report
+
+import (
+ "crypto/sha256"
+ "encoding/binary"
+ "fmt"
+ "regexp"
+
+ "github.com/google/pprof/internal/measurement"
+ "github.com/google/pprof/profile"
+)
+
+// StackSet holds a set of stacks corresponding to a profile.
+//
+// Slices in StackSet and the types it contains are always non-nil,
+// which makes Javascript code that uses the JSON encoding less error-prone.
+type StackSet struct {
+ Total int64 // Total value of the profile.
+ Scale float64 // Multiplier to generate displayed value
+ Type string // Profile type. E.g., "cpu".
+ Unit string // One of "B", "s", "GCU", or "" (if unknown)
+ Stacks []Stack // List of stored stacks
+ Sources []StackSource // Mapping from source index to info
+}
+
+// Stack holds a single stack instance.
+type Stack struct {
+ Value int64 // Total value for all samples of this stack.
+ Sources []int // Indices in StackSet.Sources (callers before callees).
+}
+
+// StackSource holds function/location info for a stack entry.
+type StackSource struct {
+ FullName string
+ FileName string
+ UniqueName string // Disambiguates functions with same names
+ Inlined bool // If true this source was inlined into its caller
+
+ // Alternative names to display (with decreasing lengths) to make text fit.
+ // Guaranteed to be non-empty.
+ Display []string
+
+ // Regular expression (anchored) that matches exactly FullName.
+ RE string
+
+ // Places holds the list of stack slots where this source occurs.
+ // In particular, if [a,b] is an element in Places,
+ // StackSet.Stacks[a].Sources[b] points to this source.
+ //
+ // No stack will be referenced twice in the Places slice for a given
+ // StackSource. In case of recursion, Places will contain the outer-most
+ // entry in the recursive stack. E.g., if stack S has source X at positions
+ // 4,6,9,10, the Places entry for X will contain [S,4].
+ Places []StackSlot
+
+ // Combined count of stacks where this source is the leaf.
+ Self int64
+
+ // Color number to use for this source.
+ // Colors with high numbers than supported may be treated as zero.
+ Color int
+}
+
+// StackSlot identifies a particular StackSlot.
+type StackSlot struct {
+ Stack int // Index in StackSet.Stacks
+ Pos int // Index in Stack.Sources
+}
+
+// Stacks returns a StackSet for the profile in rpt.
+func (rpt *Report) Stacks() StackSet {
+ // Get scale for converting to default unit of the right type.
+ scale, unit := measurement.Scale(1, rpt.options.SampleUnit, "default")
+ if unit == "default" {
+ unit = ""
+ }
+ if rpt.options.Ratio > 0 {
+ scale *= rpt.options.Ratio
+ }
+ s := &StackSet{
+ Total: rpt.total,
+ Scale: scale,
+ Type: rpt.options.SampleType,
+ Unit: unit,
+ Stacks: []Stack{}, // Ensure non-nil
+ Sources: []StackSource{}, // Ensure non-nil
+ }
+ s.makeInitialStacks(rpt)
+ s.fillPlaces()
+ s.assignColors()
+ return *s
+}
+
+func (s *StackSet) makeInitialStacks(rpt *Report) {
+ type key struct {
+ line profile.Line
+ inlined bool
+ }
+ srcs := map[key]int{} // Sources identified so far.
+ seenFunctions := map[string]bool{}
+ unknownIndex := 1
+ getSrc := func(line profile.Line, inlined bool) int {
+ k := key{line, inlined}
+ if i, ok := srcs[k]; ok {
+ return i
+ }
+ x := StackSource{Places: []StackSlot{}} // Ensure Places is non-nil
+ if fn := line.Function; fn != nil {
+ x.FullName = fn.Name
+ x.FileName = fn.Filename
+ if !seenFunctions[fn.Name] {
+ x.UniqueName = fn.Name
+ seenFunctions[fn.Name] = true
+ } else {
+ // Assign a different name so pivoting picks this function.
+ x.UniqueName = fmt.Sprint(fn.Name, "#", fn.ID)
+ }
+ } else {
+ x.FullName = fmt.Sprintf("?%d?", unknownIndex)
+ x.UniqueName = x.FullName
+ unknownIndex++
+ }
+ x.Inlined = inlined
+ x.RE = "^" + regexp.QuoteMeta(x.UniqueName) + "$"
+ x.Display = shortNameList(x.FullName)
+ s.Sources = append(s.Sources, x)
+ srcs[k] = len(s.Sources) - 1
+ return len(s.Sources) - 1
+ }
+
+ // Synthesized root location that will be placed at the beginning of each stack.
+ s.Sources = []StackSource{{
+ FullName: "root",
+ Display: []string{"root"},
+ Places: []StackSlot{},
+ }}
+
+ for _, sample := range rpt.prof.Sample {
+ value := rpt.options.SampleValue(sample.Value)
+ stack := Stack{Value: value, Sources: []int{0}} // Start with the root
+
+ // Note: we need to reverse the order in the produced stack.
+ for i := len(sample.Location) - 1; i >= 0; i-- {
+ loc := sample.Location[i]
+ for j := len(loc.Line) - 1; j >= 0; j-- {
+ line := loc.Line[j]
+ inlined := (j != len(loc.Line)-1)
+ stack.Sources = append(stack.Sources, getSrc(line, inlined))
+ }
+ }
+
+ leaf := stack.Sources[len(stack.Sources)-1]
+ s.Sources[leaf].Self += value
+ s.Stacks = append(s.Stacks, stack)
+ }
+}
+
+func (s *StackSet) fillPlaces() {
+ for i, stack := range s.Stacks {
+ seenSrcs := map[int]bool{}
+ for j, src := range stack.Sources {
+ if seenSrcs[src] {
+ continue
+ }
+ seenSrcs[src] = true
+ s.Sources[src].Places = append(s.Sources[src].Places, StackSlot{i, j})
+ }
+ }
+}
+
+func (s *StackSet) assignColors() {
+ // Assign different color indices to different packages.
+ const numColors = 1048576
+ for i, src := range s.Sources {
+ pkg := packageName(src.FullName)
+ h := sha256.Sum256([]byte(pkg))
+ index := binary.LittleEndian.Uint32(h[:])
+ s.Sources[i].Color = int(index % numColors)
+ }
+}
import (
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"net/url"
"path/filepath"
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http post %s: %v", source, statusCodeError(resp))
}
- return ioutil.ReadAll(resp.Body)
+ return io.ReadAll(resp.Body)
}
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)
}
}
"crypto/tls"
"crypto/x509"
"fmt"
- "io/ioutil"
"net/http"
+ "os"
"sync"
"github.com/google/pprof/internal/plugin"
if ca != "" {
caCertPool := x509.NewCertPool()
- caCert, err := ioutil.ReadFile(ca)
+ caCert, err := os.ReadFile(ca)
if err != nil {
return fmt.Errorf("could not load CA specified by -tls_ca: %v", err)
}
// repeated Location location = 4
func(b *buffer, m message) error {
x := new(Location)
- x.Line = make([]Line, 0, 8) // Pre-allocate Line buffer
+ x.Line = b.tmpLines[:0] // Use shared space temporarily
pp := m.(*Profile)
pp.Location = append(pp.Location, x)
err := decodeMessage(b, x)
- var tmp []Line
- x.Line = append(tmp, x.Line...) // Shrink to allocated size
+ b.tmpLines = x.Line[:0]
+ // Copy to shrink size and detach from shared space.
+ x.Line = append([]Line(nil), x.Line...)
return err
},
// repeated Function function = 5
st.Unit, err = getString(p.stringTable, &st.unitX, err)
}
+ // Pre-allocate space for all locations.
+ numLocations := 0
for _, s := range p.Sample {
- labels := make(map[string][]string, len(s.labelX))
- numLabels := make(map[string][]int64, len(s.labelX))
- numUnits := make(map[string][]string, len(s.labelX))
- for _, l := range s.labelX {
- var key, value string
- key, err = getString(p.stringTable, &l.keyX, err)
- if l.strX != 0 {
- value, err = getString(p.stringTable, &l.strX, err)
- labels[key] = append(labels[key], value)
- } else if l.numX != 0 || l.unitX != 0 {
- numValues := numLabels[key]
- units := numUnits[key]
- if l.unitX != 0 {
- var unit string
- unit, err = getString(p.stringTable, &l.unitX, err)
- units = padStringArray(units, len(numValues))
- numUnits[key] = append(units, unit)
+ numLocations += len(s.locationIDX)
+ }
+ locBuffer := make([]*Location, numLocations)
+
+ for _, s := range p.Sample {
+ if len(s.labelX) > 0 {
+ labels := make(map[string][]string, len(s.labelX))
+ numLabels := make(map[string][]int64, len(s.labelX))
+ numUnits := make(map[string][]string, len(s.labelX))
+ for _, l := range s.labelX {
+ var key, value string
+ key, err = getString(p.stringTable, &l.keyX, err)
+ if l.strX != 0 {
+ value, err = getString(p.stringTable, &l.strX, err)
+ labels[key] = append(labels[key], value)
+ } else if l.numX != 0 || l.unitX != 0 {
+ numValues := numLabels[key]
+ units := numUnits[key]
+ if l.unitX != 0 {
+ var unit string
+ unit, err = getString(p.stringTable, &l.unitX, err)
+ units = padStringArray(units, len(numValues))
+ numUnits[key] = append(units, unit)
+ }
+ numLabels[key] = append(numLabels[key], l.numX)
}
- numLabels[key] = append(numLabels[key], l.numX)
}
- }
- if len(labels) > 0 {
- s.Label = labels
- }
- if len(numLabels) > 0 {
- s.NumLabel = numLabels
- for key, units := range numUnits {
- if len(units) > 0 {
- numUnits[key] = padStringArray(units, len(numLabels[key]))
+ if len(labels) > 0 {
+ s.Label = labels
+ }
+ if len(numLabels) > 0 {
+ s.NumLabel = numLabels
+ for key, units := range numUnits {
+ if len(units) > 0 {
+ numUnits[key] = padStringArray(units, len(numLabels[key]))
+ }
}
+ s.NumUnit = numUnits
}
- s.NumUnit = numUnits
}
- s.Location = make([]*Location, len(s.locationIDX))
+
+ s.Location = locBuffer[:len(s.locationIDX)]
+ locBuffer = locBuffer[len(s.locationIDX):]
for i, lid := range s.locationIDX {
if lid < uint64(len(locationIds)) {
s.Location[i] = locationIds[lid]
// samples where at least one frame matches focus but none match ignore.
// Returns true is the corresponding regexp matched at least one sample.
func (p *Profile) FilterSamplesByName(focus, ignore, hide, show *regexp.Regexp) (fm, im, hm, hnm bool) {
+ if focus == nil && ignore == nil && hide == nil && show == nil {
+ fm = true // Missing focus implies a match
+ return
+ }
focusOrIgnore := make(map[uint64]bool)
hidden := make(map[uint64]bool)
for _, l := range p.Location {
// Recognize each thread and populate profile samples.
for !isMemoryMapSentinel(line) {
if strings.HasPrefix(line, "---- no stack trace for") {
- line = ""
break
}
if t := threadStartRE.FindStringSubmatch(line); len(t) != 4 {
package profile
import (
+ "encoding/binary"
"fmt"
"sort"
"strconv"
for _, src := range srcs {
// Clear the profile-specific hash tables
- pm.locationsByID = make(map[uint64]*Location, len(src.Location))
+ pm.locationsByID = makeLocationIDMap(len(src.Location))
pm.functionsByID = make(map[uint64]*Function, len(src.Function))
pm.mappingsByID = make(map[uint64]mapInfo, len(src.Mapping))
p *Profile
// Memoization tables within a profile.
- locationsByID map[uint64]*Location
+ locationsByID locationIDMap
functionsByID map[uint64]*Function
mappingsByID map[uint64]mapInfo
}
func (pm *profileMerger) mapSample(src *Sample) *Sample {
+ // Check memoization table
+ k := pm.sampleKey(src)
+ if ss, ok := pm.samples[k]; ok {
+ for i, v := range src.Value {
+ ss.Value[i] += v
+ }
+ return ss
+ }
+
+ // Make new sample.
s := &Sample{
Location: make([]*Location, len(src.Location)),
Value: make([]int64, len(src.Value)),
s.NumLabel[k] = vv
s.NumUnit[k] = uu
}
- // Check memoization table. Must be done on the remapped location to
- // account for the remapped mapping. Add current values to the
- // existing sample.
- k := s.key()
- if ss, ok := pm.samples[k]; ok {
- for i, v := range src.Value {
- ss.Value[i] += v
- }
- return ss
- }
copy(s.Value, src.Value)
pm.samples[k] = s
pm.p.Sample = append(pm.p.Sample, s)
return s
}
-// key generates sampleKey to be used as a key for maps.
-func (sample *Sample) key() sampleKey {
- ids := make([]string, len(sample.Location))
- for i, l := range sample.Location {
- ids[i] = strconv.FormatUint(l.ID, 16)
+func (pm *profileMerger) sampleKey(sample *Sample) sampleKey {
+ // Accumulate contents into a string.
+ var buf strings.Builder
+ buf.Grow(64) // Heuristic to avoid extra allocs
+
+ // encode a number
+ putNumber := func(v uint64) {
+ var num [binary.MaxVarintLen64]byte
+ n := binary.PutUvarint(num[:], v)
+ buf.Write(num[:n])
+ }
+
+ // encode a string prefixed with its length.
+ putDelimitedString := func(s string) {
+ putNumber(uint64(len(s)))
+ buf.WriteString(s)
+ }
+
+ for _, l := range sample.Location {
+ // Get the location in the merged profile, which may have a different ID.
+ if loc := pm.mapLocation(l); loc != nil {
+ putNumber(loc.ID)
+ }
}
+ putNumber(0) // Delimiter
- labels := make([]string, 0, len(sample.Label))
- for k, v := range sample.Label {
- labels = append(labels, fmt.Sprintf("%q%q", k, v))
+ for _, l := range sortedKeys1(sample.Label) {
+ putDelimitedString(l)
+ values := sample.Label[l]
+ putNumber(uint64(len(values)))
+ for _, v := range values {
+ putDelimitedString(v)
+ }
}
- sort.Strings(labels)
- numlabels := make([]string, 0, len(sample.NumLabel))
- for k, v := range sample.NumLabel {
- numlabels = append(numlabels, fmt.Sprintf("%q%x%x", k, v, sample.NumUnit[k]))
+ for _, l := range sortedKeys2(sample.NumLabel) {
+ putDelimitedString(l)
+ values := sample.NumLabel[l]
+ putNumber(uint64(len(values)))
+ for _, v := range values {
+ putNumber(uint64(v))
+ }
+ units := sample.NumUnit[l]
+ putNumber(uint64(len(units)))
+ for _, v := range units {
+ putDelimitedString(v)
+ }
}
- sort.Strings(numlabels)
- return sampleKey{
- strings.Join(ids, "|"),
- strings.Join(labels, ""),
- strings.Join(numlabels, ""),
+ return sampleKey(buf.String())
+}
+
+type sampleKey string
+
+// sortedKeys1 returns the sorted keys found in a string->[]string map.
+//
+// Note: this is currently non-generic since github pprof runs golint,
+// which does not support generics. When that issue is fixed, it can
+// be merged with sortedKeys2 and made into a generic function.
+func sortedKeys1(m map[string][]string) []string {
+ if len(m) == 0 {
+ return nil
}
+ keys := make([]string, 0, len(m))
+ for k := range m {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ return keys
}
-type sampleKey struct {
- locations string
- labels string
- numlabels string
+// sortedKeys2 returns the sorted keys found in a string->[]int64 map.
+//
+// Note: this is currently non-generic since github pprof runs golint,
+// which does not support generics. When that issue is fixed, it can
+// be merged with sortedKeys1 and made into a generic function.
+func sortedKeys2(m map[string][]int64) []string {
+ if len(m) == 0 {
+ return nil
+ }
+ keys := make([]string, 0, len(m))
+ for k := range m {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ return keys
}
func (pm *profileMerger) mapLocation(src *Location) *Location {
return nil
}
- if l, ok := pm.locationsByID[src.ID]; ok {
+ if l := pm.locationsByID.get(src.ID); l != nil {
return l
}
// account for the remapped mapping ID.
k := l.key()
if ll, ok := pm.locations[k]; ok {
- pm.locationsByID[src.ID] = ll
+ pm.locationsByID.set(src.ID, ll)
return ll
}
- pm.locationsByID[src.ID] = l
+ pm.locationsByID.set(src.ID, l)
pm.locations[k] = l
pm.p.Location = append(pm.p.Location, l)
return l
func equalValueType(st1, st2 *ValueType) bool {
return st1.Type == st2.Type && st1.Unit == st2.Unit
}
+
+// locationIDMap is like a map[uint64]*Location, but provides efficiency for
+// ids that are densely numbered, which is often the case.
+type locationIDMap struct {
+ dense []*Location // indexed by id for id < len(dense)
+ sparse map[uint64]*Location // indexed by id for id >= len(dense)
+}
+
+func makeLocationIDMap(n int) locationIDMap {
+ return locationIDMap{
+ dense: make([]*Location, n),
+ sparse: map[uint64]*Location{},
+ }
+}
+
+func (lm locationIDMap) get(id uint64) *Location {
+ if id < uint64(len(lm.dense)) {
+ return lm.dense[int(id)]
+ }
+ return lm.sparse[id]
+}
+
+func (lm locationIDMap) set(id uint64, loc *Location) {
+ if id < uint64(len(lm.dense)) {
+ lm.dense[id] = loc
+ return
+ }
+ lm.sparse[id] = loc
+}
+
+// CompatibilizeSampleTypes makes profiles compatible to be compared/merged. It
+// keeps sample types that appear in all profiles only and drops/reorders the
+// sample types as necessary.
+//
+// In the case of sample types order is not the same for given profiles the
+// order is derived from the first profile.
+//
+// Profiles are modified in-place.
+//
+// It returns an error if the sample type's intersection is empty.
+func CompatibilizeSampleTypes(ps []*Profile) error {
+ sTypes := commonSampleTypes(ps)
+ if len(sTypes) == 0 {
+ return fmt.Errorf("profiles have empty common sample type list")
+ }
+ for _, p := range ps {
+ if err := compatibilizeSampleTypes(p, sTypes); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// commonSampleTypes returns sample types that appear in all profiles in the
+// order how they ordered in the first profile.
+func commonSampleTypes(ps []*Profile) []string {
+ if len(ps) == 0 {
+ return nil
+ }
+ sTypes := map[string]int{}
+ for _, p := range ps {
+ for _, st := range p.SampleType {
+ sTypes[st.Type]++
+ }
+ }
+ var res []string
+ for _, st := range ps[0].SampleType {
+ if sTypes[st.Type] == len(ps) {
+ res = append(res, st.Type)
+ }
+ }
+ return res
+}
+
+// compatibilizeSampleTypes drops sample types that are not present in sTypes
+// list and reorder them if needed.
+//
+// It sets DefaultSampleType to sType[0] if it is not in sType list.
+//
+// It assumes that all sample types from the sTypes list are present in the
+// given profile otherwise it returns an error.
+func compatibilizeSampleTypes(p *Profile, sTypes []string) error {
+ if len(sTypes) == 0 {
+ return fmt.Errorf("sample type list is empty")
+ }
+ defaultSampleType := sTypes[0]
+ reMap, needToModify := make([]int, len(sTypes)), false
+ for i, st := range sTypes {
+ if st == p.DefaultSampleType {
+ defaultSampleType = p.DefaultSampleType
+ }
+ idx := searchValueType(p.SampleType, st)
+ if idx < 0 {
+ return fmt.Errorf("%q sample type is not found in profile", st)
+ }
+ reMap[i] = idx
+ if idx != i {
+ needToModify = true
+ }
+ }
+ if !needToModify && len(sTypes) == len(p.SampleType) {
+ return nil
+ }
+ p.DefaultSampleType = defaultSampleType
+ oldSampleTypes := p.SampleType
+ p.SampleType = make([]*ValueType, len(sTypes))
+ for i, idx := range reMap {
+ p.SampleType[i] = oldSampleTypes[idx]
+ }
+ values := make([]int64, len(sTypes))
+ for _, s := range p.Sample {
+ for i, idx := range reMap {
+ values[i] = s.Value[idx]
+ }
+ s.Value = s.Value[:len(values)]
+ copy(s.Value, values)
+ }
+ return nil
+}
+
+func searchValueType(vts []*ValueType, s string) int {
+ for i, vt := range vts {
+ if vt.Type == s {
+ return i
+ }
+ }
+ return -1
+}
"compress/gzip"
"fmt"
"io"
- "io/ioutil"
"math"
"path/filepath"
"regexp"
// may be a gzip-compressed encoded protobuf or one of many legacy
// profile formats which may be unsupported in the future.
func Parse(r io.Reader) (*Profile, error) {
- data, err := ioutil.ReadAll(r)
+ data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err == nil {
- data, err = ioutil.ReadAll(gz)
+ data, err = io.ReadAll(gz)
}
if err != nil {
return nil, fmt.Errorf("decompressing profile: %v", err)
)
type buffer struct {
- field int // field tag
- typ int // proto wire type code for field
- u64 uint64
- data []byte
- tmp [16]byte
+ field int // field tag
+ typ int // proto wire type code for field
+ u64 uint64
+ data []byte
+ tmp [16]byte
+ tmpLines []Line // temporary storage used while decoding "repeated Line".
}
type decoder func(*buffer, message) error
if b.typ == 2 {
// Packed encoding
data := b.data
- tmp := make([]int64, 0, len(data)) // Maximally sized
for len(data) > 0 {
var u uint64
var err error
if u, data, err = decodeVarint(data); err != nil {
return err
}
- tmp = append(tmp, int64(u))
+ *x = append(*x, int64(u))
}
- *x = append(*x, tmp...)
return nil
}
var i int64
if b.typ == 2 {
data := b.data
// Packed encoding
- tmp := make([]uint64, 0, len(data)) // Maximally sized
for len(data) > 0 {
var u uint64
var err error
if u, data, err = decodeVarint(data); err != nil {
return err
}
- tmp = append(tmp, u)
+ *x = append(*x, u)
}
- *x = append(*x, tmp...)
return nil
}
var u uint64
prune := make(map[uint64]bool)
pruneBeneath := make(map[uint64]bool)
+ // simplifyFunc can be expensive, so cache results.
+ // Note that the same function name can be encountered many times due
+ // different lines and addresses in the same function.
+ pruneCache := map[string]bool{} // Map from function to whether or not to prune
+ pruneFromHere := func(s string) bool {
+ if r, ok := pruneCache[s]; ok {
+ return r
+ }
+ funcName := simplifyFunc(s)
+ if dropRx.MatchString(funcName) {
+ if keepRx == nil || !keepRx.MatchString(funcName) {
+ pruneCache[s] = true
+ return true
+ }
+ }
+ pruneCache[s] = false
+ return false
+ }
+
for _, loc := range p.Location {
var i int
for i = len(loc.Line) - 1; i >= 0; i-- {
if fn := loc.Line[i].Function; fn != nil && fn.Name != "" {
- funcName := simplifyFunc(fn.Name)
- if dropRx.MatchString(funcName) {
- if keepRx == nil || !keepRx.MatchString(funcName) {
- break
- }
+ if pruneFromHere(fn.Name) {
+ break
}
}
}
-# github.com/google/pprof v0.0.0-20220729232143-a41b82acbcb1
-## explicit; go 1.17
+# github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26
+## explicit; go 1.18
github.com/google/pprof/driver
github.com/google/pprof/internal/binutils
github.com/google/pprof/internal/driver