]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/vendor/github.com/google/pprof: refresh from upstream
authorAlberto Donizetti <alb.donizetti@gmail.com>
Sun, 20 Aug 2017 10:27:32 +0000 (12:27 +0200)
committerBrad Fitzpatrick <bradfitz@golang.org>
Thu, 2 Nov 2017 23:51:45 +0000 (23:51 +0000)
Update vendored pprof to commit 4fc39a00b6b8c1aad05260f01429ec70e127252c
from github.com/google/pprof (2017-11-01).

Fixes #19380
Updates #21047

Change-Id: Ib64a94a45209039e5945acbcfa0392790c8ee41e
Reviewed-on: https://go-review.googlesource.com/57370
Run-TryBot: Alberto Donizetti <alb.donizetti@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
105 files changed:
src/cmd/vendor/github.com/google/pprof/.gitignore [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/CONTRIBUTING.md [moved from src/cmd/vendor/github.com/google/pprof/CONTRIBUTING with 81% similarity]
src/cmd/vendor/github.com/google/pprof/README.md
src/cmd/vendor/github.com/google/pprof/doc/developer/profile.proto.md
src/cmd/vendor/github.com/google/pprof/doc/pprof.md
src/cmd/vendor/github.com/google/pprof/driver/driver.go
src/cmd/vendor/github.com/google/pprof/internal/binutils/addr2liner.go
src/cmd/vendor/github.com/google/pprof/internal/binutils/addr2liner_llvm.go
src/cmd/vendor/github.com/google/pprof/internal/binutils/addr2liner_nm.go
src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils.go
src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils_test.go
src/cmd/vendor/github.com/google/pprof/internal/binutils/disasm_test.go
src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/fake-llvm-symbolizer [new file with mode: 0755]
src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/hello [new file with mode: 0755]
src/cmd/vendor/github.com/google/pprof/internal/driver/cli.go
src/cmd/vendor/github.com/google/pprof/internal/driver/commands.go
src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go
src/cmd/vendor/github.com/google/pprof/internal/driver/driver_focus.go
src/cmd/vendor/github.com/google/pprof/internal/driver/driver_test.go
src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go
src/cmd/vendor/github.com/google/pprof/internal/driver/fetch_test.go
src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/cppbench.contention [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/cppbench.small.contention [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.contention.cum.files.dot
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.contention.flat.addresses.dot.focus.ignore
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.call_tree.callgrind [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.comments
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.cum.lines.text.focus.hide [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.cum.lines.text.hide
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.cum.lines.text.show
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.cum.lines.topproto.hide
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.flat.addresses.disasm
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.flat.addresses.weblist
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.flat.functions.call_tree.dot [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.flat.functions.dot
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.flat.functions.text
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.peek
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.tags
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.tags.focus.ignore
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.traces
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap.cum.lines.tree.focus
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap.cum.relative_percentages.tree.focus
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap.flat.files.text.focus [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap.flat.inuse_objects.text
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap.flat.inuse_space.dot.focus
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap.flat.inuse_space.dot.focus.ignore
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap.flat.lines.dot.focus
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap.tags
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap.tags.unit
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_alloc.flat.alloc_objects.text
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_alloc.flat.alloc_space.dot [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_alloc.flat.alloc_space.dot.focus
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_alloc.flat.alloc_space.dot.hide
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_request.tags.focus [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_sizetags.dot [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_tags.traces [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.unknown.flat.functions.call_tree.text [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.unknown.flat.functions.text [deleted file]
src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/webui_test.go [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/elfexec/elfexec.go
src/cmd/vendor/github.com/google/pprof/internal/elfexec/elfexec_test.go
src/cmd/vendor/github.com/google/pprof/internal/graph/dotgraph.go
src/cmd/vendor/github.com/google/pprof/internal/graph/dotgraph_test.go
src/cmd/vendor/github.com/google/pprof/internal/graph/graph.go
src/cmd/vendor/github.com/google/pprof/internal/graph/graph_test.go
src/cmd/vendor/github.com/google/pprof/internal/graph/testdata/compose1.dot
src/cmd/vendor/github.com/google/pprof/internal/graph/testdata/compose2.dot
src/cmd/vendor/github.com/google/pprof/internal/graph/testdata/compose3.dot
src/cmd/vendor/github.com/google/pprof/internal/graph/testdata/compose4.dot
src/cmd/vendor/github.com/google/pprof/internal/graph/testdata/compose5.dot
src/cmd/vendor/github.com/google/pprof/internal/graph/testdata/compose6.dot [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/measurement/measurement.go
src/cmd/vendor/github.com/google/pprof/internal/measurement/measurement_test.go [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/plugin/plugin.go
src/cmd/vendor/github.com/google/pprof/internal/proftest/proftest.go
src/cmd/vendor/github.com/google/pprof/internal/report/report.go
src/cmd/vendor/github.com/google/pprof/internal/report/report_test.go
src/cmd/vendor/github.com/google/pprof/internal/report/source.go
src/cmd/vendor/github.com/google/pprof/internal/report/source_html.go
src/cmd/vendor/github.com/google/pprof/internal/report/source_test.go [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/report/testdata/README.md [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/report/testdata/sample.bin [new file with mode: 0755]
src/cmd/vendor/github.com/google/pprof/internal/report/testdata/sample.cpu [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/report/testdata/sample/sample.go [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/report/testdata/source.dot
src/cmd/vendor/github.com/google/pprof/internal/symbolizer/symbolizer.go
src/cmd/vendor/github.com/google/pprof/internal/symbolizer/symbolizer_test.go
src/cmd/vendor/github.com/google/pprof/internal/symbolz/symbolz.go
src/cmd/vendor/github.com/google/pprof/internal/symbolz/symbolz_test.go
src/cmd/vendor/github.com/google/pprof/profile/encode.go
src/cmd/vendor/github.com/google/pprof/profile/filter.go
src/cmd/vendor/github.com/google/pprof/profile/legacy_java_profile.go
src/cmd/vendor/github.com/google/pprof/profile/merge.go
src/cmd/vendor/github.com/google/pprof/profile/profile.go
src/cmd/vendor/github.com/google/pprof/profile/profile_test.go
src/cmd/vendor/github.com/google/pprof/profile/proto.go
src/cmd/vendor/github.com/google/pprof/profile/proto_test.go
src/cmd/vendor/github.com/google/pprof/profile/prune.go
src/cmd/vendor/github.com/google/pprof/profile/prune_test.go
src/cmd/vendor/github.com/google/pprof/proto/profile.proto
src/cmd/vendor/github.com/google/pprof/test.sh [new file with mode: 0755]
src/cmd/vendor/vendor.json

diff --git a/src/cmd/vendor/github.com/google/pprof/.gitignore b/src/cmd/vendor/github.com/google/pprof/.gitignore
new file mode 100644 (file)
index 0000000..5a0c00f
--- /dev/null
@@ -0,0 +1,8 @@
+.DS_Store
+*~
+*.orig
+*.exe
+.*.swp
+core
+coverage.txt
+pprof
similarity index 81%
rename from src/cmd/vendor/github.com/google/pprof/CONTRIBUTING
rename to src/cmd/vendor/github.com/google/pprof/CONTRIBUTING.md
index 2827b7d3fa277e2daab95ea3cfaff1c2bfc1389e..5976686d734749cfe1429e8eee7ca8d065d18e6c 100644 (file)
@@ -1,9 +1,9 @@
 Want to contribute? Great! First, read this page (including the small print at the end).
 
 ### Before you contribute
+
 Before we can use your code, you must sign the
-[Google Individual Contributor License Agreement]
-(https://cla.developers.google.com/about/google-individual)
+[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)
 (CLA), which you can do online. The CLA is necessary mainly because you own the
 copyright to your changes, even after your contribution becomes part of our
 codebase, so we need your permission to use and distribute your code. We also
@@ -17,11 +17,11 @@ possibly guide you. Coordinating up front makes it much easier to avoid
 frustration later on.
 
 ### Code reviews
-All submissions, including submissions by project members, require review. We
-use Github pull requests for this purpose.
+
+All submissions, including submissions by project members, require review.  
+We use Github pull requests for this purpose.
 
 ### The small print
-Contributions made by corporations are covered by a different agreement than
-the one above, the
-[Software Grant and Corporate Contributor License Agreement]
-(https://cla.developers.google.com/about/google-corporate).
+
+Contributions made by corporations are covered by a different agreement than the one above,  
+the [Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
index a134872079ded54d67b01329202afacb9b4360ac..e8c2f0329da01957a3322fbaf640f1d60f7a039e 100644 (file)
@@ -1,3 +1,6 @@
+[![Build Status](https://travis-ci.org/google/pprof.svg?branch=master)](https://travis-ci.org/google/pprof)
+[![codecov](https://codecov.io/gh/google/pprof/graph/badge.svg)](https://codecov.io/gh/google/pprof)
+
 # Introduction
 
 pprof is a tool for visualization and analysis of profiling data.
@@ -24,7 +27,7 @@ them through the use of the native binutils tools (addr2line and nm).
 
 Prerequisites:
 
-- Go development kit. Known to work with Go 1.5.
+- Go development kit. Requires Go 1.7 or newer.
   Follow [these instructions](http://golang.org/doc/code.html) to install the 
   go tool and set up GOPATH.
 
@@ -35,6 +38,10 @@ To build and install it, use the `go get` tool.
 
     go get github.com/google/pprof
 
+Remember to set GOPATH to the directory where you want pprof to be
+installed.  The binary will be in `$GOPATH/bin` and the sources under
+`$GOPATH/src/github.com/google/pprof`.
+
 # Basic usage
 
 pprof can read a profile from a file or directly from a server via http.
@@ -70,12 +77,28 @@ This will open a simple shell that takes pprof commands to generate reports.
 Type 'help' for available commands/options.
 ```
 
+## Run pprof via a web interface
+
+If the `-http` flag is specified, pprof starts a web server at
+the specified host:port that provides an interactive web-based interface to pprof.
+Host is optional, and is "localhost" by default. Port is optional, and is a
+random available port by default. `-http=":"` starts a server locally at
+a random port.
+
+```
+pprof -http=[host]:[port] [main_binary] profile.pb.gz
+```
+
+The preceding command should automatically open your web browser at
+the right page; if not, you can manually visit the specified port in
+your web browser.
+
 ## Using pprof with Linux Perf
 
 pprof can read `perf.data` files generated by the
-[Linux perf](https://perf.wiki.kernel.org/index.php) tool by using the
+[Linux perf](https://perf.wiki.kernel.org/index.php/Main_Page) tool by using the
 `perf_to_profile` program from the
-[perf_data_converter](http://github.com/google/perf_data_converter) package.
+[perf_data_converter](https://github.com/google/perf_data_converter) package.
 
 ## Further documentation
 
index 9932e7e20cc2e7c01d85fe1db94b2e1e3559d296..d3b0b84191962f299ea1035f89c9d33a305fa87d 100644 (file)
@@ -128,9 +128,11 @@ size of 6MB.
 
 Labels can be string-based or numeric. They are represented by the Label
 message, with a key identifying the label and either a string or numeric
-value. For numeric labels, by convention the key represents the measurement unit
-of the numeric value. So for the previous example, the samples would have labels
-{“bytes”, 2097152} and {“bytes”, 4194304}.
+value. For numeric labels, the measurement unit can be specified in the profile.
+If no unit is specified and the key is "request" or "alignment",
+then the units are assumed to be "bytes". Otherwise when no unit is specified
+the key will be used as the measurement unit of the numeric value. All tags with
+the same key should have the same unit.
 
 ## Keep and drop expressions
 
index d2c7e26e311875cf5055bf1de9fd4477037ef4b2..57613fad9637a6e3ee9bb0769d1b81584706f75b 100644 (file)
@@ -29,7 +29,40 @@ location. pprof is agnostic to the profile semantics, so other uses are
 possible. The interpretation of the reports generated by pprof depends on the
 semantics defined by the source of the profile.
 
-# General usage
+# Usage Modes
+
+There are few different ways of using `pprof`.
+
+## Report generation
+
+If a report format is requested on the command line:
+
+    pprof <format> [options] source
+
+pprof will generate a report in the specified format and exit.
+Formats can be either text, or graphical. See below for details about
+supported formats, options, and sources.
+
+## Interactive terminal use
+
+Without a format specifier:
+
+    pprof [options] source
+
+pprof will start an interactive shell in which the user can type
+commands.  Type `help` to get online help.
+
+## Web interface
+
+If a host:port is specified on the command line:
+
+    pprof -http=[host]:[port] [options] source
+
+pprof will start serving HTTP requests on the specified port.  Visit
+the HTTP url corresponding to the port (typically `http://<host>:<port>/`)
+in a browser to see the interface.
+
+# Details
 
 The objective of pprof is to generate a report for a profile. The report is
 generated from a location hierarchy, which is reconstructed from the profile
@@ -38,14 +71,12 @@ itself, while *cum* is the value of the location plus all its
 descendants. Samples that include a location multiple times (eg for recursive
 functions) are counted only once per location.
 
-The basic usage of pprof is
-
-    pprof <format> [options] source
+## Options
 
-Where *format* selects the nature of the report, and *options* configure the
-contents of the report. Each option has a value, which can be boolean, numeric,
-or strings. While only one format can be specified, most options can be selected
-independently of each other.
+*options* configure the contents of a report. Each option has a value,
+which can be boolean, numeric, or strings. While only one format can
+be specified, most options can be selected independently of each
+other.
 
 Some common pprof options are:
 
@@ -74,10 +105,56 @@ number of values - 1) or the name of the sample value.
 
 Sample values are numeric values associated to a unit. If pprof can recognize
 these units, it will attempt to scale the values to a suitable unit for
-visualization. The `unite=` option will force the use of a specific unit. For
-example, `sample_index=sec` will force any time values to be reported in
+visualization. The `unit=` option will force the use of a specific unit. For
+example, `unit=sec` will force any time values to be reported in
 seconds. pprof recognizes most common time and memory size units.
 
+## Tag filtering
+
+Samples in a profile may have tags. These tags have a name and a value; this
+value can be either numeric or a string. pprof can select samples from a
+profile based on these tags using the `-tagfocus` and `-tagignore` options.
+
+Generally, these options work as follows:
+* **-tagfocus=_regex_** or **-tagfocus=_range_:** Restrict to samples with tags
+  matched by regexp or in range.
+* **-tagignore=_regex_** or **-tagignore=_range_:** Discard samples with tags
+  matched by regexp or in range.
+
+When using `-tagfocus=regex` and `-tagignore=regex`, the regex will be compared
+to each value associated with each tag. If one specifies a value
+like `regex1,regex2`, then only samples with a tag value matching `regex1`
+and a tag value matching `regex2` will be kept.
+
+In addition to being able to filter on tag values, one can specify the name of
+the tag which a certain value must be associated with using the notation
+`-tagfocus=tagName=value`. Here, the `tagName` must match the tag's name
+exactly, and the value can be either a regex or a range. If one specifies
+a value like `regex1,regex2`, then samples with a tag value (paired with the
+specified tag name) matching either `regex1` or matching `regex2` will match.
+
+Here are examples explaining how `tagfocus` can be used:
+
+* `-tagfocus 128kb:512kb` accepts a sample iff it has any numeric tag with
+  memory value in the specified range.
+* `-tagfocus mytag=128kb:512kb` accepts a sample iff it has a numeric tag
+  `mytag` with memory value in the specified range. There isn't a way to say
+   `-tagfocus mytag=128kb:512kb,16kb:32kb`
+   or `-tagfocus mytag=128kb:512kb,mytag2=128kb:512kb`. Just single value or
+   range for numeric tags.
+* `-tagfocus someregex` accepts a sample iff it has any string tag with
+  `tagName:tagValue` string matching specified regexp. In the future, this
+  will change to accept sample iff it has any string tag with `tagValue` string
+  matching specified regexp.
+* `-tagfocus mytag=myvalue1,myvalue2` matches if either of the two tag values
+  are present.
+
+`-tagignore` works similarly, except that it discards matching samples, instead
+of keeping them.
+
+If both the `-tagignore` and `-tagfocus` expressions (either a regexp or a
+range) match a given sample, then the sample will be discarded.
+
 ## Text reports
 
 pprof text reports show the location hierarchy in text format.
index d01d0fa344003d9e85a56515cd8c08205309cb55..ee7d67d7843defdcb267b2c66e965b47b1556f28 100644 (file)
@@ -29,10 +29,10 @@ import (
 // manager. Then it generates a report formatted according to the
 // options selected through the flags package.
 func PProf(o *Options) error {
-       return internaldriver.PProf(o.InternalOptions())
+       return internaldriver.PProf(o.internalOptions())
 }
 
-func (o *Options) InternalOptions() *plugin.Options {
+func (o *Options) internalOptions() *plugin.Options {
        var obj plugin.ObjTool
        if o.Obj != nil {
                obj = &internalObjTool{o.Obj}
@@ -273,9 +273,9 @@ type internalSymbolizer struct {
 }
 
 func (s *internalSymbolizer) Symbolize(mode string, srcs plugin.MappingSources, prof *profile.Profile) error {
-       isrcs := plugin.MappingSources{}
+       isrcs := MappingSources{}
        for m, s := range srcs {
                isrcs[m] = s
        }
-       return s.Symbolize(mode, isrcs, prof)
+       return s.Symbolizer.Symbolize(mode, isrcs, prof)
 }
index e3a7777253a2b3ff168bff2c3996db122cee740d..71e471b5d6ace9947aee35959ef07f28176d6906 100644 (file)
@@ -21,6 +21,7 @@ import (
        "os/exec"
        "strconv"
        "strings"
+       "sync"
 
        "github.com/google/pprof/internal/plugin"
 )
@@ -36,6 +37,7 @@ const (
 // addr2Liner is a connection to an addr2line command for obtaining
 // address and line number information from a binary.
 type addr2Liner struct {
+       mu   sync.Mutex
        rw   lineReaderWriter
        base uint64
 
@@ -170,9 +172,10 @@ func (d *addr2Liner) readFrame() (plugin.Frame, bool) {
                Line: linenumber}, false
 }
 
-// addrInfo returns the stack frame information for a specific program
-// address. It returns nil if the address could not be identified.
-func (d *addr2Liner) addrInfo(addr uint64) ([]plugin.Frame, error) {
+func (d *addr2Liner) rawAddrInfo(addr uint64) ([]plugin.Frame, error) {
+       d.mu.Lock()
+       defer d.mu.Unlock()
+
        if err := d.rw.write(fmt.Sprintf("%x", addr-d.base)); err != nil {
                return nil, err
        }
@@ -201,6 +204,16 @@ func (d *addr2Liner) addrInfo(addr uint64) ([]plugin.Frame, error) {
                        stack = append(stack, frame)
                }
        }
+       return stack, err
+}
+
+// addrInfo returns the stack frame information for a specific program
+// address. It returns nil if the address could not be identified.
+func (d *addr2Liner) addrInfo(addr uint64) ([]plugin.Frame, error) {
+       stack, err := d.rawAddrInfo(addr)
+       if err != nil {
+               return nil, err
+       }
 
        // Get better name from nm if possible.
        if len(stack) > 0 && d.nm != nil {
index 7692b0a5cb252da39b0bc5a55a5b25f89dc12608..68fa5593ad1c80f01267472c325c444e0bc656d8 100644 (file)
@@ -21,6 +21,7 @@ import (
        "os/exec"
        "strconv"
        "strings"
+       "sync"
 
        "github.com/google/pprof/internal/plugin"
 )
@@ -32,6 +33,7 @@ const (
 // llvmSymbolizer is a connection to an llvm-symbolizer command for
 // obtaining address and line number information from a binary.
 type llvmSymbolizer struct {
+       sync.Mutex
        filename string
        rw       lineReaderWriter
        base     uint64
@@ -150,6 +152,9 @@ func (d *llvmSymbolizer) readFrame() (plugin.Frame, bool) {
 // addrInfo returns the stack frame information for a specific program
 // address. It returns nil if the address could not be identified.
 func (d *llvmSymbolizer) addrInfo(addr uint64) ([]plugin.Frame, error) {
+       d.Lock()
+       defer d.Unlock()
+
        if err := d.rw.write(fmt.Sprintf("%s 0x%x", d.filename, addr-d.base)); err != nil {
                return nil, err
        }
index e7a8e10b3405a08d94ba4fedf7a47be079634be5..1987bd3dabab36b58837b0b795a988b0d737bc5e 100644 (file)
@@ -48,22 +48,23 @@ func newAddr2LinerNM(cmd, file string, base uint64) (*addr2LinerNM, error) {
        if cmd == "" {
                cmd = defaultNM
        }
-
-       a := &addr2LinerNM{
-               m: []symbolInfo{},
-       }
-
        var b bytes.Buffer
        c := exec.Command(cmd, "-n", file)
        c.Stdout = &b
-
        if err := c.Run(); err != nil {
                return nil, err
        }
+       return parseAddr2LinerNM(base, &b)
+}
+
+func parseAddr2LinerNM(base uint64, nm io.Reader) (*addr2LinerNM, error) {
+       a := &addr2LinerNM{
+               m: []symbolInfo{},
+       }
 
        // Parse nm output and populate symbol map.
        // Skip lines we fail to parse.
-       buf := bufio.NewReader(&b)
+       buf := bufio.NewReader(nm)
        for {
                line, err := buf.ReadString('\n')
                if line == "" && err != nil {
index 9854c9a262d48a48f08bceb28b1a2045203b6aa5..9a82cb8e925395683c4150ad2b3aff72a8209132 100644 (file)
@@ -24,14 +24,21 @@ import (
        "path/filepath"
        "regexp"
        "strings"
+       "sync"
 
        "github.com/google/pprof/internal/elfexec"
        "github.com/google/pprof/internal/plugin"
 )
 
 // A Binutils implements plugin.ObjTool by invoking the GNU binutils.
-// SetConfig must be called before any of the other methods.
 type Binutils struct {
+       mu  sync.Mutex
+       rep *binrep
+}
+
+// binrep is an immutable representation for Binutils.  It is atomically
+// replaced on every mutation to provide thread-safe access.
+type binrep struct {
        // Commands to invoke.
        llvmSymbolizer      string
        llvmSymbolizerFound bool
@@ -47,11 +54,38 @@ type Binutils struct {
        fast bool
 }
 
+// get returns the current representation for bu, initializing it if necessary.
+func (bu *Binutils) get() *binrep {
+       bu.mu.Lock()
+       r := bu.rep
+       if r == nil {
+               r = &binrep{}
+               initTools(r, "")
+               bu.rep = r
+       }
+       bu.mu.Unlock()
+       return r
+}
+
+// update modifies the rep for bu via the supplied function.
+func (bu *Binutils) update(fn func(r *binrep)) {
+       r := &binrep{}
+       bu.mu.Lock()
+       defer bu.mu.Unlock()
+       if bu.rep == nil {
+               initTools(r, "")
+       } else {
+               *r = *bu.rep
+       }
+       fn(r)
+       bu.rep = r
+}
+
 // SetFastSymbolization sets a toggle that makes binutils use fast
 // symbolization (using nm), which is much faster than addr2line but
 // provides only symbol name information (no file/line).
-func (b *Binutils) SetFastSymbolization(fast bool) {
-       b.fast = fast
+func (bu *Binutils) SetFastSymbolization(fast bool) {
+       bu.update(func(r *binrep) { r.fast = fast })
 }
 
 // SetTools processes the contents of the tools option. It
@@ -59,7 +93,11 @@ func (b *Binutils) SetFastSymbolization(fast bool) {
 // of the form t:path, where cmd will be used to look only for the
 // tool named t. If t is not specified, the path is searched for all
 // tools.
-func (b *Binutils) SetTools(config string) {
+func (bu *Binutils) SetTools(config string) {
+       bu.update(func(r *binrep) { initTools(r, config) })
+}
+
+func initTools(b *binrep, config string) {
        // paths collect paths per tool; Key "" contains the default.
        paths := make(map[string][]string)
        for _, t := range strings.Split(config, ",") {
@@ -91,11 +129,8 @@ func findExe(cmd string, paths []string) (string, bool) {
 
 // Disasm returns the assembly instructions for the specified address range
 // of a binary.
-func (b *Binutils) Disasm(file string, start, end uint64) ([]plugin.Inst, error) {
-       if b.addr2line == "" {
-               // Update the command invocations if not initialized.
-               b.SetTools("")
-       }
+func (bu *Binutils) Disasm(file string, start, end uint64) ([]plugin.Inst, error) {
+       b := bu.get()
        cmd := exec.Command(b.objdump, "-d", "-C", "--no-show-raw-insn", "-l",
                fmt.Sprintf("--start-address=%#x", start),
                fmt.Sprintf("--stop-address=%#x", end),
@@ -109,11 +144,8 @@ func (b *Binutils) Disasm(file string, start, end uint64) ([]plugin.Inst, error)
 }
 
 // Open satisfies the plugin.ObjTool interface.
-func (b *Binutils) Open(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
-       if b.addr2line == "" {
-               // Update the command invocations if not initialized.
-               b.SetTools("")
-       }
+func (bu *Binutils) Open(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
+       b := bu.get()
 
        // Make sure file is a supported executable.
        // The pprof driver uses Open to sniff the difference
@@ -140,7 +172,7 @@ func (b *Binutils) Open(name string, start, limit, offset uint64) (plugin.ObjFil
        return nil, fmt.Errorf("unrecognized binary: %s", name)
 }
 
-func (b *Binutils) openMachO(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
+func (b *binrep) openMachO(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
        of, err := macho.Open(name)
        if err != nil {
                return nil, fmt.Errorf("Parsing %s: %v", name, err)
@@ -153,7 +185,7 @@ func (b *Binutils) openMachO(name string, start, limit, offset uint64) (plugin.O
        return &fileAddr2Line{file: file{b: b, name: name}}, nil
 }
 
-func (b *Binutils) openELF(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
+func (b *binrep) openELF(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
        ef, err := elf.Open(name)
        if err != nil {
                return nil, fmt.Errorf("Parsing %s: %v", name, err)
@@ -202,7 +234,7 @@ func (b *Binutils) openELF(name string, start, limit, offset uint64) (plugin.Obj
 
 // file implements the binutils.ObjFile interface.
 type file struct {
-       b       *Binutils
+       b       *binrep
        name    string
        base    uint64
        buildID string
@@ -263,22 +295,27 @@ func (f *fileNM) SourceLine(addr uint64) ([]plugin.Frame, error) {
 // information). It can be slow for large binaries with debug
 // information.
 type fileAddr2Line struct {
+       once sync.Once
        file
        addr2liner     *addr2Liner
        llvmSymbolizer *llvmSymbolizer
 }
 
 func (f *fileAddr2Line) SourceLine(addr uint64) ([]plugin.Frame, error) {
+       f.once.Do(f.init)
        if f.llvmSymbolizer != nil {
                return f.llvmSymbolizer.addrInfo(addr)
        }
        if f.addr2liner != nil {
                return f.addr2liner.addrInfo(addr)
        }
+       return nil, fmt.Errorf("could not find local addr2liner")
+}
 
+func (f *fileAddr2Line) init() {
        if llvmSymbolizer, err := newLLVMSymbolizer(f.b.llvmSymbolizer, f.name, f.base); err == nil {
                f.llvmSymbolizer = llvmSymbolizer
-               return f.llvmSymbolizer.addrInfo(addr)
+               return
        }
 
        if addr2liner, err := newAddr2Liner(f.b.addr2line, f.name, f.base); err == nil {
@@ -290,13 +327,14 @@ func (f *fileAddr2Line) SourceLine(addr uint64) ([]plugin.Frame, error) {
                if nm, err := newAddr2LinerNM(f.b.nm, f.name, f.base); err == nil {
                        f.addr2liner.nm = nm
                }
-               return f.addr2liner.addrInfo(addr)
        }
-
-       return nil, fmt.Errorf("could not find local addr2liner")
 }
 
 func (f *fileAddr2Line) Close() error {
+       if f.llvmSymbolizer != nil {
+               f.llvmSymbolizer.rw.close()
+               f.llvmSymbolizer = nil
+       }
        if f.addr2liner != nil {
                f.addr2liner.rw.close()
                f.addr2liner = nil
index b0ba5f67a86663fba74c19976923600c616bea71..989a290071877bda301780e19d99b595f729c0b3 100644 (file)
 package binutils
 
 import (
+       "bytes"
        "fmt"
+       "math"
+       "path/filepath"
+       "reflect"
+       "regexp"
+       "runtime"
        "testing"
 
        "github.com/google/pprof/internal/plugin"
@@ -37,7 +43,7 @@ func functionName(level int) (name string) {
 func TestAddr2Liner(t *testing.T) {
        const offset = 0x500
 
-       a := addr2Liner{&mockAddr2liner{}, offset, nil}
+       a := addr2Liner{rw: &mockAddr2liner{}, base: offset}
        for i := 1; i < 8; i++ {
                addr := i*0x1000 + offset
                s, err := a.addrInfo(uint64(addr))
@@ -112,24 +118,23 @@ func (a *mockAddr2liner) close() {
 }
 
 func TestAddr2LinerLookup(t *testing.T) {
-       oddSizedMap := addr2LinerNM{
-               m: []symbolInfo{
-                       {0x1000, "0x1000"},
-                       {0x2000, "0x2000"},
-                       {0x3000, "0x3000"},
-               },
-       }
-       evenSizedMap := addr2LinerNM{
-               m: []symbolInfo{
-                       {0x1000, "0x1000"},
-                       {0x2000, "0x2000"},
-                       {0x3000, "0x3000"},
-                       {0x4000, "0x4000"},
-               },
-       }
-       for _, a := range []*addr2LinerNM{
-               &oddSizedMap, &evenSizedMap,
-       } {
+       const oddSizedData = `
+00001000 T 0x1000
+00002000 T 0x2000
+00003000 T 0x3000
+`
+       const evenSizedData = `
+0000000000001000 T 0x1000
+0000000000002000 T 0x2000
+0000000000003000 T 0x3000
+0000000000004000 T 0x4000
+`
+       for _, d := range []string{oddSizedData, evenSizedData} {
+               a, err := parseAddr2LinerNM(0, bytes.NewBufferString(d))
+               if err != nil {
+                       t.Errorf("nm parse error: %v", err)
+                       continue
+               }
                for address, want := range map[uint64]string{
                        0x1000: "0x1000",
                        0x1001: "0x1000",
@@ -141,6 +146,11 @@ func TestAddr2LinerLookup(t *testing.T) {
                                t.Errorf("%x: got %v, want %s", address, got, want)
                        }
                }
+               for _, unknown := range []uint64{0x0fff, 0x4001} {
+                       if got, _ := a.addrInfo(unknown); got != nil {
+                               t.Errorf("%x: got %v, want nil", unknown, got)
+                       }
+               }
        }
 }
 
@@ -150,3 +160,116 @@ func checkAddress(got []plugin.Frame, address uint64, want string) bool {
        }
        return got[0].Func == want
 }
+
+func TestSetTools(t *testing.T) {
+       // Test that multiple calls work.
+       bu := &Binutils{}
+       bu.SetTools("")
+       bu.SetTools("")
+}
+
+func TestSetFastSymbolization(t *testing.T) {
+       // Test that multiple calls work.
+       bu := &Binutils{}
+       bu.SetFastSymbolization(true)
+       bu.SetFastSymbolization(false)
+}
+
+func skipUnlessLinuxAmd64(t *testing.T) {
+       if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
+               t.Skip("Disasm only tested on x86-64 linux")
+       }
+}
+
+func TestDisasm(t *testing.T) {
+       skipUnlessLinuxAmd64(t)
+       bu := &Binutils{}
+       insts, err := bu.Disasm(filepath.Join("testdata", "hello"), 0, math.MaxUint64)
+       if err != nil {
+               t.Fatalf("Disasm: unexpected error %v", err)
+       }
+       mainCount := 0
+       for _, x := range insts {
+               if x.Function == "main" {
+                       mainCount++
+               }
+       }
+       if mainCount == 0 {
+               t.Error("Disasm: found no main instructions")
+       }
+}
+
+func TestObjFile(t *testing.T) {
+       skipUnlessLinuxAmd64(t)
+       bu := &Binutils{}
+       f, err := bu.Open(filepath.Join("testdata", "hello"), 0, math.MaxUint64, 0)
+       if err != nil {
+               t.Fatalf("Open: unexpected error %v", err)
+       }
+       defer f.Close()
+       syms, err := f.Symbols(regexp.MustCompile("main"), 0)
+       if err != nil {
+               t.Fatalf("Symbols: unexpected error %v", err)
+       }
+
+       find := func(name string) *plugin.Sym {
+               for _, s := range syms {
+                       for _, n := range s.Name {
+                               if n == name {
+                                       return s
+                               }
+                       }
+               }
+               return nil
+       }
+       m := find("main")
+       if m == nil {
+               t.Fatalf("Symbols: did not find main")
+       }
+       frames, err := f.SourceLine(m.Start)
+       if err != nil {
+               t.Fatalf("SourceLine: unexpected error %v", err)
+       }
+       expect := []plugin.Frame{
+               {Func: "main", File: "/tmp/hello.c", Line: 3},
+       }
+       if !reflect.DeepEqual(frames, expect) {
+               t.Fatalf("SourceLine for main: expect %v; got %v\n", expect, frames)
+       }
+}
+
+func TestLLVMSymbolizer(t *testing.T) {
+       if runtime.GOOS != "linux" {
+               t.Skip("testtdata/llvm-symbolizer has only been tested on linux")
+       }
+
+       cmd := filepath.Join("testdata", "fake-llvm-symbolizer")
+       symbolizer, err := newLLVMSymbolizer(cmd, "foo", 0)
+       if err != nil {
+               t.Fatalf("newLLVMSymbolizer: unexpected error %v", err)
+       }
+       defer symbolizer.rw.close()
+
+       for _, c := range []struct {
+               addr   uint64
+               frames []plugin.Frame
+       }{
+               {0x10, []plugin.Frame{
+                       {Func: "Inlined_0x10", File: "foo.h", Line: 0},
+                       {Func: "Func_0x10", File: "foo.c", Line: 2},
+               }},
+               {0x20, []plugin.Frame{
+                       {Func: "Inlined_0x20", File: "foo.h", Line: 0},
+                       {Func: "Func_0x20", File: "foo.c", Line: 2},
+               }},
+       } {
+               frames, err := symbolizer.addrInfo(c.addr)
+               if err != nil {
+                       t.Errorf("LLVM: unexpected error %v", err)
+                       continue
+               }
+               if !reflect.DeepEqual(frames, c.frames) {
+                       t.Errorf("LLVM: expect %v; got %v\n", c.frames, frames)
+               }
+       }
+}
index 7fc25741ce2c87f174b1d689c475b35c8d843bfc..3563198f48f851048634edd18d98c75148987a1f 100644 (file)
@@ -73,7 +73,7 @@ func TestFindSymbols(t *testing.T) {
 
 func checkSymbol(got []*plugin.Sym, want []plugin.Sym) error {
        if len(got) != len(want) {
-               return fmt.Errorf("unexpected number of symbols %d (want %d)\n", len(got), len(want))
+               return fmt.Errorf("unexpected number of symbols %d (want %d)", len(got), len(want))
        }
 
        for i, g := range got {
@@ -134,8 +134,6 @@ func TestFunctionAssembly(t *testing.T) {
                },
        }
 
-       const objdump = "testdata/wrapper/objdump"
-
        for _, tc := range testcases {
                insts, err := disassemble([]byte(tc.asm))
                if err != nil {
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/fake-llvm-symbolizer b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/fake-llvm-symbolizer
new file mode 100755 (executable)
index 0000000..596713c
--- /dev/null
@@ -0,0 +1,34 @@
+#!/bin/sh
+#
+# Copyright 2014 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.
+#
+# Fake llvm-symbolizer to use in tests
+
+set -f
+IFS=" "
+
+while read line; do
+  # line has form:
+  #    filename 0xaddr
+  # Emit dummy output that matches llvm-symbolizer output format.
+  set -- $line
+  fname=$1
+  addr=$2
+  echo "Inlined_$addr"
+  echo "$fname.h"
+  echo "Func_$addr"
+  echo "$fname.c:2"
+  echo
+done
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/hello b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/hello
new file mode 100755 (executable)
index 0000000..d86dc7c
Binary files /dev/null and b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/hello differ
index 0005ead70bb33f721dd5dccb129b9cc35fc044cc..e2e8c6c93684d9c594de92e3490e0c99949f40db 100644 (file)
@@ -24,14 +24,17 @@ import (
 )
 
 type source struct {
-       Sources  []string
-       ExecName string
-       BuildID  string
-       Base     []string
-
-       Seconds   int
-       Timeout   int
-       Symbolize string
+       Sources   []string
+       ExecName  string
+       BuildID   string
+       Base      []string
+       Normalize bool
+
+       Seconds      int
+       Timeout      int
+       Symbolize    string
+       HTTPHostport string
+       Comment      string
 }
 
 // Parse parses the command lines through the specified flags package
@@ -41,9 +44,11 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
        flag := o.Flagset
        // Comparisons.
        flagBase := flag.StringList("base", "", "Source for base profile for comparison")
-       // Internal options.
+       // Source options.
        flagSymbolize := flag.String("symbolize", "", "Options for profile symbolization")
        flagBuildID := flag.String("buildid", "", "Override build id for first mapping")
+       flagTimeout := flag.Int("timeout", -1, "Timeout in seconds for fetching a profile")
+       flagAddComment := flag.String("add_comment", "", "Annotation string to record in the profile")
        // CPU profile options
        flagSeconds := flag.Int("seconds", -1, "Length of time for dynamic profiles")
        // Heap profile options
@@ -57,7 +62,7 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
        flagMeanDelay := flag.Bool("mean_delay", false, "Display mean delay at each region")
        flagTools := flag.String("tools", os.Getenv("PPROF_TOOLS"), "Path for object tool pathnames")
 
-       flagTimeout := flag.Int("timeout", -1, "Timeout in seconds for fetching a profile")
+       flagHTTP := flag.String("http", "", "Present interactive web based UI at the specified http host:port")
 
        // Flags used during command processing
        installedFlags := installFlags(flag)
@@ -106,6 +111,9 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
        if err != nil {
                return nil, nil, err
        }
+       if cmd != nil && *flagHTTP != "" {
+               return nil, nil, fmt.Errorf("-http is not compatible with an output format on the command line")
+       }
 
        si := pprofVariables["sample_index"].value
        si = sampleIndex(flagTotalDelay, si, "delay", "-total_delay", o.UI)
@@ -122,12 +130,14 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
        }
 
        source := &source{
-               Sources:   args,
-               ExecName:  execName,
-               BuildID:   *flagBuildID,
-               Seconds:   *flagSeconds,
-               Timeout:   *flagTimeout,
-               Symbolize: *flagSymbolize,
+               Sources:      args,
+               ExecName:     execName,
+               BuildID:      *flagBuildID,
+               Seconds:      *flagSeconds,
+               Timeout:      *flagTimeout,
+               Symbolize:    *flagSymbolize,
+               HTTPHostport: *flagHTTP,
+               Comment:      *flagAddComment,
        }
 
        for _, s := range *flagBase {
@@ -136,6 +146,12 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
                }
        }
 
+       normalize := pprofVariables["normalize"].boolValue()
+       if normalize && len(source.Base) == 0 {
+               return nil, nil, fmt.Errorf("Must have base profile to normalize by")
+       }
+       source.Normalize = normalize
+
        if bu, ok := o.Obj.(*binutils.Binutils); ok {
                bu.SetTools(*flagTools)
        }
@@ -240,13 +256,33 @@ func outputFormat(bcmd map[string]*bool, acmd map[string]*string) (cmd []string,
        return cmd, nil
 }
 
-var usageMsgHdr = "usage: pprof [options] [-base source] [binary] <source> ...\n"
+var usageMsgHdr = `usage:
+
+Produce output in the specified format.
+
+   pprof <format> [options] [binary] <source> ...
+
+Omit the format to get an interactive shell whose commands can be used
+to generate various views of a profile
+
+   pprof [options] [binary] <source> ...
+
+Omit the format and provide the "-http" flag to get an interactive web
+interface at the specified host:port that can be used to navigate through
+various views of a profile.
+
+   pprof -http [host]:[port] [options] [binary] <source> ...
+
+Details:
+`
 
 var usageMsgSrc = "\n\n" +
        "  Source options:\n" +
        "    -seconds              Duration for time-based profile collection\n" +
        "    -timeout              Timeout in seconds for profile collection\n" +
        "    -buildid              Override build id for main binary\n" +
+       "    -add_comment          Free-form annotation to add to the profile\n" +
+       "                          Displayed on some reports or with pprof -comments\n" +
        "    -base source          Source of profile to use as baseline\n" +
        "    profile.pb.gz         Profile in compressed protobuf format\n" +
        "    legacy_profile        Profile in legacy pprof format\n" +
@@ -261,7 +297,19 @@ var usageMsgSrc = "\n\n" +
 
 var usageMsgVars = "\n\n" +
        "  Misc options:\n" +
-       "   -tools                 Search path for object tools\n" +
+       "   -http              Provide web based interface at host:port.\n" +
+       "                      Host is optional and 'localhost' by default.\n" +
+       "                      Port is optional and a randomly available port by default.\n" +
+       "   -tools             Search path for object tools\n" +
+       "\n" +
+       "  Legacy convenience options:\n" +
+       "   -inuse_space           Same as -sample_index=inuse_space\n" +
+       "   -inuse_objects         Same as -sample_index=inuse_objects\n" +
+       "   -alloc_space           Same as -sample_index=alloc_space\n" +
+       "   -alloc_objects         Same as -sample_index=alloc_objects\n" +
+       "   -total_delay           Same as -sample_index=delay\n" +
+       "   -contentions           Same as -sample_index=contentions\n" +
+       "   -mean_delay            Same as -mean -sample_index=delay\n" +
        "\n" +
        "  Environment Variables:\n" +
        "   PPROF_TMPDIR       Location for saved profiles (default $HOME/pprof)\n" +
index 5e5406277186b4fd357a0f1c547a5575c9c96473..66e5c86b9d869d697c579f366627ed633d9bbbda 100644 (file)
@@ -139,10 +139,10 @@ var pprofVariables = variables{
        // Comparisons.
        "positive_percentages": &variable{boolKind, "f", "", helpText(
                "Ignore negative samples when computing percentages",
-               " Do not count negative samples when computing the total value",
-               " of the profile, used to compute percentages. If set, and the -base",
-               " option is used, percentages reported will be computed against the",
-               " main profile, ignoring the base profile.")},
+               "Do not count negative samples when computing the total value",
+               "of the profile, used to compute percentages. If set, and the -base",
+               "option is used, percentages reported will be computed against the",
+               "main profile, ignoring the base profile.")},
 
        // Graph handling options.
        "call_tree": &variable{boolKind, "f", "", helpText(
@@ -157,9 +157,9 @@ var pprofVariables = variables{
        "unit": &variable{stringKind, "minimum", "", helpText(
                "Measurement units to display",
                "Scale the sample values to this unit.",
-               " For time-based profiles, use seconds, milliseconds, nanoseconds, etc.",
-               " For memory profiles, use megabytes, kilobytes, bytes, etc.",
-               " auto will scale each value independently to the most natural unit.")},
+               "For time-based profiles, use seconds, milliseconds, nanoseconds, etc.",
+               "For memory profiles, use megabytes, kilobytes, bytes, etc.",
+               "Using auto will scale each value independently to the most natural unit.")},
        "compact_labels": &variable{boolKind, "f", "", "Show minimal headers"},
        "source_path":    &variable{stringKind, "", "", "Search path for source files"},
 
@@ -195,11 +195,15 @@ var pprofVariables = variables{
                "If set, only show nodes that match this location.",
                "Matching includes the function name, filename or object name.")},
        "tagfocus": &variable{stringKind, "", "", helpText(
-               "Restrict to samples with tags in range or matched by regexp",
-               "Discard samples that do not include a node with a tag matching this regexp.")},
+               "Restricts to samples with tags in range or matched by regexp",
+               "Use name=value syntax to limit the matching to a specific tag.",
+               "Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:",
+               "String tag filter examples: foo, foo.*bar, mytag=foo.*bar")},
        "tagignore": &variable{stringKind, "", "", helpText(
                "Discard samples with tags in range or matched by regexp",
-               "Discard samples that do include a node with a tag matching this regexp.")},
+               "Use name=value syntax to limit the matching to a specific tag.",
+               "Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:",
+               "String tag filter examples: foo, foo.*bar, mytag=foo.*bar")},
        "tagshow": &variable{stringKind, "", "", helpText(
                "Only consider tags matching this regexp",
                "Discard tags that do not match this regexp")},
@@ -218,6 +222,8 @@ var pprofVariables = variables{
                "Sample value to report (0-based index or name)",
                "Profiles contain multiple values per sample.",
                "Use sample_index=i to select the ith value (starting at 0).")},
+       "normalize": &variable{boolKind, "f", "", helpText(
+               "Scales profile based on the base profile.")},
 
        // Data sorting criteria
        "flat": &variable{boolKind, "t", "cumulative", helpText("Sort entries based on own weight")},
@@ -227,9 +233,6 @@ var pprofVariables = variables{
        "functions": &variable{boolKind, "t", "granularity", helpText(
                "Aggregate at the function level.",
                "Takes into account the filename/lineno where the function was defined.")},
-       "functionnameonly": &variable{boolKind, "f", "granularity", helpText(
-               "Aggregate at the function level.",
-               "Ignores the filename/lineno where the function was defined.")},
        "files": &variable{boolKind, "f", "granularity", "Aggregate at the file level."},
        "lines": &variable{boolKind, "f", "granularity", "Aggregate at the source code line level."},
        "addresses": &variable{boolKind, "f", "granularity", helpText(
@@ -266,7 +269,7 @@ func usage(commandLine bool) string {
 
        var help string
        if commandLine {
-               help = "  Output formats (select only one):\n"
+               help = "  Output formats (select at most one):\n"
        } else {
                help = "  Commands:\n"
                commands = append(commands, fmtHelp("o/options", "List options and their current values"))
@@ -471,7 +474,7 @@ func (vars variables) set(name, value string) error {
        case boolKind:
                var b bool
                if b, err = stringToBool(value); err == nil {
-                       if v.group != "" && b == false {
+                       if v.group != "" && !b {
                                err = fmt.Errorf("%q can only be set to true", name)
                        }
                }
index 2ca09dfa32be4a1690403f92218fd5b2b7e4093a..bc5f36612894102f92bb8d937e2bf318889389a5 100644 (file)
@@ -23,6 +23,7 @@ import (
        "os"
        "path/filepath"
        "regexp"
+       "strings"
 
        "github.com/google/pprof/internal/plugin"
        "github.com/google/pprof/internal/report"
@@ -52,24 +53,30 @@ func PProf(eo *plugin.Options) error {
                return generateReport(p, cmd, pprofVariables, o)
        }
 
+       if src.HTTPHostport != "" {
+               return serveWebInterface(src.HTTPHostport, p, o)
+       }
        return interactive(p, o)
 }
 
-func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error {
+func generateRawReport(p *profile.Profile, cmd []string, vars variables, 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)
+
        vars = applyCommandOverrides(cmd, vars)
 
        // Delay focus after configuring report to get percentages on all samples.
        relative := vars["relative_percentages"].boolValue()
        if relative {
-               if err := applyFocus(p, vars, o.UI); err != nil {
-                       return err
+               if err := applyFocus(p, numLabelUnits, vars, o.UI); err != nil {
+                       return nil, nil, err
                }
        }
-       ropt, err := reportOptions(p, vars)
+       ropt, err := reportOptions(p, numLabelUnits, vars)
        if err != nil {
-               return err
+               return nil, nil, err
        }
        c := pprofCommands[cmd[0]]
        if c == nil {
@@ -79,18 +86,27 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
        if len(cmd) == 2 {
                s, err := regexp.Compile(cmd[1])
                if err != nil {
-                       return fmt.Errorf("parsing argument regexp %s: %v", cmd[1], err)
+                       return nil, nil, fmt.Errorf("parsing argument regexp %s: %v", cmd[1], err)
                }
                ropt.Symbol = s
        }
 
        rpt := report.New(p, ropt)
        if !relative {
-               if err := applyFocus(p, vars, o.UI); err != nil {
-                       return err
+               if err := applyFocus(p, numLabelUnits, vars, o.UI); err != nil {
+                       return nil, nil, err
                }
        }
        if err := aggregate(p, vars); err != nil {
+               return nil, nil, err
+       }
+
+       return c, rpt, nil
+}
+
+func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error {
+       c, rpt, err := generateRawReport(p, cmd, vars, o)
+       if err != nil {
                return err
        }
 
@@ -160,20 +176,20 @@ func applyCommandOverrides(cmd []string, v variables) variables {
                        v.set("nodecount", "80")
                }
        }
-       if trim == false {
+       if !trim {
                v.set("nodecount", "0")
                v.set("nodefraction", "0")
                v.set("edgefraction", "0")
        }
-       if focus == false {
+       if !focus {
                v.set("focus", "")
                v.set("ignore", "")
        }
-       if tagfocus == false {
+       if !tagfocus {
                v.set("tagfocus", "")
                v.set("tagignore", "")
        }
-       if hide == false {
+       if !hide {
                v.set("hide", "")
                v.set("show", "")
        }
@@ -196,25 +212,20 @@ func aggregate(prof *profile.Profile, v variables) error {
        case v["functions"].boolValue():
                inlines = true
                function = true
-               filename = true
        case v["noinlines"].boolValue():
                function = true
-               filename = true
        case v["addressnoinlines"].boolValue():
                function = true
                filename = true
                linenumber = true
                address = true
-       case v["functionnameonly"].boolValue():
-               inlines = true
-               function = true
        default:
                return fmt.Errorf("unexpected granularity")
        }
        return prof.Aggregate(inlines, function, filename, linenumber, address)
 }
 
-func reportOptions(p *profile.Profile, vars variables) (*report.Options, error) {
+func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars variables) (*report.Options, error) {
        si, mean := vars["sample_index"].value, vars["mean"].boolValue()
        value, meanDiv, sample, err := sampleFormat(p, si, mean)
        if err != nil {
@@ -230,6 +241,14 @@ func reportOptions(p *profile.Profile, vars variables) (*report.Options, error)
                return nil, fmt.Errorf("zero divisor specified")
        }
 
+       var filters []string
+       for _, k := range []string{"focus", "ignore", "hide", "show", "tagfocus", "tagignore", "tagshow", "taghide"} {
+               v := vars[k].value
+               if v != "" {
+                       filters = append(filters, k+"="+v)
+               }
+       }
+
        ropt := &report.Options{
                CumSort:             vars["cum"].boolValue(),
                CallTree:            vars["call_tree"].boolValue(),
@@ -243,6 +262,9 @@ func reportOptions(p *profile.Profile, vars variables) (*report.Options, error)
                NodeFraction: vars["nodefraction"].floatValue(),
                EdgeFraction: vars["edgefraction"].floatValue(),
 
+               ActiveFilters: filters,
+               NumLabelUnits: numLabelUnits,
+
                SampleValue:       value,
                SampleMeanDivisor: meanDiv,
                SampleType:        stype,
@@ -260,6 +282,19 @@ func reportOptions(p *profile.Profile, vars variables) (*report.Options, error)
        return ropt, nil
 }
 
+// identifyNumLabelUnits returns a map of numeric label keys to the units
+// associated with those keys.
+func identifyNumLabelUnits(p *profile.Profile, ui plugin.UI) map[string]string {
+       numLabelUnits, ignoredUnits := p.NumLabelUnits()
+
+       // Print errors for tags with multiple units associated with
+       // a single key.
+       for k, units := range ignoredUnits {
+               ui.PrintErr(fmt.Sprintf("For tag %s used unit %s, also encountered unit(s) %s", k, numLabelUnits[k], strings.Join(units, ", ")))
+       }
+       return numLabelUnits
+}
+
 type sampleValueFunc func([]int64) int64
 
 // sampleFormat returns a function to extract values out of a profile.Sample,
index c60ad8157e40edf7d64bdda237e66858aa4d4b69..ba5b502ad9e29e8b4eb2ba8080e9121cb95a985b 100644 (file)
@@ -28,13 +28,13 @@ import (
 var tagFilterRangeRx = regexp.MustCompile("([[:digit:]]+)([[:alpha:]]+)")
 
 // applyFocus filters samples based on the focus/ignore options
-func applyFocus(prof *profile.Profile, v variables, ui plugin.UI) error {
+func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variables, ui plugin.UI) error {
        focus, err := compileRegexOption("focus", v["focus"].value, nil)
        ignore, err := compileRegexOption("ignore", v["ignore"].value, err)
        hide, err := compileRegexOption("hide", v["hide"].value, err)
        show, err := compileRegexOption("show", v["show"].value, err)
-       tagfocus, err := compileTagFilter("tagfocus", v["tagfocus"].value, ui, err)
-       tagignore, err := compileTagFilter("tagignore", v["tagignore"].value, ui, err)
+       tagfocus, err := compileTagFilter("tagfocus", v["tagfocus"].value, numLabelUnits, ui, err)
+       tagignore, err := compileTagFilter("tagignore", v["tagignore"].value, numLabelUnits, ui, err)
        prunefrom, err := compileRegexOption("prune_from", v["prune_from"].value, err)
        if err != nil {
                return err
@@ -59,7 +59,7 @@ func applyFocus(prof *profile.Profile, v variables, ui plugin.UI) error {
        if prunefrom != nil {
                prof.PruneFrom(prunefrom)
        }
-       return nil
+       return err
 }
 
 func compileRegexOption(name, value string, err error) (*regexp.Regexp, error) {
@@ -73,23 +73,49 @@ func compileRegexOption(name, value string, err error) (*regexp.Regexp, error) {
        return rx, nil
 }
 
-func compileTagFilter(name, value string, ui plugin.UI, err error) (func(*profile.Sample) bool, error) {
+func compileTagFilter(name, value string, numLabelUnits map[string]string, ui plugin.UI, err error) (func(*profile.Sample) bool, error) {
        if value == "" || err != nil {
                return nil, err
        }
+
+       tagValuePair := strings.SplitN(value, "=", 2)
+       var wantKey string
+       if len(tagValuePair) == 2 {
+               wantKey = tagValuePair[0]
+               value = tagValuePair[1]
+       }
+
        if numFilter := parseTagFilterRange(value); numFilter != nil {
                ui.PrintErr(name, ":Interpreted '", value, "' as range, not regexp")
-               return func(s *profile.Sample) bool {
-                       for key, vals := range s.NumLabel {
-                               for _, val := range vals {
-                                       if numFilter(val, key) {
+               labelFilter := func(vals []int64, unit string) bool {
+                       for _, val := range vals {
+                               if numFilter(val, unit) {
+                                       return true
+                               }
+                       }
+                       return false
+               }
+               numLabelUnit := func(key string) string {
+                       return numLabelUnits[key]
+               }
+               if wantKey == "" {
+                       return func(s *profile.Sample) bool {
+                               for key, vals := range s.NumLabel {
+                                       if labelFilter(vals, numLabelUnit(key)) {
                                                return true
                                        }
                                }
+                               return false
+                       }, nil
+               }
+               return func(s *profile.Sample) bool {
+                       if vals, ok := s.NumLabel[wantKey]; ok {
+                               return labelFilter(vals, numLabelUnit(wantKey))
                        }
                        return false
                }, nil
        }
+
        var rfx []*regexp.Regexp
        for _, tagf := range strings.Split(value, ",") {
                fx, err := regexp.Compile(tagf)
@@ -98,19 +124,34 @@ func compileTagFilter(name, value string, ui plugin.UI, err error) (func(*profil
                }
                rfx = append(rfx, fx)
        }
+       if wantKey == "" {
+               return func(s *profile.Sample) bool {
+               matchedrx:
+                       for _, rx := range rfx {
+                               for key, vals := range s.Label {
+                                       for _, val := range vals {
+                                               // TODO: Match against val, not key:val in future
+                                               if rx.MatchString(key + ":" + val) {
+                                                       continue matchedrx
+                                               }
+                                       }
+                               }
+                               return false
+                       }
+                       return true
+               }, nil
+       }
        return func(s *profile.Sample) bool {
-       matchedrx:
-               for _, rx := range rfx {
-                       for key, vals := range s.Label {
+               if vals, ok := s.Label[wantKey]; ok {
+                       for _, rx := range rfx {
                                for _, val := range vals {
-                                       if rx.MatchString(key + ":" + val) {
-                                               continue matchedrx
+                                       if rx.MatchString(val) {
+                                               return true
                                        }
                                }
                        }
-                       return false
                }
-               return true
+               return false
        }, nil
 }
 
index 75eaebec39f82478d4c5586fd1d1c90e18f8e4f5..1289a096b8937722b2d2e559a3ef736dd1fe4836 100644 (file)
@@ -16,9 +16,13 @@ package driver
 
 import (
        "bytes"
+       "flag"
        "fmt"
        "io/ioutil"
+       "net"
+       _ "net/http/pprof"
        "os"
+       "reflect"
        "regexp"
        "runtime"
        "strconv"
@@ -32,52 +36,61 @@ import (
        "github.com/google/pprof/profile"
 )
 
+var updateFlag = flag.Bool("update", false, "Update the golden files")
+
 func TestParse(t *testing.T) {
        // Override weblist command to collect output in buffer
        pprofCommands["weblist"].postProcess = nil
 
        // Our mockObjTool.Open will always return success, causing
-       // driver.locateBinaries to "find" the binaries below in a non-existant
+       // driver.locateBinaries to "find" the binaries below in a non-existent
        // directory. As a workaround, point the search path to the fake
        // directory containing out fake binaries.
        savePath := os.Getenv("PPROF_BINARY_PATH")
        os.Setenv("PPROF_BINARY_PATH", "/path/to")
        defer os.Setenv("PPROF_BINARY_PATH", savePath)
-
        testcase := []struct {
                flags, source string
        }{
                {"text,functions,flat", "cpu"},
                {"tree,addresses,flat,nodecount=4", "cpusmall"},
-               {"text,functions,flat", "unknown"},
+               {"text,functions,flat,nodecount=5,call_tree", "unknown"},
                {"text,alloc_objects,flat", "heap_alloc"},
                {"text,files,flat", "heap"},
+               {"text,files,flat,focus=[12]00,taghide=[X3]00", "heap"},
                {"text,inuse_objects,flat", "heap"},
                {"text,lines,cum,hide=line[X3]0", "cpu"},
                {"text,lines,cum,show=[12]00", "cpu"},
+               {"text,lines,cum,hide=line[X3]0,focus=[12]00", "cpu"},
                {"topproto,lines,cum,hide=mangled[X3]0", "cpu"},
                {"tree,lines,cum,focus=[24]00", "heap"},
                {"tree,relative_percentages,cum,focus=[24]00", "heap"},
                {"callgrind", "cpu"},
+               {"callgrind,call_tree", "cpu"},
                {"callgrind", "heap"},
                {"dot,functions,flat", "cpu"},
+               {"dot,functions,flat,call_tree", "cpu"},
                {"dot,lines,flat,focus=[12]00", "heap"},
+               {"dot,unit=minimum", "heap_sizetags"},
                {"dot,addresses,flat,ignore=[X3]002,focus=[X1]000", "contention"},
                {"dot,files,cum", "contention"},
-               {"comments", "cpu"},
+               {"comments,add_comment=some-comment", "cpu"},
                {"comments", "heap"},
                {"tags", "cpu"},
                {"tags,tagignore=tag[13],tagfocus=key[12]", "cpu"},
                {"tags", "heap"},
                {"tags,unit=bytes", "heap"},
                {"traces", "cpu"},
+               {"traces", "heap_tags"},
                {"dot,alloc_space,flat,focus=[234]00", "heap_alloc"},
+               {"dot,alloc_space,flat,tagshow=[2]00", "heap_alloc"},
                {"dot,alloc_space,flat,hide=line.*1?23?", "heap_alloc"},
                {"dot,inuse_space,flat,tagfocus=1mb:2gb", "heap"},
                {"dot,inuse_space,flat,tagfocus=30kb:,tagignore=1mb:2mb", "heap"},
                {"disasm=line[13],addresses,flat", "cpu"},
                {"peek=line.*01", "cpu"},
                {"weblist=line[13],addresses,flat", "cpu"},
+               {"tags,tagfocus=400kb:", "heap_request"},
        }
 
        baseVars := pprofVariables
@@ -99,6 +112,7 @@ func TestParse(t *testing.T) {
                if err != nil {
                        t.Errorf("cannot create tempfile: %v", err)
                }
+               defer os.Remove(protoTempFile.Name())
                defer protoTempFile.Close()
                f.strings["output"] = protoTempFile.Name()
 
@@ -124,6 +138,7 @@ func TestParse(t *testing.T) {
                if err != nil {
                        t.Errorf("cannot create tempfile: %v", err)
                }
+               defer os.Remove(outputTempFile.Name())
                defer outputTempFile.Close()
                f.strings["output"] = outputTempFile.Name()
                f.args = []string{protoTempFile.Name()}
@@ -140,6 +155,8 @@ func TestParse(t *testing.T) {
                        addFlags(&f, flags[:1])
                        solution = solutionFilename(tc.source, &f)
                }
+               // The add_comment flag is not idempotent so only apply it on the first run.
+               delete(f.strings, "add_comment")
 
                // Second pprof invocation to read the profile from profile.proto
                // and generate a report.
@@ -180,6 +197,12 @@ func TestParse(t *testing.T) {
                                t.Fatalf("diff %s %v", solution, err)
                        }
                        t.Errorf("%s\n%s\n", solution, d)
+                       if *updateFlag {
+                               err := ioutil.WriteFile(solution, b, 0644)
+                               if err != nil {
+                                       t.Errorf("failed to update the solution file %q: %v", solution, err)
+                               }
+                       }
                }
        }
 }
@@ -214,14 +237,19 @@ func addFlags(f *testFlags, flags []string) {
        }
 }
 
+func testSourceURL(port int) string {
+       return fmt.Sprintf("http://%s/", net.JoinHostPort(testSourceAddress, strconv.Itoa(port)))
+}
+
 // solutionFilename returns the name of the solution file for the test
 func solutionFilename(source string, f *testFlags) string {
-       name := []string{"pprof", strings.TrimPrefix(source, "http://host:8000/")}
+       name := []string{"pprof", strings.TrimPrefix(source, testSourceURL(8000))}
        name = addString(name, f, []string{"flat", "cum"})
        name = addString(name, f, []string{"functions", "files", "lines", "addresses"})
        name = addString(name, f, []string{"inuse_space", "inuse_objects", "alloc_space", "alloc_objects"})
        name = addString(name, f, []string{"relative_percentages"})
        name = addString(name, f, []string{"seconds"})
+       name = addString(name, f, []string{"call_tree"})
        name = addString(name, f, []string{"text", "tree", "callgrind", "dot", "svg", "tags", "dot", "traces", "disasm", "peek", "weblist", "topproto", "comments"})
        if f.strings["focus"] != "" || f.strings["tagfocus"] != "" {
                name = append(name, "focus")
@@ -247,11 +275,12 @@ func addString(name []string, f *testFlags, components []string) []string {
 
 // testFlags implements the plugin.FlagSet interface.
 type testFlags struct {
-       bools   map[string]bool
-       ints    map[string]int
-       floats  map[string]float64
-       strings map[string]string
-       args    []string
+       bools       map[string]bool
+       ints        map[string]int
+       floats      map[string]float64
+       strings     map[string]string
+       args        []string
+       stringLists map[string][]*string
 }
 
 func (testFlags) ExtraUsage() string { return "" }
@@ -317,6 +346,9 @@ func (f testFlags) StringVar(p *string, s, d, c string) {
 }
 
 func (f testFlags) StringList(s, d, c string) *[]*string {
+       if t, ok := f.stringLists[s]; ok {
+               return &t
+       }
        return &[]*string{}
 }
 
@@ -345,9 +377,6 @@ func baseFlags() testFlags {
        }
 }
 
-type testProfile struct {
-}
-
 const testStart = 0x1000
 const testOffset = 0x5000
 
@@ -355,7 +384,6 @@ type testFetcher struct{}
 
 func (testFetcher) Fetch(s string, d, t time.Duration) (*profile.Profile, string, error) {
        var p *profile.Profile
-       s = strings.TrimPrefix(s, "http://host:8000/")
        switch s {
        case "cpu", "unknown":
                p = cpuProfile()
@@ -369,21 +397,36 @@ func (testFetcher) Fetch(s string, d, t time.Duration) (*profile.Profile, string
                        {Type: "alloc_objects", Unit: "count"},
                        {Type: "alloc_space", Unit: "bytes"},
                }
+       case "heap_request":
+               p = heapProfile()
+               for _, s := range p.Sample {
+                       s.NumLabel["request"] = s.NumLabel["bytes"]
+               }
+       case "heap_sizetags":
+               p = heapProfile()
+               tags := []int64{2, 4, 8, 16, 32, 64, 128, 256}
+               for _, s := range p.Sample {
+                       numValues := append(s.NumLabel["bytes"], tags...)
+                       s.NumLabel["bytes"] = numValues
+               }
+       case "heap_tags":
+               p = heapProfile()
+               for i := 0; i < len(p.Sample); i += 2 {
+                       s := p.Sample[i]
+                       if s.Label == nil {
+                               s.Label = make(map[string][]string)
+                       }
+                       s.NumLabel["request"] = s.NumLabel["bytes"]
+                       s.Label["key1"] = []string{"tag"}
+               }
        case "contention":
                p = contentionProfile()
        case "symbolz":
                p = symzProfile()
-       case "http://host2/symbolz":
-               p = symzProfile()
-               p.Mapping[0].Start += testOffset
-               p.Mapping[0].Limit += testOffset
-               for i := range p.Location {
-                       p.Location[i].Address += testOffset
-               }
        default:
                return nil, "", fmt.Errorf("unexpected source: %s", s)
        }
-       return p, s, nil
+       return p, testSourceURL(8000) + s, nil
 }
 
 type testSymbolizer struct{}
@@ -406,7 +449,19 @@ func (testSymbolizeDemangler) Symbolize(_ string, _ plugin.MappingSources, p *pr
 func testFetchSymbols(source, post string) ([]byte, error) {
        var buf bytes.Buffer
 
-       if source == "http://host2/symbolz" {
+       switch source {
+       case testSourceURL(8000) + "symbolz":
+               for _, address := range strings.Split(post, "+") {
+                       a, _ := strconv.ParseInt(address, 0, 64)
+                       fmt.Fprintf(&buf, "%v\t", address)
+                       if a-testStart > testOffset {
+                               fmt.Fprintf(&buf, "wrong_source_%v_", address)
+                               continue
+                       }
+                       fmt.Fprintf(&buf, "%#x\n", a-testStart)
+               }
+               return buf.Bytes(), nil
+       case testSourceURL(8001) + "symbolz":
                for _, address := range strings.Split(post, "+") {
                        a, _ := strconv.ParseInt(address, 0, 64)
                        fmt.Fprintf(&buf, "%v\t", address)
@@ -417,23 +472,15 @@ func testFetchSymbols(source, post string) ([]byte, error) {
                        fmt.Fprintf(&buf, "%#x\n", a-testStart-testOffset)
                }
                return buf.Bytes(), nil
+       default:
+               return nil, fmt.Errorf("unexpected source: %s", source)
        }
-       for _, address := range strings.Split(post, "+") {
-               a, _ := strconv.ParseInt(address, 0, 64)
-               fmt.Fprintf(&buf, "%v\t", address)
-               if a-testStart > testOffset {
-                       fmt.Fprintf(&buf, "wrong_source_%v_", address)
-                       continue
-               }
-               fmt.Fprintf(&buf, "%#x\n", a-testStart)
-       }
-       return buf.Bytes(), nil
 }
 
 type testSymbolzSymbolizer struct{}
 
 func (testSymbolzSymbolizer) Symbolize(variables string, sources plugin.MappingSources, p *profile.Profile) error {
-       return symbolz.Symbolize(sources, testFetchSymbols, p, nil)
+       return symbolz.Symbolize(p, false, sources, testFetchSymbols, nil)
 }
 
 func fakeDemangler(name string) string {
@@ -543,32 +590,32 @@ func cpuProfile() *profile.Profile {
                                Location: []*profile.Location{cpuL[0], cpuL[1], cpuL[2]},
                                Value:    []int64{1000, 1000},
                                Label: map[string][]string{
-                                       "key1": []string{"tag1"},
-                                       "key2": []string{"tag1"},
+                                       "key1": {"tag1"},
+                                       "key2": {"tag1"},
                                },
                        },
                        {
                                Location: []*profile.Location{cpuL[0], cpuL[3]},
                                Value:    []int64{100, 100},
                                Label: map[string][]string{
-                                       "key1": []string{"tag2"},
-                                       "key3": []string{"tag2"},
+                                       "key1": {"tag2"},
+                                       "key3": {"tag2"},
                                },
                        },
                        {
                                Location: []*profile.Location{cpuL[1], cpuL[4]},
                                Value:    []int64{10, 10},
                                Label: map[string][]string{
-                                       "key1": []string{"tag3"},
-                                       "key2": []string{"tag2"},
+                                       "key1": {"tag3"},
+                                       "key2": {"tag2"},
                                },
                        },
                        {
                                Location: []*profile.Location{cpuL[2]},
                                Value:    []int64{10, 10},
                                Label: map[string][]string{
-                                       "key1": []string{"tag4"},
-                                       "key2": []string{"tag1"},
+                                       "key1": {"tag4"},
+                                       "key2": {"tag1"},
                                },
                        },
                },
@@ -744,30 +791,22 @@ func heapProfile() *profile.Profile {
                        {
                                Location: []*profile.Location{heapL[0], heapL[1], heapL[2]},
                                Value:    []int64{10, 1024000},
-                               NumLabel: map[string][]int64{
-                                       "bytes": []int64{102400},
-                               },
+                               NumLabel: map[string][]int64{"bytes": {102400}},
                        },
                        {
                                Location: []*profile.Location{heapL[0], heapL[3]},
                                Value:    []int64{20, 4096000},
-                               NumLabel: map[string][]int64{
-                                       "bytes": []int64{204800},
-                               },
+                               NumLabel: map[string][]int64{"bytes": {204800}},
                        },
                        {
                                Location: []*profile.Location{heapL[1], heapL[4]},
                                Value:    []int64{40, 65536000},
-                               NumLabel: map[string][]int64{
-                                       "bytes": []int64{1638400},
-                               },
+                               NumLabel: map[string][]int64{"bytes": {1638400}},
                        },
                        {
                                Location: []*profile.Location{heapL[2]},
                                Value:    []int64{80, 32768000},
-                               NumLabel: map[string][]int64{
-                                       "bytes": []int64{409600},
-                               },
+                               NumLabel: map[string][]int64{"bytes": {409600}},
                        },
                },
                DropFrames: ".*operator new.*|malloc",
@@ -950,31 +989,394 @@ func TestAutoComplete(t *testing.T) {
 
 func TestTagFilter(t *testing.T) {
        var tagFilterTests = []struct {
-               name, value string
+               desc, value string
                tags        map[string][]string
                want        bool
        }{
-               {"test1", "tag2", map[string][]string{"value1": {"tag1", "tag2"}}, true},
-               {"test2", "tag3", map[string][]string{"value1": {"tag1", "tag2"}}, false},
-               {"test3", "tag1,tag3", map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}}, true},
-               {"test4", "t..[12],t..3", map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}}, true},
-               {"test5", "tag2,tag3", map[string][]string{"value1": {"tag1", "tag2"}}, false},
+               {
+                       "1 key with 1 matching value",
+                       "tag2",
+                       map[string][]string{"value1": {"tag1", "tag2"}},
+                       true,
+               },
+               {
+                       "1 key with no matching values",
+                       "tag3",
+                       map[string][]string{"value1": {"tag1", "tag2"}},
+                       false,
+               },
+               {
+                       "two keys, each with value matching different one value in list",
+                       "tag1,tag3",
+                       map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}},
+                       true,
+               },
+               {"two keys, all value matching different regex value in list",
+                       "t..[12],t..3",
+                       map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}},
+                       true,
+               },
+               {
+                       "one key, not all values in list matched",
+                       "tag2,tag3",
+                       map[string][]string{"value1": {"tag1", "tag2"}},
+                       false,
+               },
+               {
+                       "key specified, list of tags where all tags in list matched",
+                       "key1=tag1,tag2",
+                       map[string][]string{"key1": {"tag1", "tag2"}},
+                       true,
+               },
+               {"key specified, list of tag values where not all are matched",
+                       "key1=tag1,tag2",
+                       map[string][]string{"key1": {"tag1"}},
+                       true,
+               },
+               {
+                       "key included for regex matching, list of values where all values in list matched",
+                       "key1:tag1,tag2",
+                       map[string][]string{"key1": {"tag1", "tag2"}},
+                       true,
+               },
+               {
+                       "key included for regex matching, list of values where not only second value matched",
+                       "key1:tag1,tag2",
+                       map[string][]string{"key1": {"tag2"}},
+                       false,
+               },
+               {
+                       "key included for regex matching, list of values where not only first value matched",
+                       "key1:tag1,tag2",
+                       map[string][]string{"key1": {"tag1"}},
+                       false,
+               },
+       }
+       for _, test := range tagFilterTests {
+               t.Run(test.desc, func(*testing.T) {
+                       filter, err := compileTagFilter(test.desc, test.value, nil, &proftest.TestUI{T: t}, nil)
+                       if err != nil {
+                               t.Fatalf("tagFilter %s:%v", test.desc, err)
+                       }
+                       s := profile.Sample{
+                               Label: test.tags,
+                       }
+                       if got := filter(&s); got != test.want {
+                               t.Errorf("tagFilter %s: got %v, want %v", test.desc, got, test.want)
+                       }
+               })
        }
+}
 
+func TestIdentifyNumLabelUnits(t *testing.T) {
+       var tagFilterTests = []struct {
+               desc               string
+               tagVals            []map[string][]int64
+               tagUnits           []map[string][]string
+               wantUnits          map[string]string
+               allowedRx          string
+               wantIgnoreErrCount int
+       }{
+               {
+                       "Multiple keys, no units for all keys",
+                       []map[string][]int64{{"keyA": {131072}, "keyB": {128}}},
+                       []map[string][]string{{"keyA": {}, "keyB": {""}}},
+                       map[string]string{"keyA": "keyA", "keyB": "keyB"},
+                       "",
+                       0,
+               },
+               {
+                       "Multiple keys, different units for each key",
+                       []map[string][]int64{{"keyA": {131072}, "keyB": {128}}},
+                       []map[string][]string{{"keyA": {"bytes"}, "keyB": {"kilobytes"}}},
+                       map[string]string{"keyA": "bytes", "keyB": "kilobytes"},
+                       "",
+                       0,
+               },
+               {
+                       "Multiple keys with multiple values, different units for each key",
+                       []map[string][]int64{{"keyC": {131072, 1}, "keyD": {128, 252}}},
+                       []map[string][]string{{"keyC": {"bytes", "bytes"}, "keyD": {"kilobytes", "kilobytes"}}},
+                       map[string]string{"keyC": "bytes", "keyD": "kilobytes"},
+                       "",
+                       0,
+               },
+               {
+                       "Multiple keys with multiple values, some units missing",
+                       []map[string][]int64{{"key1": {131072, 1}, "A": {128, 252}, "key3": {128}, "key4": {1}}, {"key3": {128}, "key4": {1}}},
+                       []map[string][]string{{"key1": {"", "bytes"}, "A": {"kilobytes", ""}, "key3": {""}, "key4": {"hour"}}, {"key3": {"seconds"}, "key4": {""}}},
+                       map[string]string{"key1": "bytes", "A": "kilobytes", "key3": "seconds", "key4": "hour"},
+                       "",
+                       0,
+               },
+               {
+                       "One key with three units in same sample",
+                       []map[string][]int64{{"key": {8, 8, 16}}},
+                       []map[string][]string{{"key": {"bytes", "megabytes", "kilobytes"}}},
+                       map[string]string{"key": "bytes"},
+                       `(For tag key used unit bytes, also encountered unit\(s\) kilobytes, megabytes)`,
+                       1,
+               },
+               {
+                       "One key with four units in same sample",
+                       []map[string][]int64{{"key": {8, 8, 16, 32}}},
+                       []map[string][]string{{"key": {"bytes", "kilobytes", "a", "megabytes"}}},
+                       map[string]string{"key": "bytes"},
+                       `(For tag key used unit bytes, also encountered unit\(s\) a, kilobytes, megabytes)`,
+                       1,
+               },
+               {
+                       "One key with two units in same sample",
+                       []map[string][]int64{{"key": {8, 8}}},
+                       []map[string][]string{{"key": {"bytes", "seconds"}}},
+                       map[string]string{"key": "bytes"},
+                       `(For tag key used unit bytes, also encountered unit\(s\) seconds)`,
+                       1,
+               },
+               {
+                       "One key with different units in different samples",
+                       []map[string][]int64{{"key1": {8}}, {"key1": {8}}, {"key1": {8}}},
+                       []map[string][]string{{"key1": {"bytes"}}, {"key1": {"kilobytes"}}, {"key1": {"megabytes"}}},
+                       map[string]string{"key1": "bytes"},
+                       `(For tag key1 used unit bytes, also encountered unit\(s\) kilobytes, megabytes)`,
+                       1,
+               },
+               {
+                       "Key alignment, unit not specified",
+                       []map[string][]int64{{"alignment": {8}}},
+                       []map[string][]string{nil},
+                       map[string]string{"alignment": "bytes"},
+                       "",
+                       0,
+               },
+               {
+                       "Key request, unit not specified",
+                       []map[string][]int64{{"request": {8}}, {"request": {8, 8}}},
+                       []map[string][]string{nil, nil},
+                       map[string]string{"request": "bytes"},
+                       "",
+                       0,
+               },
+               {
+                       "Check units not over-written for keys with default units",
+                       []map[string][]int64{{
+                               "alignment": {8},
+                               "request":   {8},
+                               "bytes":     {8},
+                       }},
+                       []map[string][]string{{
+                               "alignment": {"seconds"},
+                               "request":   {"minutes"},
+                               "bytes":     {"hours"},
+                       }},
+                       map[string]string{
+                               "alignment": "seconds",
+                               "request":   "minutes",
+                               "bytes":     "hours",
+                       },
+                       "",
+                       0,
+               },
+       }
        for _, test := range tagFilterTests {
-               filter, err := compileTagFilter(test.name, test.value, &proftest.TestUI{T: t}, nil)
-               if err != nil {
-                       t.Errorf("tagFilter %s:%v", test.name, err)
-                       continue
-               }
-               s := profile.Sample{
-                       Label: test.tags,
-               }
+               t.Run(test.desc, func(*testing.T) {
+                       p := profile.Profile{Sample: make([]*profile.Sample, len(test.tagVals))}
+                       for i, numLabel := range test.tagVals {
+                               s := profile.Sample{
+                                       NumLabel: numLabel,
+                                       NumUnit:  test.tagUnits[i],
+                               }
+                               p.Sample[i] = &s
+                       }
+                       testUI := &proftest.TestUI{T: t, AllowRx: test.allowedRx}
+                       units := identifyNumLabelUnits(&p, testUI)
+                       if !reflect.DeepEqual(test.wantUnits, units) {
+                               t.Errorf("got %v units, want %v", units, test.wantUnits)
+                       }
+                       if got, want := testUI.NumAllowRxMatches, test.wantIgnoreErrCount; want != got {
+                               t.Errorf("got %d errors logged, want %d errors logged", got, want)
+                       }
+               })
+       }
+}
+
+func TestNumericTagFilter(t *testing.T) {
+       var tagFilterTests = []struct {
+               desc, value     string
+               tags            map[string][]int64
+               identifiedUnits map[string]string
+               want            bool
+       }{
+               {
+                       "Match when unit conversion required",
+                       "128kb",
+                       map[string][]int64{"key1": {131072}, "key2": {128}},
+                       map[string]string{"key1": "bytes", "key2": "kilobytes"},
+                       true,
+               },
+               {
+                       "Match only when values equal after unit conversion",
+                       "512kb",
+                       map[string][]int64{"key1": {512}, "key2": {128}},
+                       map[string]string{"key1": "bytes", "key2": "kilobytes"},
+                       false,
+               },
+               {
+                       "Match when values and units initially equal",
+                       "10bytes",
+                       map[string][]int64{"key1": {10}, "key2": {128}},
+                       map[string]string{"key1": "bytes", "key2": "kilobytes"},
+                       true,
+               },
+               {
+                       "Match range without lower bound, no unit conversion required",
+                       ":10bytes",
+                       map[string][]int64{"key1": {8}},
+                       map[string]string{"key1": "bytes"},
+                       true,
+               },
+               {
+                       "Match range without lower bound, unit conversion required",
+                       ":10kb",
+                       map[string][]int64{"key1": {8}},
+                       map[string]string{"key1": "bytes"},
+                       true,
+               },
+               {
+                       "Match range without upper bound, unit conversion required",
+                       "10b:",
+                       map[string][]int64{"key1": {8}},
+                       map[string]string{"key1": "kilobytes"},
+                       true,
+               },
+               {
+                       "Match range without upper bound, no unit conversion required",
+                       "10b:",
+                       map[string][]int64{"key1": {12}},
+                       map[string]string{"key1": "bytes"},
+                       true,
+               },
+               {
+                       "Don't match range without upper bound, no unit conversion required",
+                       "10b:",
+                       map[string][]int64{"key1": {8}},
+                       map[string]string{"key1": "bytes"},
+                       false,
+               },
+               {
+                       "Multiple keys with different units, don't match range without upper bound",
+                       "10kb:",
+                       map[string][]int64{"key1": {8}},
+                       map[string]string{"key1": "bytes", "key2": "kilobytes"},
+                       false,
+               },
+               {
+                       "Match range without upper bound, unit conversion required",
+                       "10b:",
+                       map[string][]int64{"key1": {8}},
+                       map[string]string{"key1": "kilobytes"},
+                       true,
+               },
+               {
+                       "Don't match range without lower bound, no unit conversion required",
+                       ":10b",
+                       map[string][]int64{"key1": {12}},
+                       map[string]string{"key1": "bytes"},
+                       false,
+               },
+               {
+                       "Match specific key, key present, one of two values match",
+                       "bytes=5b",
+                       map[string][]int64{"bytes": {10, 5}},
+                       map[string]string{"bytes": "bytes"},
+                       true,
+               },
+               {
+                       "Match specific key, key present and value matches",
+                       "bytes=1024b",
+                       map[string][]int64{"bytes": {1024}},
+                       map[string]string{"bytes": "kilobytes"},
+                       false,
+               },
+               {
+                       "Match specific key, matching key present and value matches, also non-matching key",
+                       "bytes=1024b",
+                       map[string][]int64{"bytes": {1024}, "key2": {5}},
+                       map[string]string{"bytes": "bytes", "key2": "bytes"},
+                       true,
+               },
+               {
+                       "Match specific key and range of values, value matches",
+                       "bytes=512b:1024b",
+                       map[string][]int64{"bytes": {780}},
+                       map[string]string{"bytes": "bytes"},
+                       true,
+               },
+               {
+                       "Match specific key and range of values, value too large",
+                       "key1=1kb:2kb",
+                       map[string][]int64{"key1": {4096}},
+                       map[string]string{"key1": "bytes"},
+                       false,
+               },
+               {
+                       "Match specific key and range of values, value too small",
+                       "key1=1kb:2kb",
+                       map[string][]int64{"key1": {256}},
+                       map[string]string{"key1": "bytes"},
+                       false,
+               },
+               {
+                       "Match specific key and value, unit conversion required",
+                       "bytes=1024b",
+                       map[string][]int64{"bytes": {1}},
+                       map[string]string{"bytes": "kilobytes"},
+                       true,
+               },
+               {
+                       "Match specific key and value, key does not appear",
+                       "key2=256bytes",
+                       map[string][]int64{"key1": {256}},
+                       map[string]string{"key1": "bytes"},
+                       false,
+               },
+       }
+       for _, test := range tagFilterTests {
+               t.Run(test.desc, func(*testing.T) {
+                       wantErrMsg := strings.Join([]string{"(", test.desc, ":Interpreted '", test.value[strings.Index(test.value, "=")+1:], "' as range, not regexp", ")"}, "")
+                       filter, err := compileTagFilter(test.desc, test.value, test.identifiedUnits, &proftest.TestUI{T: t,
+                               AllowRx: wantErrMsg}, nil)
+                       if err != nil {
+                               t.Fatalf("%v", err)
+                       }
+                       s := profile.Sample{
+                               NumLabel: test.tags,
+                       }
+                       if got := filter(&s); got != test.want {
+                               t.Fatalf("got %v, want %v", got, test.want)
+                       }
+               })
+       }
+}
+
+type testSymbolzMergeFetcher struct{}
 
-               if got := filter(&s); got != test.want {
-                       t.Errorf("tagFilter %s: got %v, want %v", test.name, got, test.want)
+func (testSymbolzMergeFetcher) Fetch(s string, d, t time.Duration) (*profile.Profile, string, error) {
+       var p *profile.Profile
+       switch s {
+       case testSourceURL(8000) + "symbolz":
+               p = symzProfile()
+       case testSourceURL(8001) + "symbolz":
+               p = symzProfile()
+               p.Mapping[0].Start += testOffset
+               p.Mapping[0].Limit += testOffset
+               for i := range p.Location {
+                       p.Location[i].Address += testOffset
                }
+       default:
+               return nil, "", fmt.Errorf("unexpected source: %s", s)
        }
+       return p, s, nil
 }
 
 func TestSymbolzAfterMerge(t *testing.T) {
@@ -983,7 +1385,10 @@ func TestSymbolzAfterMerge(t *testing.T) {
        defer func() { pprofVariables = baseVars }()
 
        f := baseFlags()
-       f.args = []string{"symbolz", "http://host2/symbolz"}
+       f.args = []string{
+               testSourceURL(8000) + "symbolz",
+               testSourceURL(8001) + "symbolz",
+       }
 
        o := setDefaults(nil)
        o.Flagset = f
@@ -997,7 +1402,7 @@ func TestSymbolzAfterMerge(t *testing.T) {
                t.Fatalf("parseFlags returned command %v, want [proto]", cmd)
        }
 
-       o.Fetch = testFetcher{}
+       o.Fetch = testSymbolzMergeFetcher{}
        o.Sym = testSymbolzSymbolizer{}
        p, err := fetchProfiles(src, o)
        if err != nil {
@@ -1028,10 +1433,10 @@ func (m *mockObjTool) Disasm(file string, start, end uint64) ([]plugin.Inst, err
        switch start {
        case 0x1000:
                return []plugin.Inst{
-                       {Addr: 0x1000, Text: "instruction one"},
-                       {Addr: 0x1001, Text: "instruction two"},
-                       {Addr: 0x1002, Text: "instruction three"},
-                       {Addr: 0x1003, Text: "instruction four"},
+                       {Addr: 0x1000, Text: "instruction one", File: "file1000.src", Line: 1},
+                       {Addr: 0x1001, Text: "instruction two", File: "file1000.src", Line: 1},
+                       {Addr: 0x1002, Text: "instruction three", File: "file1000.src", Line: 2},
+                       {Addr: 0x1003, Text: "instruction four", File: "file1000.src", Line: 1},
                }, nil
        case 0x3000:
                return []plugin.Inst{
@@ -1046,7 +1451,7 @@ func (m *mockObjTool) Disasm(file string, start, end uint64) ([]plugin.Inst, err
 }
 
 type mockFile struct {
-       name, buildId string
+       name, buildID string
        base          uint64
 }
 
@@ -1062,7 +1467,7 @@ func (m *mockFile) Base() uint64 {
 
 // BuildID returns the GNU build ID of the file, or an empty string.
 func (m *mockFile) BuildID() string {
-       return m.buildId
+       return m.buildID
 }
 
 // SourceLine reports the source line information for a given
index f9e8231419fea41c0c42b1233c15123624e3ec3d..2b1d90dafdf2e3d6128914773f0e76ae976eb590 100644 (file)
@@ -41,39 +41,52 @@ import (
 // there are some failures. It will return an error if it is unable to
 // fetch any profiles.
 func fetchProfiles(s *source, o *plugin.Options) (*profile.Profile, error) {
-       sources := make([]profileSource, 0, len(s.Sources)+len(s.Base))
+       sources := make([]profileSource, 0, len(s.Sources))
        for _, src := range s.Sources {
                sources = append(sources, profileSource{
                        addr:   src,
                        source: s,
-                       scale:  1,
                })
        }
+
+       bases := make([]profileSource, 0, len(s.Base))
        for _, src := range s.Base {
-               sources = append(sources, profileSource{
+               bases = append(bases, profileSource{
                        addr:   src,
                        source: s,
-                       scale:  -1,
                })
        }
-       p, msrcs, save, cnt, err := chunkedGrab(sources, o.Fetch, o.Obj, o.UI)
+
+       p, pbase, m, mbase, save, err := grabSourcesAndBases(sources, bases, o.Fetch, o.Obj, o.UI)
        if err != nil {
                return nil, err
        }
-       if cnt == 0 {
-               return nil, fmt.Errorf("failed to fetch any profiles")
-       }
-       if want, got := len(sources), cnt; want != got {
-               o.UI.PrintErr(fmt.Sprintf("fetched %d profiles out of %d", got, want))
+
+       if pbase != nil {
+               if s.Normalize {
+                       err := p.Normalize(pbase)
+                       if err != nil {
+                               return nil, err
+                       }
+               }
+               pbase.Scale(-1)
+               p, m, err = combineProfiles([]*profile.Profile{p, pbase}, []plugin.MappingSources{m, mbase})
+               if err != nil {
+                       return nil, err
+               }
        }
 
        // Symbolize the merged profile.
-       if err := o.Sym.Symbolize(s.Symbolize, msrcs, p); err != nil {
+       if err := o.Sym.Symbolize(s.Symbolize, m, p); err != nil {
                return nil, err
        }
        p.RemoveUninteresting()
        unsourceMappings(p)
 
+       if s.Comment != "" {
+               p.Comments = append(p.Comments, s.Comment)
+       }
+
        // Save a copy of the merged profile if there is at least one remote source.
        if save {
                dir, err := setTmpDir(o.UI)
@@ -107,6 +120,47 @@ func fetchProfiles(s *source, o *plugin.Options) (*profile.Profile, error) {
        return p, nil
 }
 
+func grabSourcesAndBases(sources, bases []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI) (*profile.Profile, *profile.Profile, plugin.MappingSources, plugin.MappingSources, bool, error) {
+       wg := sync.WaitGroup{}
+       wg.Add(2)
+       var psrc, pbase *profile.Profile
+       var msrc, mbase plugin.MappingSources
+       var savesrc, savebase bool
+       var errsrc, errbase error
+       var countsrc, countbase int
+       go func() {
+               defer wg.Done()
+               psrc, msrc, savesrc, countsrc, errsrc = chunkedGrab(sources, fetch, obj, ui)
+       }()
+       go func() {
+               defer wg.Done()
+               pbase, mbase, savebase, countbase, errbase = chunkedGrab(bases, fetch, obj, ui)
+       }()
+       wg.Wait()
+       save := savesrc || savebase
+
+       if errsrc != nil {
+               return nil, nil, nil, nil, false, fmt.Errorf("problem fetching source profiles: %v", errsrc)
+       }
+       if errbase != nil {
+               return nil, nil, nil, nil, false, fmt.Errorf("problem fetching base profiles: %v,", errbase)
+       }
+       if countsrc == 0 {
+               return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any source profiles")
+       }
+       if countbase == 0 && len(bases) > 0 {
+               return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any base profiles")
+       }
+       if want, got := len(sources), countsrc; want != got {
+               ui.PrintErr(fmt.Sprintf("Fetched %d source profiles out of %d", got, want))
+       }
+       if want, got := len(bases), countbase; want != got {
+               ui.PrintErr(fmt.Sprintf("Fetched %d base profiles out of %d", got, want))
+       }
+
+       return psrc, pbase, msrc, mbase, save, nil
+}
+
 // chunkedGrab fetches the profiles described in source and merges them into
 // a single profile. It fetches a chunk of profiles concurrently, with a maximum
 // chunk size to limit its memory usage.
@@ -142,6 +196,7 @@ func chunkedGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTo
                        count += chunkCount
                }
        }
+
        return p, msrc, save, count, nil
 }
 
@@ -152,7 +207,7 @@ func concurrentGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.Ob
        for i := range sources {
                go func(s *profileSource) {
                        defer wg.Done()
-                       s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, s.scale, fetch, obj, ui)
+                       s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, fetch, obj, ui)
                }(&sources[i])
        }
        wg.Wait()
@@ -207,7 +262,6 @@ func combineProfiles(profiles []*profile.Profile, msrcs []plugin.MappingSources)
 type profileSource struct {
        addr   string
        source *source
-       scale  float64
 
        p      *profile.Profile
        msrc   plugin.MappingSources
@@ -227,12 +281,18 @@ func homeEnv() string {
 }
 
 // setTmpDir prepares the directory to use to save profiles retrieved
-// remotely. It is selected from PPROF_TMPDIR, defaults to $HOME/pprof.
+// remotely. It is selected from PPROF_TMPDIR, defaults to $HOME/pprof, and, if
+// $HOME is not set, falls back to os.TempDir().
 func setTmpDir(ui plugin.UI) (string, error) {
+       var dirs []string
        if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir != "" {
-               return profileDir, nil
+               dirs = append(dirs, profileDir)
        }
-       for _, tmpDir := range []string{os.Getenv(homeEnv()) + "/pprof", os.TempDir()} {
+       if homeDir := os.Getenv(homeEnv()); homeDir != "" {
+               dirs = append(dirs, filepath.Join(homeDir, "pprof"))
+       }
+       dirs = append(dirs, os.TempDir())
+       for _, tmpDir := range dirs {
                if err := os.MkdirAll(tmpDir, 0755); err != nil {
                        ui.PrintErr("Could not use temp dir ", tmpDir, ": ", err.Error())
                        continue
@@ -242,10 +302,12 @@ func setTmpDir(ui plugin.UI) (string, error) {
        return "", fmt.Errorf("failed to identify temp dir")
 }
 
+const testSourceAddress = "pproftest.local"
+
 // grabProfile fetches a profile. Returns the profile, sources for the
 // profile mappings, a bool indicating if the profile was fetched
 // remotely, and an error.
-func grabProfile(s *source, source string, scale float64, fetcher plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI) (p *profile.Profile, msrc plugin.MappingSources, remote bool, err error) {
+func grabProfile(s *source, source string, fetcher plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI) (p *profile.Profile, msrc plugin.MappingSources, remote bool, err error) {
        var src string
        duration, timeout := time.Duration(s.Seconds)*time.Second, time.Duration(s.Timeout)*time.Second
        if fetcher != nil {
@@ -266,9 +328,6 @@ func grabProfile(s *source, source string, scale float64, fetcher plugin.Fetcher
                return
        }
 
-       // Apply local changes to the profile.
-       p.Scale(scale)
-
        // Update the binary locations from command line and paths.
        locateBinaries(p, s, obj, ui)
 
@@ -276,6 +335,11 @@ func grabProfile(s *source, source string, scale float64, fetcher plugin.Fetcher
        if src != "" {
                msrc = collectMappingSources(p, src)
                remote = true
+               if strings.HasPrefix(src, "http://"+testSourceAddress) {
+                       // Treat test inputs as local to avoid saving
+                       // testcase profiles during driver testing.
+                       remote = false
+               }
        }
        return
 }
@@ -366,20 +430,20 @@ mapping:
                        }
                }
        }
+       if len(p.Mapping) == 0 {
+               // If there are no mappings, add a fake mapping to attempt symbolization.
+               // This is useful for some profiles generated by the golang runtime, which
+               // do not include any mappings. Symbolization with a fake mapping will only
+               // be successful against a non-PIE binary.
+               m := &profile.Mapping{ID: 1}
+               p.Mapping = []*profile.Mapping{m}
+               for _, l := range p.Location {
+                       l.Mapping = m
+               }
+       }
        // Replace executable filename/buildID with the overrides from source.
        // Assumes the executable is the first Mapping entry.
        if execName, buildID := s.ExecName, s.BuildID; execName != "" || buildID != "" {
-               if len(p.Mapping) == 0 {
-                       // If there are no mappings, add a fake mapping to attempt symbolization.
-                       // This is useful for some profiles generated by the golang runtime, which
-                       // do not include any mappings. Symbolization with a fake mapping will only
-                       // be successful against a non-PIE binary.
-                       m := &profile.Mapping{ID: 1}
-                       p.Mapping = []*profile.Mapping{m}
-                       for _, l := range p.Location {
-                               l.Mapping = m
-                       }
-               }
                m := p.Mapping[0]
                if execName != "" {
                        m.File = execName
index 90b84b27c5109d7a17fb4572560a11e71ac2d07e..dd78bc7a7d37ef9e25bfd49f12e3fde74ef4c88f 100644 (file)
 package driver
 
 import (
+       "crypto/ecdsa"
+       "crypto/elliptic"
+       "crypto/rand"
+       "crypto/tls"
+       "crypto/x509"
+       "encoding/pem"
        "fmt"
        "io/ioutil"
+       "math/big"
        "net/http"
        "net/url"
        "os"
@@ -24,11 +31,14 @@ import (
        "reflect"
        "regexp"
        "runtime"
+       "strings"
        "testing"
        "time"
 
+       "github.com/google/pprof/internal/binutils"
        "github.com/google/pprof/internal/plugin"
        "github.com/google/pprof/internal/proftest"
+       "github.com/google/pprof/internal/symbolizer"
        "github.com/google/pprof/profile"
 )
 
@@ -165,6 +175,8 @@ func TestFetch(t *testing.T) {
        const path = "testdata/"
 
        // Intercept http.Get calls from HTTPFetcher.
+       savedHTTPGet := httpGet
+       defer func() { httpGet = savedHTTPGet }()
        httpGet = stubHTTPGet
 
        type testcase struct {
@@ -176,7 +188,7 @@ func TestFetch(t *testing.T) {
                {path + "go.nomappings.crash", "/bin/gotest.exe"},
                {"http://localhost/profile?file=cppbench.cpu", ""},
        } {
-               p, _, _, err := grabProfile(&source{ExecName: tc.execName}, tc.source, 0, nil, testObj{}, &proftest.TestUI{T: t})
+               p, _, _, err := grabProfile(&source{ExecName: tc.execName}, tc.source, nil, testObj{}, &proftest.TestUI{T: t})
                if err != nil {
                        t.Fatalf("%s: %s", tc.source, err)
                }
@@ -194,6 +206,117 @@ func TestFetch(t *testing.T) {
        }
 }
 
+func TestFetchWithBase(t *testing.T) {
+       baseVars := pprofVariables
+       defer func() { pprofVariables = baseVars }()
+
+       const path = "testdata/"
+       type testcase struct {
+               desc            string
+               sources         []string
+               bases           []string
+               normalize       bool
+               expectedSamples [][]int64
+       }
+
+       testcases := []testcase{
+               {
+                       "not normalized base is same as source",
+                       []string{path + "cppbench.contention"},
+                       []string{path + "cppbench.contention"},
+                       false,
+                       [][]int64{},
+               },
+               {
+                       "not normalized single source, multiple base (all profiles same)",
+                       []string{path + "cppbench.contention"},
+                       []string{path + "cppbench.contention", path + "cppbench.contention"},
+                       false,
+                       [][]int64{{-2700, -608881724}, {-100, -23992}, {-200, -179943}, {-100, -17778444}, {-100, -75976}, {-300, -63568134}},
+               },
+               {
+                       "not normalized, different base and source",
+                       []string{path + "cppbench.contention"},
+                       []string{path + "cppbench.small.contention"},
+                       false,
+                       [][]int64{{1700, 608878600}, {100, 23992}, {200, 179943}, {100, 17778444}, {100, 75976}, {300, 63568134}},
+               },
+               {
+                       "normalized base is same as source",
+                       []string{path + "cppbench.contention"},
+                       []string{path + "cppbench.contention"},
+                       true,
+                       [][]int64{},
+               },
+               {
+                       "normalized single source, multiple base (all profiles same)",
+                       []string{path + "cppbench.contention"},
+                       []string{path + "cppbench.contention", path + "cppbench.contention"},
+                       true,
+                       [][]int64{},
+               },
+               {
+                       "normalized different base and source",
+                       []string{path + "cppbench.contention"},
+                       []string{path + "cppbench.small.contention"},
+                       true,
+                       [][]int64{{-229, -370}, {28, 0}, {57, 0}, {28, 80}, {28, 0}, {85, 287}},
+               },
+       }
+
+       for _, tc := range testcases {
+               t.Run(tc.desc, func(t *testing.T) {
+                       pprofVariables = baseVars.makeCopy()
+
+                       base := make([]*string, len(tc.bases))
+                       for i, s := range tc.bases {
+                               base[i] = &s
+                       }
+
+                       f := testFlags{
+                               stringLists: map[string][]*string{
+                                       "base": base,
+                               },
+                               bools: map[string]bool{
+                                       "normalize": tc.normalize,
+                               },
+                       }
+                       f.args = tc.sources
+
+                       o := setDefaults(nil)
+                       o.Flagset = f
+                       src, _, err := parseFlags(o)
+
+                       if err != nil {
+                               t.Fatalf("%s: %v", tc.desc, err)
+                       }
+
+                       p, err := fetchProfiles(src, o)
+                       pprofVariables = baseVars
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+
+                       if want, got := len(tc.expectedSamples), len(p.Sample); want != got {
+                               t.Fatalf("want %d samples got %d", want, got)
+                       }
+
+                       if len(p.Sample) > 0 {
+                               for i, sample := range p.Sample {
+                                       if want, got := len(tc.expectedSamples[i]), len(sample.Value); want != got {
+                                               t.Errorf("want %d values for sample %d, got %d", want, i, got)
+                                       }
+                                       for j, value := range sample.Value {
+                                               if want, got := tc.expectedSamples[i][j], value; want != got {
+                                                       t.Errorf("want value of %d for value %d of sample %d, got %d", want, j, i, got)
+                                               }
+                                       }
+                               }
+                       }
+               })
+       }
+}
+
 // mappingSources creates MappingSources map with a single item.
 func mappingSources(key, source string, start uint64) plugin.MappingSources {
        return plugin.MappingSources{
@@ -227,3 +350,123 @@ func stubHTTPGet(source string, _ time.Duration) (*http.Response, error) {
        c := &http.Client{Transport: t}
        return c.Get("file:///" + file)
 }
+
+func TestHttpsInsecure(t *testing.T) {
+       if runtime.GOOS == "nacl" {
+               t.Skip("test assumes tcp available")
+       }
+
+       baseVars := pprofVariables
+       pprofVariables = baseVars.makeCopy()
+       defer func() { pprofVariables = baseVars }()
+
+       tlsConfig := &tls.Config{Certificates: []tls.Certificate{selfSignedCert(t)}}
+
+       l, err := tls.Listen("tcp", "localhost:0", tlsConfig)
+       if err != nil {
+               t.Fatalf("net.Listen: got error %v, want no error", err)
+       }
+
+       donec := make(chan error, 1)
+       go func(donec chan<- error) {
+               donec <- http.Serve(l, nil)
+       }(donec)
+       defer func() {
+               if got, want := <-donec, "use of closed"; !strings.Contains(got.Error(), want) {
+                       t.Fatalf("Serve got error %v, want %q", got, want)
+               }
+       }()
+       defer l.Close()
+
+       go func() {
+               deadline := time.Now().Add(5 * time.Second)
+               for time.Now().Before(deadline) {
+                       // Simulate a hotspot function. Spin in the inner loop for 100M iterations
+                       // to ensure we get most of the samples landed here rather than in the
+                       // library calls. We assume Go compiler won't elide the empty loop.
+                       for i := 0; i < 1e8; i++ {
+                       }
+                       runtime.Gosched()
+               }
+       }()
+
+       outputTempFile, err := ioutil.TempFile("", "profile_output")
+       if err != nil {
+               t.Fatalf("Failed to create tempfile: %v", err)
+       }
+       defer os.Remove(outputTempFile.Name())
+       defer outputTempFile.Close()
+
+       address := "https+insecure://" + l.Addr().String() + "/debug/pprof/profile"
+       s := &source{
+               Sources:   []string{address},
+               Seconds:   10,
+               Timeout:   10,
+               Symbolize: "remote",
+       }
+       o := &plugin.Options{
+               Obj: &binutils.Binutils{},
+               UI:  &proftest.TestUI{T: t, AllowRx: "Saved profile in"},
+       }
+       o.Sym = &symbolizer.Symbolizer{Obj: o.Obj, UI: o.UI}
+       p, err := fetchProfiles(s, o)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if len(p.SampleType) == 0 {
+               t.Fatalf("fetchProfiles(%s) got empty profile: len(p.SampleType)==0", address)
+       }
+       if len(p.Function) == 0 {
+               t.Fatalf("fetchProfiles(%s) got non-symbolized profile: len(p.Function)==0", address)
+       }
+       if err := checkProfileHasFunction(p, "TestHttpsInsecure"); !badSigprofOS[runtime.GOOS] && err != nil {
+               t.Fatalf("fetchProfiles(%s) %v", address, err)
+       }
+}
+
+// Some operating systems don't trigger the profiling signal right.
+// See https://github.com/golang/go/issues/13841.
+var badSigprofOS = map[string]bool{
+       "darwin": true,
+       "netbsd": true,
+       "plan9":  true,
+}
+
+func checkProfileHasFunction(p *profile.Profile, fname string) error {
+       for _, f := range p.Function {
+               if strings.Contains(f.Name, fname) {
+                       return nil
+               }
+       }
+       return fmt.Errorf("got %s, want function %q", p.String(), fname)
+}
+
+func selfSignedCert(t *testing.T) tls.Certificate {
+       privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+       if err != nil {
+               t.Fatalf("failed to generate private key: %v", err)
+       }
+       b, err := x509.MarshalECPrivateKey(privKey)
+       if err != nil {
+               t.Fatalf("failed to marshal private key: %v", err)
+       }
+       bk := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b})
+
+       tmpl := x509.Certificate{
+               SerialNumber: big.NewInt(1),
+               NotBefore:    time.Now(),
+               NotAfter:     time.Now().Add(10 * time.Minute),
+       }
+
+       b, err = x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, privKey.Public(), privKey)
+       if err != nil {
+               t.Fatalf("failed to create cert: %v", err)
+       }
+       bc := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: b})
+
+       cert, err := tls.X509KeyPair(bc, bk)
+       if err != nil {
+               t.Fatalf("failed to create TLS key pair: %v", err)
+       }
+       return cert
+}
index aa9c5b824b862d124dd87bca82f176d78baf8754..2c36b64cc7e186b136b2f47132d5b558f83c3707 100644 (file)
@@ -123,7 +123,8 @@ var generateReportWrapper = generateReport // For testing purposes.
 // greetings prints a brief welcome and some overall profile
 // information before accepting interactive commands.
 func greetings(p *profile.Profile, ui plugin.UI) {
-       ropt, err := reportOptions(p, pprofVariables)
+       numLabelUnits := identifyNumLabelUnits(p, ui)
+       ropt, err := reportOptions(p, numLabelUnits, pprofVariables)
        if err == nil {
                ui.Print(strings.Join(report.ProfileLabels(report.New(p, ropt)), "\n"))
        }
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/cppbench.contention b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/cppbench.contention
new file mode 100644 (file)
index 0000000..66a64c9
--- /dev/null
@@ -0,0 +1,24 @@
+--- contentionz 1 ---
+cycles/second = 3201000000
+sampling period = 100
+ms since reset = 16502830
+discarded samples = 0
+  19490304       27 @ 0xbccc97 0xc61202 0x42ed5f 0x42edc1 0x42e15a 0x5261af 0x526edf 0x5280ab 0x79e80a 0x7a251b 0x7a296d 0xa456e4 0x7fcdc2ff214e
+       768        1 @ 0xbccc97 0xa42dc7 0xa456e4 0x7fcdc2ff214e
+      5760        2 @ 0xbccc97 0xb82b73 0xb82bcb 0xb87eab 0xb8814c 0x4e969d 0x4faa17 0x4fc5f6 0x4fd028 0x4fd230 0x79e80a 0x7a251b 0x7a296d 0xa456e4 0x7fcdc2ff214e
+    569088        1 @ 0xbccc97 0xb82b73 0xb82bcb 0xb87f08 0xb8814c 0x42ed5f 0x42edc1 0x42e15a 0x5261af 0x526edf 0x5280ab 0x79e80a 0x7a251b 0x7a296d 0xa456e4 0x7fcdc2ff214e
+      2432        1 @ 0xbccc97 0xb82b73 0xb82bcb 0xb87eab 0xb8814c 0x7aa74c 0x7ab844 0x7ab914 0x79e9e9 0x79e326 0x4d299e 0x4d4b7b 0x4b7be8 0x4b7ff1 0x4d2dae 0x79e80a
+   2034816        3 @ 0xbccc97 0xb82f0f 0xb83003 0xb87d50 0xc635f0 0x42ecc3 0x42e14c 0x5261af 0x526edf 0x5280ab 0x79e80a 0x7a251b 0x7a296d 0xa456e4 0x7fcdc2ff214e
+--- Memory map: ---
+  00400000-00fcb000: cppbench_server_main
+  7fcdc231e000-7fcdc2321000: /libnss_cache-2.15.so
+  7fcdc2522000-7fcdc252e000: /libnss_files-2.15.so
+  7fcdc272f000-7fcdc28dd000: /libc-2.15.so
+  7fcdc2ae7000-7fcdc2be2000: /libm-2.15.so
+  7fcdc2de3000-7fcdc2dea000: /librt-2.15.so
+  7fcdc2feb000-7fcdc3003000: /libpthread-2.15.so
+  7fcdc3208000-7fcdc320a000: /libdl-2.15.so
+  7fcdc340c000-7fcdc3415000: /libcrypt-2.15.so
+  7fcdc3645000-7fcdc3669000: /ld-2.15.so
+  7fff86bff000-7fff86c00000: [vdso]
+  ffffffffff600000-ffffffffff601000: [vsyscall]
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/cppbench.small.contention b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/cppbench.small.contention
new file mode 100644 (file)
index 0000000..230cd90
--- /dev/null
@@ -0,0 +1,19 @@
+--- contentionz 1 ---
+cycles/second = 3201000000
+sampling period = 100
+ms since reset = 16502830
+discarded samples = 0
+       100     10 @ 0xbccc97 0xc61202 0x42ed5f 0x42edc1 0x42e15a 0x5261af 0x526edf 0x5280ab 0x79e80a 0x7a251b 0x7a296d 0xa456e4 0x7fcdc2ff214e
+--- Memory map: ---
+  00400000-00fcb000: cppbench_server_main
+  7fcdc231e000-7fcdc2321000: /libnss_cache-2.15.so
+  7fcdc2522000-7fcdc252e000: /libnss_files-2.15.so
+  7fcdc272f000-7fcdc28dd000: /libc-2.15.so
+  7fcdc2ae7000-7fcdc2be2000: /libm-2.15.so
+  7fcdc2de3000-7fcdc2dea000: /librt-2.15.so
+  7fcdc2feb000-7fcdc3003000: /libpthread-2.15.so
+  7fcdc3208000-7fcdc320a000: /libdl-2.15.so
+  7fcdc340c000-7fcdc3415000: /libcrypt-2.15.so
+  7fcdc3645000-7fcdc3669000: /ld-2.15.so
+  7fff86bff000-7fff86c00000: [vdso]
+  ffffffffff600000-ffffffffff601000: [vsyscall]
index 2e130c809f10683dbbff90e1b06c9bb0234295b8..30cece7a37d48bdfa77be7160fed04dc9f6a0e54 100644 (file)
@@ -1,9 +1,9 @@
 digraph "unnamed" {
 node [style=filled fillcolor="#f8f8f8"]
 subgraph cluster_L { "Build ID: buildid-contention" [shape=box fontsize=16 label="Build ID: buildid-contention\lComment #1\lComment #2\lType: delay\lShowing nodes accounting for 149.50ms, 100% of 149.50ms total\l"] }
-N1 [label="file3000.src\n32.77ms (21.92%)\nof 149.50ms (100%)" fontsize=20 shape=box tooltip="testdata/file3000.src (149.50ms)" color="#b20000" fillcolor="#edd5d5"]
-N2 [label="file1000.src\n51.20ms (34.25%)" fontsize=23 shape=box tooltip="testdata/file1000.src (51.20ms)" color="#b23100" fillcolor="#eddbd5"]
-N3 [label="file2000.src\n65.54ms (43.84%)\nof 75.78ms (50.68%)" fontsize=24 shape=box tooltip="testdata/file2000.src (75.78ms)" color="#b22000" fillcolor="#edd9d5"]
+N1 [label="file3000.src\n32.77ms (21.92%)\nof 149.50ms (100%)" id="node1" fontsize=20 shape=box tooltip="testdata/file3000.src (149.50ms)" color="#b20000" fillcolor="#edd5d5"]
+N2 [label="file1000.src\n51.20ms (34.25%)" id="node2" fontsize=23 shape=box tooltip="testdata/file1000.src (51.20ms)" color="#b23100" fillcolor="#eddbd5"]
+N3 [label="file2000.src\n65.54ms (43.84%)\nof 75.78ms (50.68%)" id="node3" fontsize=24 shape=box tooltip="testdata/file2000.src (75.78ms)" color="#b22000" fillcolor="#edd9d5"]
 N1 -> N3 [label=" 75.78ms" weight=51 penwidth=3 color="#b22000" tooltip="testdata/file3000.src -> testdata/file2000.src (75.78ms)" labeltooltip="testdata/file3000.src -> testdata/file2000.src (75.78ms)"]
 N1 -> N2 [label=" 40.96ms" weight=28 penwidth=2 color="#b23900" tooltip="testdata/file3000.src -> testdata/file1000.src (40.96ms)" labeltooltip="testdata/file3000.src -> testdata/file1000.src (40.96ms)"]
 N3 -> N2 [label=" 10.24ms" weight=7 color="#b29775" tooltip="testdata/file2000.src -> testdata/file1000.src (10.24ms)" labeltooltip="testdata/file2000.src -> testdata/file1000.src (10.24ms)"]
index aa08a41c99272b0cf804d31b0ff0818411a0785e..03fbbb529685814811a220b7ac4c9793b8818b5d 100644 (file)
@@ -1,9 +1,9 @@
 digraph "unnamed" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "Build ID: buildid-contention" [shape=box fontsize=16 label="Build ID: buildid-contention\lComment #1\lComment #2\lType: delay\lShowing nodes accounting for 40.96ms, 27.40% of 149.50ms total\l"] }
-N1 [label="0000000000001000\nline1000\nfile1000.src:1\n40.96ms (27.40%)" fontsize=24 shape=box tooltip="0000000000001000 line1000 testdata/file1000.src:1 (40.96ms)" color="#b23900" fillcolor="#edddd5"]
-N2 [label="0000000000003001\nline3000\nfile3000.src:5\n0 of 40.96ms (27.40%)" fontsize=8 shape=box tooltip="0000000000003001 line3000 testdata/file3000.src:5 (40.96ms)" color="#b23900" fillcolor="#edddd5"]
-N3 [label="0000000000003001\nline3001\nfile3000.src:3\n0 of 40.96ms (27.40%)" fontsize=8 shape=box tooltip="0000000000003001 line3001 testdata/file3000.src:3 (40.96ms)" color="#b23900" fillcolor="#edddd5"]
+subgraph cluster_L { "Build ID: buildid-contention" [shape=box fontsize=16 label="Build ID: buildid-contention\lComment #1\lComment #2\lType: delay\lActive filters:\l   focus=[X1]000\l   ignore=[X3]002\lShowing nodes accounting for 40.96ms, 27.40% of 149.50ms total\l"] }
+N1 [label="0000000000001000\nline1000\nfile1000.src:1\n40.96ms (27.40%)" id="node1" fontsize=24 shape=box tooltip="0000000000001000 line1000 testdata/file1000.src:1 (40.96ms)" color="#b23900" fillcolor="#edddd5"]
+N2 [label="0000000000003001\nline3000\nfile3000.src:5\n0 of 40.96ms (27.40%)" id="node2" fontsize=8 shape=box tooltip="0000000000003001 line3000 testdata/file3000.src:5 (40.96ms)" color="#b23900" fillcolor="#edddd5"]
+N3 [label="0000000000003001\nline3001\nfile3000.src:3\n0 of 40.96ms (27.40%)" id="node3" fontsize=8 shape=box tooltip="0000000000003001 line3001 testdata/file3000.src:3 (40.96ms)" color="#b23900" fillcolor="#edddd5"]
 N2 -> N3 [label=" 40.96ms\n (inline)" weight=28 penwidth=2 color="#b23900" tooltip="0000000000003001 line3000 testdata/file3000.src:5 -> 0000000000003001 line3001 testdata/file3000.src:3 (40.96ms)" labeltooltip="0000000000003001 line3000 testdata/file3000.src:5 -> 0000000000003001 line3001 testdata/file3000.src:3 (40.96ms)"]
 N3 -> N1 [label=" 40.96ms" weight=28 penwidth=2 color="#b23900" tooltip="0000000000003001 line3001 testdata/file3000.src:3 -> 0000000000001000 line1000 testdata/file1000.src:1 (40.96ms)" labeltooltip="0000000000003001 line3001 testdata/file3000.src:3 -> 0000000000001000 line1000 testdata/file1000.src:1 (40.96ms)"]
 }
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.call_tree.callgrind b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.call_tree.callgrind
new file mode 100644 (file)
index 0000000..e2286f6
--- /dev/null
@@ -0,0 +1,99 @@
+positions: instr line
+events: cpu(ms)
+
+ob=(1) /path/to/testbinary
+fl=(1) testdata/file1000.src
+fn=(1) line1000
+0x1000 1 1000
+* 1 100
+
+ob=(1)
+fl=(2) testdata/file2000.src
+fn=(2) line2001
++4096 9 10
+
+ob=(1)
+fl=(3) testdata/file3000.src
+fn=(3) line3002
++4096 2 10
+cfl=(2)
+cfn=(4) line2000 [1/2]
+calls=0 * 4
+* * 1000
+
+ob=(1)
+fl=(2)
+fn=(5) line2000
+-4096 4 0
+cfl=(2)
+cfn=(6) line2001 [2/2]
+calls=0 -4096 9
+* * 1000
+* 4 0
+cfl=(2)
+cfn=(7) line2001 [1/2]
+calls=0 * 9
+* * 10
+
+ob=(1)
+fl=(2)
+fn=(2)
+* 9 0
+cfl=(1)
+cfn=(8) line1000 [1/2]
+calls=0 -4096 1
+* * 1000
+
+ob=(1)
+fl=(3)
+fn=(9) line3000
++4096 6 0
+cfl=(3)
+cfn=(10) line3001 [1/2]
+calls=0 +4096 5
+* * 1010
+
+ob=(1)
+fl=(3)
+fn=(11) line3001
+* 5 0
+cfl=(3)
+cfn=(12) line3002 [1/2]
+calls=0 * 2
+* * 1010
+
+ob=(1)
+fl=(3)
+fn=(9)
++1 9 0
+cfl=(3)
+cfn=(13) line3001 [2/2]
+calls=0 +1 8
+* * 100
+
+ob=(1)
+fl=(3)
+fn=(11)
+* 8 0
+cfl=(1)
+cfn=(14) line1000 [2/2]
+calls=0 -8193 1
+* * 100
+
+ob=(1)
+fl=(3)
+fn=(9)
++1 9 0
+cfl=(3)
+cfn=(15) line3002 [2/2]
+calls=0 +1 5
+* * 10
+
+ob=(1)
+fl=(3)
+fn=(3)
+* 5 0
+cfl=(2)
+cfn=(16) line2000 [2/2]
+calls=0 -4098 4
+* * 10
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.cum.lines.text.focus.hide b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.cum.lines.text.focus.hide
new file mode 100644 (file)
index 0000000..f0d928d
--- /dev/null
@@ -0,0 +1,8 @@
+Active filters:
+   focus=[12]00
+   hide=line[X3]0
+Showing nodes accounting for 1.11s, 99.11% of 1.12s total
+      flat  flat%   sum%        cum   cum%
+     1.10s 98.21% 98.21%      1.10s 98.21%  line1000 testdata/file1000.src:1
+         0     0% 98.21%      1.01s 90.18%  line2000 testdata/file2000.src:4
+     0.01s  0.89% 99.11%      1.01s 90.18%  line2001 testdata/file2000.src:9 (inline)
index 9d172713e5e00e75846bd9457d76b1ecd9014a32..bf503a57dba16829f6ae5a9da5682ed535f63df3 100644 (file)
@@ -1,3 +1,5 @@
+Active filters:
+   hide=line[X3]0
 Showing nodes accounting for 1.11s, 99.11% of 1.12s total
       flat  flat%   sum%        cum   cum%
      1.10s 98.21% 98.21%      1.10s 98.21%  line1000 testdata/file1000.src:1
index 9d172713e5e00e75846bd9457d76b1ecd9014a32..7604cb8d7b061b150b072315acfd8de520362e0a 100644 (file)
@@ -1,3 +1,5 @@
+Active filters:
+   show=[12]00
 Showing nodes accounting for 1.11s, 99.11% of 1.12s total
       flat  flat%   sum%        cum   cum%
      1.10s 98.21% 98.21%      1.10s 98.21%  line1000 testdata/file1000.src:1
index 33bf6814a463ce3b7bee54ade74e9b15f1489347..94b9be83df2e960476110e45028ef69c0f2921ac 100644 (file)
@@ -1,3 +1,5 @@
+Active filters:
+   hide=mangled[X3]0
 Showing nodes accounting for 1s, 100% of 1s total
       flat  flat%   sum%        cum   cum%
         1s   100%   100%         1s   100%  mangled1000 testdata/file1000.src:1
index 9c8e603195ee8859ac628970b8b43f9d4fc23127..e1df7b1b6437d9e8ca30e3685581943de346dcba 100644 (file)
@@ -2,9 +2,9 @@ Total: 1.12s
 ROUTINE ======================== line1000
      1.10s      1.10s (flat, cum) 98.21% of Total
      1.10s      1.10s       1000: instruction one                         ;line1000 file1000.src:1
-         .          .       1001: instruction two
-         .          .       1002: instruction three
-         .          .       1003: instruction four
+         .          .       1001: instruction two                         ;file1000.src:1
+         .          .       1002: instruction three                       ;file1000.src:2
+         .          .       1003: instruction four                        ;file1000.src:1
 ROUTINE ======================== line3000
       10ms      1.12s (flat, cum)   100% of Total
       10ms      1.01s       3000: instruction one                         ;line3000 file3000.src:6
index ccf4ee84491adb469c3b5349765961a09d474697..befc412db35e7045ee41b5a0b1cf4b2d400a96e4 100644 (file)
@@ -2,6 +2,7 @@
 <!DOCTYPE html>
 <html>
 <head>
+<meta charset="UTF-8">
 <title>Pprof listing</title>
 <style type="text/css">
 body {
@@ -14,17 +15,11 @@ h1 {
 .legend {
   font-size: 1.25em;
 }
-.line {
-color: #aaaaaa;
+.line, .nop, .unimportant {
+  color: #aaaaaa;
 }
-.nop {
-color: #aaaaaa;
-}
-.unimportant {
-color: #cccccc;
-}
-.disasmloc {
-color: #000000;
+.inlinesrc {
+  color: #000066;
 }
 .deadsrc {
 cursor: pointer;
@@ -69,39 +64,41 @@ Type: cpu<br>
 Duration: 10s, Total samples = 1.12s (11.20%)<br>Total: 1.12s</div><h1>line1000</h1>testdata/file1000.src
 <pre onClick="pprof_toggle_asm(event)">
   Total:       1.10s      1.10s (flat, cum) 98.21%
-<span class=line>      1</span> <span class=deadsrc>       1.10s      1.10s line1 </span><span class=asm>               1.10s      1.10s     1000: instruction one                                  <span class=disasmloc>file1000.src:1</span>
-                   .          .     1001: instruction two                                  <span class=disasmloc></span>
-                   .          .     1002: instruction three                                <span class=disasmloc></span>
-                   .          .     1003: instruction four                                 <span class=disasmloc></span>
+<span class=line>      1</span> <span class=deadsrc>       1.10s      1.10s           line1 </span><span class=asm>               1.10s      1.10s     1000:     instruction one                                                              <span class=unimportant>file1000.src:1</span>
+                   .          .     1001:     instruction two                                                              <span class=unimportant>file1000.src:1</span>
+                                     ⋮
+                   .          .     1003:     instruction four                                                             <span class=unimportant>file1000.src:1</span>
+</span>
+<span class=line>      2</span> <span class=deadsrc>           .          .           line2 </span><span class=asm>                   .          .     1002:     instruction three                                                            <span class=unimportant>file1000.src:2</span>
 </span>
-<span class=line>      2</span> <span class=nop>           .          . line2 </span>
-<span class=line>      3</span> <span class=nop>           .          . line3 </span>
-<span class=line>      4</span> <span class=nop>           .          . line4 </span>
-<span class=line>      5</span> <span class=nop>           .          . line5 </span>
-<span class=line>      6</span> <span class=nop>           .          . line6 </span>
+<span class=line>      3</span> <span class=nop>           .          .           line3 </span>
+<span class=line>      4</span> <span class=nop>           .          .           line4 </span>
+<span class=line>      5</span> <span class=nop>           .          .           line5 </span>
+<span class=line>      6</span> <span class=nop>           .          .           line6 </span>
+<span class=line>      7</span> <span class=nop>           .          .           line7 </span>
 </pre>
 <h1>line3000</h1>testdata/file3000.src
 <pre onClick="pprof_toggle_asm(event)">
   Total:        10ms      1.12s (flat, cum)   100%
-<span class=line>      1</span> <span class=nop>           .          . line1 </span>
-<span class=line>      2</span> <span class=nop>           .          . line2 </span>
-<span class=line>      3</span> <span class=nop>           .          . line3 </span>
-<span class=line>      4</span> <span class=nop>           .          . line4 </span>
-<span class=line>      5</span> <span class=nop>           .          . line5 </span>
-<span class=line>      6</span> <span class=deadsrc>        10ms      1.01s line6 </span><span class=asm>                10ms      1.01s     3000: instruction one                                  <span class=disasmloc>file3000.src:6</span>
+<span class=line>      1</span> <span class=nop>           .          .           line1 </span>
+<span class=line>      2</span> <span class=nop>           .          .           line2 </span>
+<span class=line>      3</span> <span class=nop>           .          .           line3 </span>
+<span class=line>      4</span> <span class=nop>           .          .           line4 </span>
+<span class=line>      5</span> <span class=nop>           .          .           line5 </span>
+<span class=line>      6</span> <span class=deadsrc>        10ms      1.01s           line6 </span><span class=asm>                10ms      1.01s     3000:     instruction one                                                              <span class=unimportant>file3000.src:6</span>
 </span>
-<span class=line>      7</span> <span class=nop>           .          . line7 </span>
-<span class=line>      8</span> <span class=nop>           .          . line8 </span>
-<span class=line>      9</span> <span class=deadsrc>           .      110ms line9 </span><span class=asm>                   .      100ms     3001: instruction two                                  <span class=disasmloc>file3000.src:9</span>
-                   .       10ms     3002: instruction three                                <span class=disasmloc>file3000.src:9</span>
-                   .          .     3003: instruction four                                 <span class=disasmloc></span>
-                   .          .     3004: instruction five                                 <span class=disasmloc></span>
+<span class=line>      7</span> <span class=nop>           .          .           line7 </span>
+<span class=line>      8</span> <span class=nop>           .          .           line8 </span>
+<span class=line>      9</span> <span class=deadsrc>           .      110ms           line9 </span><span class=asm>                   .      100ms     3001:     instruction two                                                              <span class=unimportant>file3000.src:9</span>
+                   .       10ms     3002:     instruction three                                                            <span class=unimportant>file3000.src:9</span>
+                   .          .     3003:     instruction four                                                             <span class=unimportant></span>
+                   .          .     3004:     instruction five                                                             <span class=unimportant></span>
 </span>
-<span class=line>     10</span> <span class=nop>           .          . line0 </span>
-<span class=line>     11</span> <span class=nop>           .          . line1 </span>
-<span class=line>     12</span> <span class=nop>           .          . line2 </span>
-<span class=line>     13</span> <span class=nop>           .          . line3 </span>
-<span class=line>     14</span> <span class=nop>           .          . line4 </span>
+<span class=line>     10</span> <span class=nop>           .          .           line0 </span>
+<span class=line>     11</span> <span class=nop>           .          .           line1 </span>
+<span class=line>     12</span> <span class=nop>           .          .           line2 </span>
+<span class=line>     13</span> <span class=nop>           .          .           line3 </span>
+<span class=line>     14</span> <span class=nop>           .          .           line4 </span>
 </pre>
 
 </body>
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.flat.functions.call_tree.dot b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.flat.functions.call_tree.dot
new file mode 100644 (file)
index 0000000..e854b5d
--- /dev/null
@@ -0,0 +1,21 @@
+digraph "testbinary" {
+node [style=filled fillcolor="#f8f8f8"]
+subgraph cluster_L { "File: testbinary" [shape=box fontsize=16 label="File: testbinary\lType: cpu\lDuration: 10s, Total samples = 1.12s (11.20%)\lShowing nodes accounting for 1.11s, 99.11% of 1.12s total\lDropped 3 nodes (cum <= 0.06s)\l" tooltip="testbinary"] }
+N1 [label="line1000\n1s (89.29%)" id="node1" fontsize=24 shape=box tooltip="line1000 (1s)" color="#b20500" fillcolor="#edd6d5"]
+N1_0 [label = "key1:tag1\nkey2:tag1" id="N1_0" fontsize=8 shape=box3d tooltip="1s"]
+N1 -> N1_0 [label=" 1s" weight=100 tooltip="1s" labeltooltip="1s"]
+N2 [label="line3000\n0 of 1.12s (100%)" id="node2" fontsize=8 shape=box tooltip="line3000 (1.12s)" color="#b20000" fillcolor="#edd5d5"]
+N3 [label="line3001\n0 of 1.11s (99.11%)" id="node3" fontsize=8 shape=box tooltip="line3001 (1.11s)" color="#b20000" fillcolor="#edd5d5"]
+N4 [label="line1000\n0.10s (8.93%)" id="node4" fontsize=14 shape=box tooltip="line1000 (0.10s)" color="#b28b62" fillcolor="#ede8e2"]
+N4_0 [label = "key1:tag2\nkey3:tag2" id="N4_0" fontsize=8 shape=box3d tooltip="0.10s"]
+N4 -> N4_0 [label=" 0.10s" weight=100 tooltip="0.10s" labeltooltip="0.10s"]
+N5 [label="line3002\n0.01s (0.89%)\nof 1.01s (90.18%)" id="node5" fontsize=10 shape=box tooltip="line3002 (1.01s)" color="#b20500" fillcolor="#edd6d5"]
+N6 [label="line2000\n0 of 1s (89.29%)" id="node6" fontsize=8 shape=box tooltip="line2000 (1s)" color="#b20500" fillcolor="#edd6d5"]
+N7 [label="line2001\n0 of 1s (89.29%)" id="node7" fontsize=8 shape=box tooltip="line2001 (1s)" color="#b20500" fillcolor="#edd6d5"]
+N2 -> N3 [label=" 1.11s\n (inline)" weight=100 penwidth=5 color="#b20000" tooltip="line3000 -> line3001 (1.11s)" labeltooltip="line3000 -> line3001 (1.11s)"]
+N3 -> N5 [label=" 1.01s\n (inline)" weight=91 penwidth=5 color="#b20500" tooltip="line3001 -> line3002 (1.01s)" labeltooltip="line3001 -> line3002 (1.01s)"]
+N6 -> N7 [label=" 1s\n (inline)" weight=90 penwidth=5 color="#b20500" tooltip="line2000 -> line2001 (1s)" labeltooltip="line2000 -> line2001 (1s)"]
+N7 -> N1 [label=" 1s" weight=90 penwidth=5 color="#b20500" tooltip="line2001 -> line1000 (1s)" labeltooltip="line2001 -> line1000 (1s)"]
+N5 -> N6 [label=" 1s" weight=90 penwidth=5 color="#b20500" tooltip="line3002 -> line2000 (1s)" labeltooltip="line3002 -> line2000 (1s)"]
+N3 -> N4 [label=" 0.10s" weight=9 color="#b28b62" tooltip="line3001 -> line1000 (0.10s)" labeltooltip="line3001 -> line1000 (0.10s)"]
+}
index 18b1abf54a9c987a892e27d996499b8f72fee640..f0a5226b89f340c07efbbd90cd5c5b271b2e65b8 100644 (file)
@@ -1,20 +1,20 @@
 digraph "testbinary" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "File: testbinary" [shape=box fontsize=16 label="File: testbinary\lType: cpu\lDuration: 10s, Total samples = 1.12s (11.20%)\lShowing nodes accounting for 1.12s, 100% of 1.12s total\l"] }
-N1 [label="line1000\nfile1000.src\n1.10s (98.21%)" fontsize=24 shape=box tooltip="line1000 testdata/file1000.src (1.10s)" color="#b20000" fillcolor="#edd5d5"]
-N1_0 [label = "key1:tag1\nkey2:tag1" fontsize=8 shape=box3d tooltip="1s"]
+subgraph cluster_L { "File: testbinary" [shape=box fontsize=16 label="File: testbinary\lType: cpu\lDuration: 10s, Total samples = 1.12s (11.20%)\lShowing nodes accounting for 1.12s, 100% of 1.12s total\l" tooltip="testbinary"] }
+N1 [label="line1000\n1.10s (98.21%)" id="node1" fontsize=24 shape=box tooltip="line1000 (1.10s)" color="#b20000" fillcolor="#edd5d5"]
+N1_0 [label = "key1:tag1\nkey2:tag1" id="N1_0" fontsize=8 shape=box3d tooltip="1s"]
 N1 -> N1_0 [label=" 1s" weight=100 tooltip="1s" labeltooltip="1s"]
-N1_1 [label = "key1:tag2\nkey3:tag2" fontsize=8 shape=box3d tooltip="0.10s"]
+N1_1 [label = "key1:tag2\nkey3:tag2" id="N1_1" fontsize=8 shape=box3d tooltip="0.10s"]
 N1 -> N1_1 [label=" 0.10s" weight=100 tooltip="0.10s" labeltooltip="0.10s"]
-N2 [label="line3000\nfile3000.src\n0 of 1.12s (100%)" fontsize=8 shape=box tooltip="line3000 testdata/file3000.src (1.12s)" color="#b20000" fillcolor="#edd5d5"]
-N3 [label="line3001\nfile3000.src\n0 of 1.11s (99.11%)" fontsize=8 shape=box tooltip="line3001 testdata/file3000.src (1.11s)" color="#b20000" fillcolor="#edd5d5"]
-N4 [label="line3002\nfile3000.src\n0.01s (0.89%)\nof 1.02s (91.07%)" fontsize=10 shape=box tooltip="line3002 testdata/file3000.src (1.02s)" color="#b20400" fillcolor="#edd6d5"]
-N5 [label="line2001\nfile2000.src\n0.01s (0.89%)\nof 1.01s (90.18%)" fontsize=10 shape=box tooltip="line2001 testdata/file2000.src (1.01s)" color="#b20500" fillcolor="#edd6d5"]
-N6 [label="line2000\nfile2000.src\n0 of 1.01s (90.18%)" fontsize=8 shape=box tooltip="line2000 testdata/file2000.src (1.01s)" color="#b20500" fillcolor="#edd6d5"]
-N2 -> N3 [label=" 1.11s\n (inline)" weight=100 penwidth=5 color="#b20000" tooltip="line3000 testdata/file3000.src -> line3001 testdata/file3000.src (1.11s)" labeltooltip="line3000 testdata/file3000.src -> line3001 testdata/file3000.src (1.11s)"]
-N6 -> N5 [label=" 1.01s\n (inline)" weight=91 penwidth=5 color="#b20500" tooltip="line2000 testdata/file2000.src -> line2001 testdata/file2000.src (1.01s)" labeltooltip="line2000 testdata/file2000.src -> line2001 testdata/file2000.src (1.01s)"]
-N3 -> N4 [label=" 1.01s\n (inline)" weight=91 penwidth=5 color="#b20500" tooltip="line3001 testdata/file3000.src -> line3002 testdata/file3000.src (1.01s)" labeltooltip="line3001 testdata/file3000.src -> line3002 testdata/file3000.src (1.01s)"]
-N4 -> N6 [label=" 1.01s" weight=91 penwidth=5 color="#b20500" tooltip="line3002 testdata/file3000.src -> line2000 testdata/file2000.src (1.01s)" labeltooltip="line3002 testdata/file3000.src -> line2000 testdata/file2000.src (1.01s)"]
-N5 -> N1 [label=" 1s" weight=90 penwidth=5 color="#b20500" tooltip="line2001 testdata/file2000.src -> line1000 testdata/file1000.src (1s)" labeltooltip="line2001 testdata/file2000.src -> line1000 testdata/file1000.src (1s)"]
-N3 -> N1 [label=" 0.10s" weight=9 color="#b28b62" tooltip="line3001 testdata/file3000.src -> line1000 testdata/file1000.src (0.10s)" labeltooltip="line3001 testdata/file3000.src -> line1000 testdata/file1000.src (0.10s)"]
+N2 [label="line3000\n0 of 1.12s (100%)" id="node2" fontsize=8 shape=box tooltip="line3000 (1.12s)" color="#b20000" fillcolor="#edd5d5"]
+N3 [label="line3001\n0 of 1.11s (99.11%)" id="node3" fontsize=8 shape=box tooltip="line3001 (1.11s)" color="#b20000" fillcolor="#edd5d5"]
+N4 [label="line3002\n0.01s (0.89%)\nof 1.02s (91.07%)" id="node4" fontsize=10 shape=box tooltip="line3002 (1.02s)" color="#b20400" fillcolor="#edd6d5"]
+N5 [label="line2001\n0.01s (0.89%)\nof 1.01s (90.18%)" id="node5" fontsize=10 shape=box tooltip="line2001 (1.01s)" color="#b20500" fillcolor="#edd6d5"]
+N6 [label="line2000\n0 of 1.01s (90.18%)" id="node6" fontsize=8 shape=box tooltip="line2000 (1.01s)" color="#b20500" fillcolor="#edd6d5"]
+N2 -> N3 [label=" 1.11s\n (inline)" weight=100 penwidth=5 color="#b20000" tooltip="line3000 -> line3001 (1.11s)" labeltooltip="line3000 -> line3001 (1.11s)"]
+N6 -> N5 [label=" 1.01s\n (inline)" weight=91 penwidth=5 color="#b20500" tooltip="line2000 -> line2001 (1.01s)" labeltooltip="line2000 -> line2001 (1.01s)"]
+N3 -> N4 [label=" 1.01s\n (inline)" weight=91 penwidth=5 color="#b20500" tooltip="line3001 -> line3002 (1.01s)" labeltooltip="line3001 -> line3002 (1.01s)"]
+N4 -> N6 [label=" 1.01s" weight=91 penwidth=5 color="#b20500" tooltip="line3002 -> line2000 (1.01s)" labeltooltip="line3002 -> line2000 (1.01s)"]
+N5 -> N1 [label=" 1s" weight=90 penwidth=5 color="#b20500" tooltip="line2001 -> line1000 (1s)" labeltooltip="line2001 -> line1000 (1s)"]
+N3 -> N1 [label=" 0.10s" weight=9 color="#b28b62" tooltip="line3001 -> line1000 (0.10s)" labeltooltip="line3001 -> line1000 (0.10s)"]
 }
index 0807ed232520b8816f10ce631caa75548df0f9ed..66e4189e0a2d502e234589e0c520c63853627908 100644 (file)
@@ -1,8 +1,8 @@
 Showing nodes accounting for 1.12s, 100% of 1.12s total
       flat  flat%   sum%        cum   cum%
-     1.10s 98.21% 98.21%      1.10s 98.21%  line1000 testdata/file1000.src
-     0.01s  0.89% 99.11%      1.01s 90.18%  line2001 testdata/file2000.src (inline)
-     0.01s  0.89%   100%      1.02s 91.07%  line3002 testdata/file3000.src (inline)
-         0     0%   100%      1.01s 90.18%  line2000 testdata/file2000.src
-         0     0%   100%      1.12s   100%  line3000 testdata/file3000.src
-         0     0%   100%      1.11s 99.11%  line3001 testdata/file3000.src (inline)
+     1.10s 98.21% 98.21%      1.10s 98.21%  line1000
+     0.01s  0.89% 99.11%      1.01s 90.18%  line2001 (inline)
+     0.01s  0.89%   100%      1.02s 91.07%  line3002 (inline)
+         0     0%   100%      1.01s 90.18%  line2000
+         0     0%   100%      1.12s   100%  line3000
+         0     0%   100%      1.11s 99.11%  line3001 (inline)
index 1a4a70c4d79f957f4cf2ac785edad60629dd0f71..3b8a3537b46d13f525c7edd3cc15b68a45b2fb6a 100644 (file)
@@ -2,12 +2,12 @@ Showing nodes accounting for 1.12s, 100% of 1.12s total
 ----------------------------------------------------------+-------------
       flat  flat%   sum%        cum   cum%   calls calls% + context             
 ----------------------------------------------------------+-------------
-                                             1.01s   100% |   line2000 testdata/file2000.src (inline)
-     0.01s  0.89%  0.89%      1.01s 90.18%                | line2001 testdata/file2000.src
-                                                1s 99.01% |   line1000 testdata/file1000.src
-----------------------------------------------------------+-------------
-                                             1.11s   100% |   line3000 testdata/file3000.src (inline)
-         0     0%  0.89%      1.11s 99.11%                | line3001 testdata/file3000.src
-                                             1.01s 90.99% |   line3002 testdata/file3000.src (inline)
-                                             0.10s  9.01% |   line1000 testdata/file1000.src
+                                             1.01s   100% |   line2000 (inline)
+     0.01s  0.89%  0.89%      1.01s 90.18%                | line2001
+                                                1s 99.01% |   line1000
+----------------------------------------------------------+-------------
+                                             1.11s   100% |   line3000 (inline)
+         0     0%  0.89%      1.11s 99.11%                | line3001
+                                             1.01s 90.99% |   line3002 (inline)
+                                             0.10s  9.01% |   line1000
 ----------------------------------------------------------+-------------
index fc784f0c4ca6ee7671cfd4ed43dfc544c8aade53..5998b5ba5bf500911d78a075b25404643da7476a 100644 (file)
@@ -1,13 +1,13 @@
-key1: Total 1120
-      1000 (89.29%): tag1
-       100 ( 8.93%): tag2
-        10 ( 0.89%): tag3
-        10 ( 0.89%): tag4
+ key1: Total 1.1s
+          1.0s (89.29%): tag1
+       100.0ms ( 8.93%): tag2
+        10.0ms ( 0.89%): tag3
+        10.0ms ( 0.89%): tag4
 
-key2: Total 1020
-      1010 (99.02%): tag1
-        10 ( 0.98%): tag2
+ key2: Total 1.0s
+         1.0s (99.02%): tag1
+       10.0ms ( 0.98%): tag2
 
-key3: Total 100
-       100 (  100%): tag2
+ key3: Total 100.0ms
+       100.0ms (  100%): tag2
 
index 650ebb1fdbde474ed0e46b3ff4f5f8b712b558ce..9b99d4368c2c89e28d15f290758ea188b007374a 100644 (file)
@@ -1,6 +1,6 @@
-key1: Total 100
-       100 (  100%): tag2
+ key1: Total 100.0ms
+       100.0ms (  100%): tag2
 
-key3: Total 100
-       100 (  100%): tag2
+ key3: Total 100.0ms
+       100.0ms (  100%): tag2
 
index d59fe30fe028bc3507f0855b455ac4d853946948..d9637c0e42555344b9dc7170bacc5ba5f05fde5d 100644 (file)
@@ -4,29 +4,29 @@ Duration: 10s, Total samples = 1.12s (11.20%)
 -----------+-------------------------------------------------------
       key1:  tag1
       key2:  tag1
-        1s   line1000 testdata/file1000.src
-             line2001 testdata/file2000.src
-             line2000 testdata/file2000.src
-             line3002 testdata/file3000.src
-             line3001 testdata/file3000.src
-             line3000 testdata/file3000.src
+        1s   line1000
+             line2001
+             line2000
+             line3002
+             line3001
+             line3000
 -----------+-------------------------------------------------------
       key1:  tag2
       key3:  tag2
-     100ms   line1000 testdata/file1000.src
-             line3001 testdata/file3000.src
-             line3000 testdata/file3000.src
+     100ms   line1000
+             line3001
+             line3000
 -----------+-------------------------------------------------------
       key1:  tag3
       key2:  tag2
-      10ms   line2001 testdata/file2000.src
-             line2000 testdata/file2000.src
-             line3002 testdata/file3000.src
-             line3000 testdata/file3000.src
+      10ms   line2001
+             line2000
+             line3002
+             line3000
 -----------+-------------------------------------------------------
       key1:  tag4
       key2:  tag1
-      10ms   line3002 testdata/file3000.src
-             line3001 testdata/file3000.src
-             line3000 testdata/file3000.src
+      10ms   line3002
+             line3001
+             line3000
 -----------+-------------------------------------------------------
index cda6d65b38a3b6cd0594080a68a7f353e7e6c5e6..9d4ba72b1f9b5da57ab60aabb5565a571ec4994b 100644 (file)
@@ -1,3 +1,5 @@
+Active filters:
+   focus=[24]00
 Showing nodes accounting for 62.50MB, 63.37% of 98.63MB total
 Dropped 2 nodes (cum <= 4.93MB)
 ----------------------------------------------------------+-------------
index 35f0bf57623e391f2d52e2e071cc6a5774b20572..c2d11838fe3cfad0c6c0a8119cb3b75e55a4fa01 100644 (file)
@@ -1,19 +1,21 @@
+Active filters:
+   focus=[24]00
 Showing nodes accounting for 62.50MB, 98.46% of 63.48MB total
 Dropped 2 nodes (cum <= 3.17MB)
 ----------------------------------------------------------+-------------
       flat  flat%   sum%        cum   cum%   calls calls% + context             
 ----------------------------------------------------------+-------------
-                                           63.48MB   100% |   line3002 testdata/file3000.src
-         0     0%     0%    63.48MB   100%                | line2000 testdata/file2000.src
-                                           63.48MB   100% |   line2001 testdata/file2000.src (inline)
+                                           63.48MB   100% |   line3002
+         0     0%     0%    63.48MB   100%                | line2000
+                                           63.48MB   100% |   line2001 (inline)
 ----------------------------------------------------------+-------------
-                                           63.48MB   100% |   line2000 testdata/file2000.src (inline)
-   62.50MB 98.46% 98.46%    63.48MB   100%                | line2001 testdata/file2000.src
+                                           63.48MB   100% |   line2000 (inline)
+   62.50MB 98.46% 98.46%    63.48MB   100%                | line2001
 ----------------------------------------------------------+-------------
-         0     0% 98.46%    63.48MB   100%                | line3000 testdata/file3000.src
-                                           63.48MB   100% |   line3002 testdata/file3000.src (inline)
+         0     0% 98.46%    63.48MB   100%                | line3000
+                                           63.48MB   100% |   line3002 (inline)
 ----------------------------------------------------------+-------------
-                                           63.48MB   100% |   line3000 testdata/file3000.src (inline)
-         0     0% 98.46%    63.48MB   100%                | line3002 testdata/file3000.src
-                                           63.48MB   100% |   line2000 testdata/file2000.src
+                                           63.48MB   100% |   line3000 (inline)
+         0     0% 98.46%    63.48MB   100%                | line3002
+                                           63.48MB   100% |   line2000
 ----------------------------------------------------------+-------------
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap.flat.files.text.focus b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap.flat.files.text.focus
new file mode 100644 (file)
index 0000000..20a503f
--- /dev/null
@@ -0,0 +1,8 @@
+Active filters:
+   focus=[12]00
+   taghide=[X3]00
+Showing nodes accounting for 67.38MB, 68.32% of 98.63MB total
+      flat  flat%   sum%        cum   cum%
+   62.50MB 63.37% 63.37%    63.48MB 64.36%  testdata/file2000.src
+    4.88MB  4.95% 68.32%     4.88MB  4.95%  testdata/file1000.src
+         0     0% 68.32%    67.38MB 68.32%  testdata/file3000.src
index bc061ad733579e657b44b7a9c50e5e420882bc4c..929461a3c1ef457dfef8a73db426a7713bb9e173 100644 (file)
@@ -1,8 +1,8 @@
 Showing nodes accounting for 150, 100% of 150 total
       flat  flat%   sum%        cum   cum%
-        80 53.33% 53.33%        130 86.67%  line3002 testdata/file3000.src (inline)
-        40 26.67% 80.00%         50 33.33%  line2001 testdata/file2000.src (inline)
-        30 20.00%   100%         30 20.00%  line1000 testdata/file1000.src
-         0     0%   100%         50 33.33%  line2000 testdata/file2000.src
-         0     0%   100%        150   100%  line3000 testdata/file3000.src
-         0     0%   100%        110 73.33%  line3001 testdata/file3000.src (inline)
+        80 53.33% 53.33%        130 86.67%  line3002 (inline)
+        40 26.67% 80.00%         50 33.33%  line2001 (inline)
+        30 20.00%   100%         30 20.00%  line1000
+         0     0%   100%         50 33.33%  line2000
+         0     0%   100%        150   100%  line3000
+         0     0%   100%        110 73.33%  line3001 (inline)
index c8533f3d442223d1e905962cd3a4e101e17c73ff..909a824f1e8221616b87d418fc04ac15a098cb73 100644 (file)
@@ -1,13 +1,13 @@
 digraph "unnamed" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: inuse_space\lShowing nodes accounting for 62.50MB, 63.37% of 98.63MB total\l"] }
-N1 [label="line2001\nfile2000.src\n62.50MB (63.37%)" fontsize=24 shape=box tooltip="line2001 testdata/file2000.src (62.50MB)" color="#b21600" fillcolor="#edd8d5"]
-NN1_0 [label = "1.56MB" fontsize=8 shape=box3d tooltip="62.50MB"]
+subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: inuse_space\lActive filters:\l   tagfocus=1mb:2gb\lShowing nodes accounting for 62.50MB, 63.37% of 98.63MB total\l"] }
+N1 [label="line2001\n62.50MB (63.37%)" id="node1" fontsize=24 shape=box tooltip="line2001 (62.50MB)" color="#b21600" fillcolor="#edd8d5"]
+NN1_0 [label = "1.56MB" id="NN1_0" fontsize=8 shape=box3d tooltip="62.50MB"]
 N1 -> NN1_0 [label=" 62.50MB" weight=100 tooltip="62.50MB" labeltooltip="62.50MB"]
-N2 [label="line3000\nfile3000.src\n0 of 62.50MB (63.37%)" fontsize=8 shape=box tooltip="line3000 testdata/file3000.src (62.50MB)" color="#b21600" fillcolor="#edd8d5"]
-N3 [label="line2000\nfile2000.src\n0 of 62.50MB (63.37%)" fontsize=8 shape=box tooltip="line2000 testdata/file2000.src (62.50MB)" color="#b21600" fillcolor="#edd8d5"]
-N4 [label="line3002\nfile3000.src\n0 of 62.50MB (63.37%)" fontsize=8 shape=box tooltip="line3002 testdata/file3000.src (62.50MB)" color="#b21600" fillcolor="#edd8d5"]
-N3 -> N1 [label=" 62.50MB\n (inline)" weight=64 penwidth=4 color="#b21600" tooltip="line2000 testdata/file2000.src -> line2001 testdata/file2000.src (62.50MB)" labeltooltip="line2000 testdata/file2000.src -> line2001 testdata/file2000.src (62.50MB)"]
-N2 -> N4 [label=" 62.50MB\n (inline)" weight=64 penwidth=4 color="#b21600" tooltip="line3000 testdata/file3000.src -> line3002 testdata/file3000.src (62.50MB)" labeltooltip="line3000 testdata/file3000.src -> line3002 testdata/file3000.src (62.50MB)"]
-N4 -> N3 [label=" 62.50MB" weight=64 penwidth=4 color="#b21600" tooltip="line3002 testdata/file3000.src -> line2000 testdata/file2000.src (62.50MB)" labeltooltip="line3002 testdata/file3000.src -> line2000 testdata/file2000.src (62.50MB)"]
+N2 [label="line3000\n0 of 62.50MB (63.37%)" id="node2" fontsize=8 shape=box tooltip="line3000 (62.50MB)" color="#b21600" fillcolor="#edd8d5"]
+N3 [label="line2000\n0 of 62.50MB (63.37%)" id="node3" fontsize=8 shape=box tooltip="line2000 (62.50MB)" color="#b21600" fillcolor="#edd8d5"]
+N4 [label="line3002\n0 of 62.50MB (63.37%)" id="node4" fontsize=8 shape=box tooltip="line3002 (62.50MB)" color="#b21600" fillcolor="#edd8d5"]
+N3 -> N1 [label=" 62.50MB\n (inline)" weight=64 penwidth=4 color="#b21600" tooltip="line2000 -> line2001 (62.50MB)" labeltooltip="line2000 -> line2001 (62.50MB)"]
+N2 -> N4 [label=" 62.50MB\n (inline)" weight=64 penwidth=4 color="#b21600" tooltip="line3000 -> line3002 (62.50MB)" labeltooltip="line3000 -> line3002 (62.50MB)"]
+N4 -> N3 [label=" 62.50MB" weight=64 penwidth=4 color="#b21600" tooltip="line3002 -> line2000 (62.50MB)" labeltooltip="line3002 -> line2000 (62.50MB)"]
 }
index 40354dd35d5b176b84c0ad8dc36cc5c8b061892d..b2929ae667d793bf58f6cfd2502df88eac896dab 100644 (file)
@@ -1,16 +1,16 @@
 digraph "unnamed" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: inuse_space\lShowing nodes accounting for 36.13MB, 36.63% of 98.63MB total\lDropped 2 nodes (cum <= 4.93MB)\l"] }
-N1 [label="line3002\nfile3000.src\n31.25MB (31.68%)\nof 32.23MB (32.67%)" fontsize=24 shape=box tooltip="line3002 testdata/file3000.src (32.23MB)" color="#b23200" fillcolor="#eddcd5"]
-NN1_0 [label = "400kB" fontsize=8 shape=box3d tooltip="31.25MB"]
+subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: inuse_space\lActive filters:\l   tagfocus=30kb:\l   tagignore=1mb:2mb\lShowing nodes accounting for 36.13MB, 36.63% of 98.63MB total\lDropped 2 nodes (cum <= 4.93MB)\l"] }
+N1 [label="line3002\n31.25MB (31.68%)\nof 32.23MB (32.67%)" id="node1" fontsize=24 shape=box tooltip="line3002 (32.23MB)" color="#b23200" fillcolor="#eddcd5"]
+NN1_0 [label = "400kB" id="NN1_0" fontsize=8 shape=box3d tooltip="31.25MB"]
 N1 -> NN1_0 [label=" 31.25MB" weight=100 tooltip="31.25MB" labeltooltip="31.25MB"]
-N2 [label="line3000\nfile3000.src\n0 of 36.13MB (36.63%)" fontsize=8 shape=box tooltip="line3000 testdata/file3000.src (36.13MB)" color="#b22e00" fillcolor="#eddbd5"]
-N3 [label="line3001\nfile3000.src\n0 of 36.13MB (36.63%)" fontsize=8 shape=box tooltip="line3001 testdata/file3000.src (36.13MB)" color="#b22e00" fillcolor="#eddbd5"]
-N4 [label="line1000\nfile1000.src\n4.88MB (4.95%)" fontsize=15 shape=box tooltip="line1000 testdata/file1000.src (4.88MB)" color="#b2a086" fillcolor="#edeae7"]
-NN4_0 [label = "200kB" fontsize=8 shape=box3d tooltip="3.91MB"]
+N2 [label="line3000\n0 of 36.13MB (36.63%)" id="node2" fontsize=8 shape=box tooltip="line3000 (36.13MB)" color="#b22e00" fillcolor="#eddbd5"]
+N3 [label="line3001\n0 of 36.13MB (36.63%)" id="node3" fontsize=8 shape=box tooltip="line3001 (36.13MB)" color="#b22e00" fillcolor="#eddbd5"]
+N4 [label="line1000\n4.88MB (4.95%)" id="node4" fontsize=15 shape=box tooltip="line1000 (4.88MB)" color="#b2a086" fillcolor="#edeae7"]
+NN4_0 [label = "200kB" id="NN4_0" fontsize=8 shape=box3d tooltip="3.91MB"]
 N4 -> NN4_0 [label=" 3.91MB" weight=100 tooltip="3.91MB" labeltooltip="3.91MB"]
-N2 -> N3 [label=" 36.13MB\n (inline)" weight=37 penwidth=2 color="#b22e00" tooltip="line3000 testdata/file3000.src -> line3001 testdata/file3000.src (36.13MB)" labeltooltip="line3000 testdata/file3000.src -> line3001 testdata/file3000.src (36.13MB)"]
-N3 -> N1 [label=" 32.23MB\n (inline)" weight=33 penwidth=2 color="#b23200" tooltip="line3001 testdata/file3000.src -> line3002 testdata/file3000.src (32.23MB)" labeltooltip="line3001 testdata/file3000.src -> line3002 testdata/file3000.src (32.23MB)"]
-N3 -> N4 [label=" 3.91MB" weight=4 color="#b2a58f" tooltip="line3001 testdata/file3000.src -> line1000 testdata/file1000.src (3.91MB)" labeltooltip="line3001 testdata/file3000.src -> line1000 testdata/file1000.src (3.91MB)"]
-N1 -> N4 [label=" 0.98MB" color="#b2b0a9" tooltip="line3002 testdata/file3000.src ... line1000 testdata/file1000.src (0.98MB)" labeltooltip="line3002 testdata/file3000.src ... line1000 testdata/file1000.src (0.98MB)" style="dotted" minlen=2]
+N2 -> N3 [label=" 36.13MB\n (inline)" weight=37 penwidth=2 color="#b22e00" tooltip="line3000 -> line3001 (36.13MB)" labeltooltip="line3000 -> line3001 (36.13MB)"]
+N3 -> N1 [label=" 32.23MB\n (inline)" weight=33 penwidth=2 color="#b23200" tooltip="line3001 -> line3002 (32.23MB)" labeltooltip="line3001 -> line3002 (32.23MB)"]
+N3 -> N4 [label=" 3.91MB" weight=4 color="#b2a58f" tooltip="line3001 -> line1000 (3.91MB)" labeltooltip="line3001 -> line1000 (3.91MB)"]
+N1 -> N4 [label=" 0.98MB" color="#b2b0a9" tooltip="line3002 ... line1000 (0.98MB)" labeltooltip="line3002 ... line1000 (0.98MB)" style="dotted" minlen=2]
 }
index f05969cfefb85daf85b4db469f935d59174caa81..9af034107604396b1c993294feb25013c556357c 100644 (file)
@@ -1,16 +1,16 @@
 digraph "unnamed" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: inuse_space\lShowing nodes accounting for 67.38MB, 68.32% of 98.63MB total\l"] }
-N1 [label="line3000\nfile3000.src:4\n0 of 67.38MB (68.32%)" fontsize=8 shape=box tooltip="line3000 testdata/file3000.src:4 (67.38MB)" color="#b21300" fillcolor="#edd7d5"]
-N2 [label="line2001\nfile2000.src:2\n62.50MB (63.37%)\nof 63.48MB (64.36%)" fontsize=24 shape=box tooltip="line2001 testdata/file2000.src:2 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
-NN2_0 [label = "1.56MB" fontsize=8 shape=box3d tooltip="62.50MB"]
+subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: inuse_space\lActive filters:\l   focus=[12]00\lShowing nodes accounting for 67.38MB, 68.32% of 98.63MB total\l"] }
+N1 [label="line3000\nfile3000.src:4\n0 of 67.38MB (68.32%)" id="node1" fontsize=8 shape=box tooltip="line3000 testdata/file3000.src:4 (67.38MB)" color="#b21300" fillcolor="#edd7d5"]
+N2 [label="line2001\nfile2000.src:2\n62.50MB (63.37%)\nof 63.48MB (64.36%)" id="node2" fontsize=24 shape=box tooltip="line2001 testdata/file2000.src:2 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
+NN2_0 [label = "1.56MB" id="NN2_0" fontsize=8 shape=box3d tooltip="62.50MB"]
 N2 -> NN2_0 [label=" 62.50MB" weight=100 tooltip="62.50MB" labeltooltip="62.50MB"]
-N3 [label="line1000\nfile1000.src:1\n4.88MB (4.95%)" fontsize=13 shape=box tooltip="line1000 testdata/file1000.src:1 (4.88MB)" color="#b2a086" fillcolor="#edeae7"]
-NN3_0 [label = "200kB" fontsize=8 shape=box3d tooltip="3.91MB"]
+N3 [label="line1000\nfile1000.src:1\n4.88MB (4.95%)" id="node3" fontsize=13 shape=box tooltip="line1000 testdata/file1000.src:1 (4.88MB)" color="#b2a086" fillcolor="#edeae7"]
+NN3_0 [label = "200kB" id="NN3_0" fontsize=8 shape=box3d tooltip="3.91MB"]
 N3 -> NN3_0 [label=" 3.91MB" weight=100 tooltip="3.91MB" labeltooltip="3.91MB"]
-N4 [label="line3002\nfile3000.src:3\n0 of 63.48MB (64.36%)" fontsize=8 shape=box tooltip="line3002 testdata/file3000.src:3 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
-N5 [label="line3001\nfile3000.src:2\n0 of 4.88MB (4.95%)" fontsize=8 shape=box tooltip="line3001 testdata/file3000.src:2 (4.88MB)" color="#b2a086" fillcolor="#edeae7"]
-N6 [label="line2000\nfile2000.src:3\n0 of 63.48MB (64.36%)" fontsize=8 shape=box tooltip="line2000 testdata/file2000.src:3 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
+N4 [label="line3002\nfile3000.src:3\n0 of 63.48MB (64.36%)" id="node4" fontsize=8 shape=box tooltip="line3002 testdata/file3000.src:3 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
+N5 [label="line3001\nfile3000.src:2\n0 of 4.88MB (4.95%)" id="node5" fontsize=8 shape=box tooltip="line3001 testdata/file3000.src:2 (4.88MB)" color="#b2a086" fillcolor="#edeae7"]
+N6 [label="line2000\nfile2000.src:3\n0 of 63.48MB (64.36%)" id="node6" fontsize=8 shape=box tooltip="line2000 testdata/file2000.src:3 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
 N6 -> N2 [label=" 63.48MB\n (inline)" weight=65 penwidth=4 color="#b21600" tooltip="line2000 testdata/file2000.src:3 -> line2001 testdata/file2000.src:2 (63.48MB)" labeltooltip="line2000 testdata/file2000.src:3 -> line2001 testdata/file2000.src:2 (63.48MB)"]
 N4 -> N6 [label=" 63.48MB" weight=65 penwidth=4 color="#b21600" tooltip="line3002 testdata/file3000.src:3 -> line2000 testdata/file2000.src:3 (63.48MB)" labeltooltip="line3002 testdata/file3000.src:3 -> line2000 testdata/file2000.src:3 (63.48MB)"]
 N1 -> N4 [label=" 62.50MB\n (inline)" weight=64 penwidth=4 color="#b21600" tooltip="line3000 testdata/file3000.src:4 -> line3002 testdata/file3000.src:3 (62.50MB)" labeltooltip="line3000 testdata/file3000.src:4 -> line3002 testdata/file3000.src:3 (62.50MB)"]
index 7a6f0a78f96a0dfa50f18b765ea9590bebb3bf42..630e452a9f0ccb235199f31581c532510b272e62 100644 (file)
@@ -1,6 +1,6 @@
-bytes: Total 150
-        80 (53.33%): 400kB
-        40 (26.67%): 1.56MB
-        20 (13.33%): 200kB
-        10 ( 6.67%): 100kB
+ bytes: Total 98.6MB
+          62.5MB (63.37%): 1.56MB
+          31.2MB (31.68%): 400kB
+           3.9MB ( 3.96%): 200kB
+        1000.0kB ( 0.99%): 100kB
 
index 7238b367101286d0694dfc0a2684f13202781e3f..5e565fc019e8f4ef552305ab906c9792c0f393df 100644 (file)
@@ -1,6 +1,6 @@
-bytes: Total 150
-        80 (53.33%): 409600B
-        40 (26.67%): 1638400B
-        20 (13.33%): 204800B
-        10 ( 6.67%): 102400B
+ bytes: Total 103424000.0B
+        65536000.0B (63.37%): 1638400B
+        32768000.0B (31.68%): 409600B
+         4096000.0B ( 3.96%): 204800B
+         1024000.0B ( 0.99%): 102400B
 
index bc061ad733579e657b44b7a9c50e5e420882bc4c..929461a3c1ef457dfef8a73db426a7713bb9e173 100644 (file)
@@ -1,8 +1,8 @@
 Showing nodes accounting for 150, 100% of 150 total
       flat  flat%   sum%        cum   cum%
-        80 53.33% 53.33%        130 86.67%  line3002 testdata/file3000.src (inline)
-        40 26.67% 80.00%         50 33.33%  line2001 testdata/file2000.src (inline)
-        30 20.00%   100%         30 20.00%  line1000 testdata/file1000.src
-         0     0%   100%         50 33.33%  line2000 testdata/file2000.src
-         0     0%   100%        150   100%  line3000 testdata/file3000.src
-         0     0%   100%        110 73.33%  line3001 testdata/file3000.src (inline)
+        80 53.33% 53.33%        130 86.67%  line3002 (inline)
+        40 26.67% 80.00%         50 33.33%  line2001 (inline)
+        30 20.00%   100%         30 20.00%  line1000
+         0     0%   100%         50 33.33%  line2000
+         0     0%   100%        150   100%  line3000
+         0     0%   100%        110 73.33%  line3001 (inline)
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_alloc.flat.alloc_space.dot b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_alloc.flat.alloc_space.dot
new file mode 100644 (file)
index 0000000..f0621a0
--- /dev/null
@@ -0,0 +1,14 @@
+digraph "unnamed" {
+node [style=filled fillcolor="#f8f8f8"]
+subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: alloc_space\lActive filters:\l   tagshow=[2]00\lShowing nodes accounting for 93.75MB, 95.05% of 98.63MB total\lDropped 1 node (cum <= 4.93MB)\l"] }
+N1 [label="line3002\n31.25MB (31.68%)\nof 94.73MB (96.04%)" id="node1" fontsize=20 shape=box tooltip="line3002 (94.73MB)" color="#b20200" fillcolor="#edd5d5"]
+N2 [label="line3000\n0 of 98.63MB (100%)" id="node2" fontsize=8 shape=box tooltip="line3000 (98.63MB)" color="#b20000" fillcolor="#edd5d5"]
+N3 [label="line2001\n62.50MB (63.37%)\nof 63.48MB (64.36%)" id="node3" fontsize=24 shape=box tooltip="line2001 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
+N4 [label="line2000\n0 of 63.48MB (64.36%)" id="node4" fontsize=8 shape=box tooltip="line2000 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
+N5 [label="line3001\n0 of 36.13MB (36.63%)" id="node5" fontsize=8 shape=box tooltip="line3001 (36.13MB)" color="#b22e00" fillcolor="#eddbd5"]
+N4 -> N3 [label=" 63.48MB\n (inline)" weight=65 penwidth=4 color="#b21600" tooltip="line2000 -> line2001 (63.48MB)" labeltooltip="line2000 -> line2001 (63.48MB)"]
+N1 -> N4 [label=" 63.48MB" weight=65 penwidth=4 color="#b21600" tooltip="line3002 -> line2000 (63.48MB)" labeltooltip="line3002 -> line2000 (63.48MB)"]
+N2 -> N1 [label=" 62.50MB\n (inline)" weight=64 penwidth=4 color="#b21600" tooltip="line3000 -> line3002 (62.50MB)" labeltooltip="line3000 -> line3002 (62.50MB)"]
+N2 -> N5 [label=" 36.13MB\n (inline)" weight=37 penwidth=2 color="#b22e00" tooltip="line3000 -> line3001 (36.13MB)" labeltooltip="line3000 -> line3001 (36.13MB)"]
+N5 -> N1 [label=" 32.23MB\n (inline)" weight=33 penwidth=2 color="#b23200" tooltip="line3001 -> line3002 (32.23MB)" labeltooltip="line3001 -> line3002 (32.23MB)"]
+}
index c693ef3478447d2b76d85a431bad128eab05764f..e412ff4813fd9b93254fbcc3b4a79806c2208430 100644 (file)
@@ -1,18 +1,18 @@
 digraph "unnamed" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: alloc_space\lShowing nodes accounting for 93.75MB, 95.05% of 98.63MB total\lDropped 1 node (cum <= 4.93MB)\l"] }
-N1 [label="line3002\nfile3000.src\n31.25MB (31.68%)\nof 94.73MB (96.04%)" fontsize=20 shape=box tooltip="line3002 testdata/file3000.src (94.73MB)" color="#b20200" fillcolor="#edd5d5"]
-NN1_0 [label = "400kB" fontsize=8 shape=box3d tooltip="31.25MB"]
+subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: alloc_space\lActive filters:\l   focus=[234]00\lShowing nodes accounting for 93.75MB, 95.05% of 98.63MB total\lDropped 1 node (cum <= 4.93MB)\l"] }
+N1 [label="line3002\n31.25MB (31.68%)\nof 94.73MB (96.04%)" id="node1" fontsize=20 shape=box tooltip="line3002 (94.73MB)" color="#b20200" fillcolor="#edd5d5"]
+NN1_0 [label = "400kB" id="NN1_0" fontsize=8 shape=box3d tooltip="31.25MB"]
 N1 -> NN1_0 [label=" 31.25MB" weight=100 tooltip="31.25MB" labeltooltip="31.25MB"]
-N2 [label="line3000\nfile3000.src\n0 of 98.63MB (100%)" fontsize=8 shape=box tooltip="line3000 testdata/file3000.src (98.63MB)" color="#b20000" fillcolor="#edd5d5"]
-N3 [label="line2001\nfile2000.src\n62.50MB (63.37%)\nof 63.48MB (64.36%)" fontsize=24 shape=box tooltip="line2001 testdata/file2000.src (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
-NN3_0 [label = "1.56MB" fontsize=8 shape=box3d tooltip="62.50MB"]
+N2 [label="line3000\n0 of 98.63MB (100%)" id="node2" fontsize=8 shape=box tooltip="line3000 (98.63MB)" color="#b20000" fillcolor="#edd5d5"]
+N3 [label="line2001\n62.50MB (63.37%)\nof 63.48MB (64.36%)" id="node3" fontsize=24 shape=box tooltip="line2001 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
+NN3_0 [label = "1.56MB" id="NN3_0" fontsize=8 shape=box3d tooltip="62.50MB"]
 N3 -> NN3_0 [label=" 62.50MB" weight=100 tooltip="62.50MB" labeltooltip="62.50MB"]
-N4 [label="line2000\nfile2000.src\n0 of 63.48MB (64.36%)" fontsize=8 shape=box tooltip="line2000 testdata/file2000.src (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
-N5 [label="line3001\nfile3000.src\n0 of 36.13MB (36.63%)" fontsize=8 shape=box tooltip="line3001 testdata/file3000.src (36.13MB)" color="#b22e00" fillcolor="#eddbd5"]
-N4 -> N3 [label=" 63.48MB\n (inline)" weight=65 penwidth=4 color="#b21600" tooltip="line2000 testdata/file2000.src -> line2001 testdata/file2000.src (63.48MB)" labeltooltip="line2000 testdata/file2000.src -> line2001 testdata/file2000.src (63.48MB)"]
-N1 -> N4 [label=" 63.48MB" weight=65 penwidth=4 color="#b21600" tooltip="line3002 testdata/file3000.src -> line2000 testdata/file2000.src (63.48MB)" labeltooltip="line3002 testdata/file3000.src -> line2000 testdata/file2000.src (63.48MB)" minlen=2]
-N2 -> N1 [label=" 62.50MB\n (inline)" weight=64 penwidth=4 color="#b21600" tooltip="line3000 testdata/file3000.src -> line3002 testdata/file3000.src (62.50MB)" labeltooltip="line3000 testdata/file3000.src -> line3002 testdata/file3000.src (62.50MB)"]
-N2 -> N5 [label=" 36.13MB\n (inline)" weight=37 penwidth=2 color="#b22e00" tooltip="line3000 testdata/file3000.src -> line3001 testdata/file3000.src (36.13MB)" labeltooltip="line3000 testdata/file3000.src -> line3001 testdata/file3000.src (36.13MB)"]
-N5 -> N1 [label=" 32.23MB\n (inline)" weight=33 penwidth=2 color="#b23200" tooltip="line3001 testdata/file3000.src -> line3002 testdata/file3000.src (32.23MB)" labeltooltip="line3001 testdata/file3000.src -> line3002 testdata/file3000.src (32.23MB)"]
+N4 [label="line2000\n0 of 63.48MB (64.36%)" id="node4" fontsize=8 shape=box tooltip="line2000 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
+N5 [label="line3001\n0 of 36.13MB (36.63%)" id="node5" fontsize=8 shape=box tooltip="line3001 (36.13MB)" color="#b22e00" fillcolor="#eddbd5"]
+N4 -> N3 [label=" 63.48MB\n (inline)" weight=65 penwidth=4 color="#b21600" tooltip="line2000 -> line2001 (63.48MB)" labeltooltip="line2000 -> line2001 (63.48MB)"]
+N1 -> N4 [label=" 63.48MB" weight=65 penwidth=4 color="#b21600" tooltip="line3002 -> line2000 (63.48MB)" labeltooltip="line3002 -> line2000 (63.48MB)" minlen=2]
+N2 -> N1 [label=" 62.50MB\n (inline)" weight=64 penwidth=4 color="#b21600" tooltip="line3000 -> line3002 (62.50MB)" labeltooltip="line3000 -> line3002 (62.50MB)"]
+N2 -> N5 [label=" 36.13MB\n (inline)" weight=37 penwidth=2 color="#b22e00" tooltip="line3000 -> line3001 (36.13MB)" labeltooltip="line3000 -> line3001 (36.13MB)"]
+N5 -> N1 [label=" 32.23MB\n (inline)" weight=33 penwidth=2 color="#b23200" tooltip="line3001 -> line3002 (32.23MB)" labeltooltip="line3001 -> line3002 (32.23MB)"]
 }
index 26a51c57a08e679366b7c04f6509d3c7846259ae..6110b114b9f735e26fdbb846ba84c3b15825ea19 100644 (file)
@@ -1,11 +1,11 @@
 digraph "unnamed" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: alloc_space\lShowing nodes accounting for 93.75MB, 95.05% of 98.63MB total\lDropped 1 node (cum <= 4.93MB)\l"] }
-N1 [label="line3000\nfile3000.src\n62.50MB (63.37%)\nof 98.63MB (100%)" fontsize=24 shape=box tooltip="line3000 testdata/file3000.src (98.63MB)" color="#b20000" fillcolor="#edd5d5"]
-NN1_0 [label = "1.56MB" fontsize=8 shape=box3d tooltip="62.50MB"]
+subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: alloc_space\lActive filters:\l   hide=line.*1?23?\lShowing nodes accounting for 93.75MB, 95.05% of 98.63MB total\lDropped 1 node (cum <= 4.93MB)\l"] }
+N1 [label="line3000\n62.50MB (63.37%)\nof 98.63MB (100%)" id="node1" fontsize=24 shape=box tooltip="line3000 (98.63MB)" color="#b20000" fillcolor="#edd5d5"]
+NN1_0 [label = "1.56MB" id="NN1_0" fontsize=8 shape=box3d tooltip="62.50MB"]
 N1 -> NN1_0 [label=" 62.50MB" weight=100 tooltip="62.50MB" labeltooltip="62.50MB"]
-N2 [label="line3001\nfile3000.src\n31.25MB (31.68%)\nof 36.13MB (36.63%)" fontsize=20 shape=box tooltip="line3001 testdata/file3000.src (36.13MB)" color="#b22e00" fillcolor="#eddbd5"]
-NN2_0 [label = "400kB" fontsize=8 shape=box3d tooltip="31.25MB"]
+N2 [label="line3001\n31.25MB (31.68%)\nof 36.13MB (36.63%)" id="node2" fontsize=20 shape=box tooltip="line3001 (36.13MB)" color="#b22e00" fillcolor="#eddbd5"]
+NN2_0 [label = "400kB" id="NN2_0" fontsize=8 shape=box3d tooltip="31.25MB"]
 N2 -> NN2_0 [label=" 31.25MB" weight=100 tooltip="31.25MB" labeltooltip="31.25MB"]
-N1 -> N2 [label=" 36.13MB\n (inline)" weight=37 penwidth=2 color="#b22e00" tooltip="line3000 testdata/file3000.src -> line3001 testdata/file3000.src (36.13MB)" labeltooltip="line3000 testdata/file3000.src -> line3001 testdata/file3000.src (36.13MB)" minlen=2]
+N1 -> N2 [label=" 36.13MB\n (inline)" weight=37 penwidth=2 color="#b22e00" tooltip="line3000 -> line3001 (36.13MB)" labeltooltip="line3000 -> line3001 (36.13MB)" minlen=2]
 }
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_request.tags.focus b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_request.tags.focus
new file mode 100644 (file)
index 0000000..b1a5f44
--- /dev/null
@@ -0,0 +1,8 @@
+ bytes: Total 93.8MB
+        62.5MB (66.67%): 1.56MB
+        31.2MB (33.33%): 400kB
+
+ request: Total 93.8MB
+          62.5MB (66.67%): 1.56MB
+          31.2MB (33.33%): 400kB
+
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_sizetags.dot b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_sizetags.dot
new file mode 100644 (file)
index 0000000..6be6112
--- /dev/null
@@ -0,0 +1,30 @@
+digraph "unnamed" {
+node [style=filled fillcolor="#f8f8f8"]
+subgraph cluster_L { "Build ID: buildid" [shape=box fontsize=16 label="Build ID: buildid\lcomment\lType: inuse_space\lShowing nodes accounting for 93.75MB, 95.05% of 98.63MB total\lDropped 1 node (cum <= 4.93MB)\l"] }
+N1 [label="line3002\n31.25MB (31.68%)\nof 94.73MB (96.04%)" id="node1" fontsize=20 shape=box tooltip="line3002 (94.73MB)" color="#b20200" fillcolor="#edd5d5"]
+NN1_0 [label = "16B..64B" id="NN1_0" fontsize=8 shape=box3d tooltip="93.75MB"]
+N1 -> NN1_0 [label=" 93.75MB" weight=100 tooltip="93.75MB" labeltooltip="93.75MB"]
+NN1_1 [label = "2B..8B" id="NN1_1" fontsize=8 shape=box3d tooltip="93.75MB"]
+N1 -> NN1_1 [label=" 93.75MB" weight=100 tooltip="93.75MB" labeltooltip="93.75MB"]
+NN1_2 [label = "256B..1.56MB" id="NN1_2" fontsize=8 shape=box3d tooltip="62.50MB"]
+N1 -> NN1_2 [label=" 62.50MB" weight=100 tooltip="62.50MB" labeltooltip="62.50MB"]
+NN1_3 [label = "128B" id="NN1_3" fontsize=8 shape=box3d tooltip="31.25MB"]
+N1 -> NN1_3 [label=" 31.25MB" weight=100 tooltip="31.25MB" labeltooltip="31.25MB"]
+N2 [label="line3000\n0 of 98.63MB (100%)" id="node2" fontsize=8 shape=box tooltip="line3000 (98.63MB)" color="#b20000" fillcolor="#edd5d5"]
+N3 [label="line2001\n62.50MB (63.37%)\nof 63.48MB (64.36%)" id="node3" fontsize=24 shape=box tooltip="line2001 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
+NN3_0 [label = "16B..64B" id="NN3_0" fontsize=8 shape=box3d tooltip="190.43MB"]
+N3 -> NN3_0 [label=" 190.43MB" weight=100 tooltip="190.43MB" labeltooltip="190.43MB" style="dotted"]
+NN3_1 [label = "2B..8B" id="NN3_1" fontsize=8 shape=box3d tooltip="190.43MB"]
+N3 -> NN3_1 [label=" 190.43MB" weight=100 tooltip="190.43MB" labeltooltip="190.43MB" style="dotted"]
+NN3_2 [label = "256B..1.56MB" id="NN3_2" fontsize=8 shape=box3d tooltip="125.98MB"]
+N3 -> NN3_2 [label=" 125.98MB" weight=100 tooltip="125.98MB" labeltooltip="125.98MB" style="dotted"]
+NN3_3 [label = "128B" id="NN3_3" fontsize=8 shape=box3d tooltip="63.48MB"]
+N3 -> NN3_3 [label=" 63.48MB" weight=100 tooltip="63.48MB" labeltooltip="63.48MB" style="dotted"]
+N4 [label="line2000\n0 of 63.48MB (64.36%)" id="node4" fontsize=8 shape=box tooltip="line2000 (63.48MB)" color="#b21600" fillcolor="#edd8d5"]
+N5 [label="line3001\n0 of 36.13MB (36.63%)" id="node5" fontsize=8 shape=box tooltip="line3001 (36.13MB)" color="#b22e00" fillcolor="#eddbd5"]
+N4 -> N3 [label=" 63.48MB\n (inline)" weight=65 penwidth=4 color="#b21600" tooltip="line2000 -> line2001 (63.48MB)" labeltooltip="line2000 -> line2001 (63.48MB)"]
+N1 -> N4 [label=" 63.48MB" weight=65 penwidth=4 color="#b21600" tooltip="line3002 -> line2000 (63.48MB)" labeltooltip="line3002 -> line2000 (63.48MB)" minlen=2]
+N2 -> N1 [label=" 62.50MB\n (inline)" weight=64 penwidth=4 color="#b21600" tooltip="line3000 -> line3002 (62.50MB)" labeltooltip="line3000 -> line3002 (62.50MB)"]
+N2 -> N5 [label=" 36.13MB\n (inline)" weight=37 penwidth=2 color="#b22e00" tooltip="line3000 -> line3001 (36.13MB)" labeltooltip="line3000 -> line3001 (36.13MB)"]
+N5 -> N1 [label=" 32.23MB\n (inline)" weight=33 penwidth=2 color="#b23200" tooltip="line3001 -> line3002 (32.23MB)" labeltooltip="line3001 -> line3002 (32.23MB)"]
+}
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_tags.traces b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.heap_tags.traces
new file mode 100644 (file)
index 0000000..547aea7
--- /dev/null
@@ -0,0 +1,32 @@
+Build ID: buildid
+comment
+Type: inuse_space
+-----------+-------------------------------------------------------
+      key1:  tag
+     bytes:  100kB
+   request:  100kB
+    1000kB   line1000
+             line2001
+             line2000
+             line3002
+             line3001
+             line3000
+-----------+-------------------------------------------------------
+     bytes:  200kB
+    3.91MB   line1000
+             line3001
+             line3000
+-----------+-------------------------------------------------------
+      key1:  tag
+     bytes:  1.56MB
+   request:  1.56MB
+   62.50MB   line2001
+             line2000
+             line3002
+             line3000
+-----------+-------------------------------------------------------
+     bytes:  400kB
+   31.25MB   line3002
+             line3001
+             line3000
+-----------+-------------------------------------------------------
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.unknown.flat.functions.call_tree.text b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.unknown.flat.functions.call_tree.text
new file mode 100644 (file)
index 0000000..78a2298
--- /dev/null
@@ -0,0 +1,8 @@
+Showing nodes accounting for 1.12s, 100% of 1.12s total
+Showing top 5 nodes out of 6
+      flat  flat%   sum%        cum   cum%
+     1.10s 98.21% 98.21%      1.10s 98.21%  line1000
+     0.01s  0.89% 99.11%      1.01s 90.18%  line2001 (inline)
+     0.01s  0.89%   100%      1.02s 91.07%  line3002 (inline)
+         0     0%   100%      1.01s 90.18%  line2000
+         0     0%   100%      1.12s   100%  line3000
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.unknown.flat.functions.text b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.unknown.flat.functions.text
deleted file mode 100644 (file)
index 0807ed2..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Showing nodes accounting for 1.12s, 100% of 1.12s total
-      flat  flat%   sum%        cum   cum%
-     1.10s 98.21% 98.21%      1.10s 98.21%  line1000 testdata/file1000.src
-     0.01s  0.89% 99.11%      1.01s 90.18%  line2001 testdata/file2000.src (inline)
-     0.01s  0.89%   100%      1.02s 91.07%  line3002 testdata/file3000.src (inline)
-         0     0%   100%      1.01s 90.18%  line2000 testdata/file2000.src
-         0     0%   100%      1.12s   100%  line3000 testdata/file3000.src
-         0     0%   100%      1.11s 99.11%  line3001 testdata/file3000.src (inline)
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
new file mode 100644 (file)
index 0000000..48f0fa1
--- /dev/null
@@ -0,0 +1,965 @@
+// Copyright 2017 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 "html/template"
+
+// addTemplates adds a set of template definitions to templates.
+func addTemplates(templates *template.Template) {
+       template.Must(templates.Parse(`
+{{define "css"}}
+<style type="text/css">
+html {
+  height: 100%;
+  min-height: 100%;
+  margin: 0px;
+}
+body {
+  margin: 0px;
+  width: 100%;
+  height: 100%;
+  min-height: 100%;
+  overflow: hidden;
+}
+#graphcontainer {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  min-height: 100%;
+  width: 100%;
+  min-width: 100%;
+  margin: 0px;
+}
+#graph {
+  flex: 1 1 auto;
+  overflow: hidden;
+}
+svg {
+  width: 100%;
+  height: auto;
+}
+button {
+  margin-top: 5px;
+  margin-bottom: 5px;
+}
+#detailtext {
+  display: none;
+  position: fixed;
+  top: 20px;
+  right: 10px;
+  background-color: #ffffff;
+  min-width: 160px;
+  border: 1px solid #888;
+  box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2);
+  z-index: 1;
+}
+#closedetails {
+  float: right;
+  margin: 2px;
+}
+#home {
+  font-size: 14pt;
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+  float: right;
+}
+.menubar {
+  display: inline-block;
+  background-color: #f8f8f8;
+  border: 1px solid #ccc;
+  width: 100%;
+}
+.menu-header {
+  position: relative;
+  display: inline-block;
+  padding: 2px 2px;
+  font-size: 14pt;
+}
+.menu {
+  display: none;
+  position: absolute;
+  background-color: #f8f8f8;
+  border: 1px solid #888;
+  box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2);
+  z-index: 1;
+  margin-top: 2px;
+  left: 0px;
+  min-width: 5em;
+}
+.menu-header, .menu {
+  cursor: default;
+  user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  -webkit-user-select: none;
+}
+.menu hr {
+  background-color: #fff;
+  margin-top: 0px;
+  margin-bottom: 0px;
+}
+.menu a, .menu button {
+  display: block;
+  width: 100%;
+  margin: 0px;
+  padding: 2px 0px 2px 0px;
+  text-align: left;
+  text-decoration: none;
+  color: #000;
+  background-color: #f8f8f8;
+  font-size: 12pt;
+  border: none;
+}
+.menu-header:hover {
+  background-color: #ccc;
+}
+.menu a:hover, .menu button:hover {
+  background-color: #ccc;
+}
+.menu a.disabled {
+  color: gray;
+  pointer-events: none;
+}
+#searchbox {
+  margin-left: 10pt;
+}
+#bodycontainer {
+  width: 100%;
+  height: 100%;
+  max-height: 100%;
+  overflow: scroll;
+  padding-top: 5px;
+}
+#toptable {
+  border-spacing: 0px;
+  width: 100%;
+  padding-bottom: 1em;
+}
+#toptable tr th {
+  border-bottom: 1px solid black;
+  text-align: right;
+  padding-left: 1em;
+  padding-top: 0.2em;
+  padding-bottom: 0.2em;
+}
+#toptable tr td {
+  padding-left: 1em;
+  font: monospace;
+  text-align: right;
+  white-space: nowrap;
+  cursor: default;
+}
+#toptable tr th:nth-child(6),
+#toptable tr th:nth-child(7),
+#toptable tr td:nth-child(6),
+#toptable tr td:nth-child(7) {
+  text-align: left;
+}
+#toptable tr td:nth-child(6) {
+  max-width: 30em;  // Truncate very long names
+  overflow: hidden;
+}
+#flathdr1, #flathdr2, #cumhdr1, #cumhdr2, #namehdr {
+  cursor: ns-resize;
+}
+.hilite {
+  background-color: #ccf;
+}
+</style>
+{{end}}
+
+{{define "header"}}
+<div id="detailtext">
+<button id="closedetails">Close</button>
+{{range .Legend}}<div>{{.}}</div>{{end}}
+</div>
+
+<div class="menubar">
+
+<div class="menu-header">
+View
+<div class="menu">
+<a title="{{.Help.top}}"  href="/top" id="topbtn">Top</a>
+<a title="{{.Help.graph}}" href="/" id="graphbtn">Graph</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>
+<hr>
+<button title="{{.Help.details}}" id="details">Details</button>
+</div>
+</div>
+
+<div class="menu-header">
+Refine
+<div class="menu">
+<a title="{{.Help.focus}}" href="{{.BaseURL}}" id="focus">Focus</a>
+<a title="{{.Help.ignore}}" href="{{.BaseURL}}" id="ignore">Ignore</a>
+<a title="{{.Help.hide}}" href="{{.BaseURL}}" id="hide">Hide</a>
+<a title="{{.Help.show}}" href="{{.BaseURL}}" id="show">Show</a>
+<hr>
+<a title="{{.Help.reset}}" href="{{.BaseURL}}">Reset</a>
+</div>
+</div>
+
+<input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
+
+<span id="home">{{.Title}}</span>
+
+</div> <!-- menubar -->
+
+<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
+{{end}}
+
+{{define "graph" -}}
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>{{.Title}}</title>
+{{template "css" .}}
+</head>
+<body>
+
+{{template "header" .}}
+<div id="graphcontainer">
+<div id="graph">
+{{.HTMLBody}}
+</div>
+
+</div>
+{{template "script" .}}
+<script>viewer({{.BaseURL}}, {{.Nodes}})</script>
+</body>
+</html>
+{{end}}
+
+{{define "script"}}
+<script>
+// Make svg pannable and zoomable.
+// Call clickHandler(t) if a click event is caught by the pan event handlers.
+function initPanAndZoom(svg, clickHandler) {
+  'use strict';
+
+  // Current mouse/touch handling mode
+  const IDLE = 0
+  const MOUSEPAN = 1
+  const TOUCHPAN = 2
+  const TOUCHZOOM = 3
+  let mode = IDLE
+
+  // State needed to implement zooming.
+  let currentScale = 1.0
+  const initWidth = svg.viewBox.baseVal.width
+  const initHeight = svg.viewBox.baseVal.height
+
+  // State needed to implement panning.
+  let panLastX = 0      // Last event X coordinate
+  let panLastY = 0      // Last event Y coordinate
+  let moved = false     // Have we seen significant movement
+  let touchid = null    // Current touch identifier
+
+  // State needed for pinch zooming
+  let touchid2 = null     // Second id for pinch zooming
+  let initGap = 1.0       // Starting gap between two touches
+  let initScale = 1.0     // currentScale when pinch zoom started
+  let centerPoint = null  // Center point for scaling
+
+  // Convert event coordinates to svg coordinates.
+  function toSvg(x, y) {
+    const p = svg.createSVGPoint()
+    p.x = x
+    p.y = y
+    let m = svg.getCTM()
+    if (m == null) m = svg.getScreenCTM()  // Firefox workaround.
+    return p.matrixTransform(m.inverse())
+  }
+
+  // Change the scaling for the svg to s, keeping the point denoted
+  // by u (in svg coordinates]) fixed at the same screen location.
+  function rescale(s, u) {
+    // Limit to a good range.
+    if (s < 0.2) s = 0.2
+    if (s > 10.0) s = 10.0
+
+    currentScale = s
+
+    // svg.viewBox defines the visible portion of the user coordinate
+    // system.  So to magnify by s, divide the visible portion by s,
+    // which will then be stretched to fit the viewport.
+    const vb = svg.viewBox
+    const w1 = vb.baseVal.width
+    const w2 = initWidth / s
+    const h1 = vb.baseVal.height
+    const h2 = initHeight / s
+    vb.baseVal.width = w2
+    vb.baseVal.height = h2
+
+    // We also want to adjust vb.baseVal.x so that u.x remains at same
+    // screen X coordinate.  In other words, want to change it from x1 to x2
+    // so that:
+    //     (u.x - x1) / w1 = (u.x - x2) / w2
+    // Simplifying that, we get
+    //     (u.x - x1) * (w2 / w1) = u.x - x2
+    //     x2 = u.x - (u.x - x1) * (w2 / w1)
+    vb.baseVal.x = u.x - (u.x - vb.baseVal.x) * (w2 / w1)
+    vb.baseVal.y = u.y - (u.y - vb.baseVal.y) * (h2 / h1)
+  }
+
+  function handleWheel(e) {
+    if (e.deltaY == 0) return
+    // Change scale factor by 1.1 or 1/1.1
+    rescale(currentScale * (e.deltaY < 0 ? 1.1 : (1/1.1)),
+            toSvg(e.offsetX, e.offsetY))
+  }
+
+  function setMode(m) {
+    mode = m
+    touchid = null
+    touchid2 = null
+  }
+
+  function panStart(x, y) {
+    moved = false
+    panLastX = x
+    panLastY = y
+  }
+
+  function panMove(x, y) {
+    let dx = x - panLastX
+    let dy = y - panLastY
+    if (Math.abs(dx) <= 2 && Math.abs(dy) <= 2) return  // Ignore tiny moves
+
+    moved = true
+    panLastX = x
+    panLastY = y
+
+    // Firefox workaround: get dimensions from parentNode.
+    const swidth = svg.clientWidth || svg.parentNode.clientWidth
+    const sheight = svg.clientHeight || svg.parentNode.clientHeight
+
+    // Convert deltas from screen space to svg space.
+    dx *= (svg.viewBox.baseVal.width / swidth)
+    dy *= (svg.viewBox.baseVal.height / sheight)
+
+    svg.viewBox.baseVal.x -= dx
+    svg.viewBox.baseVal.y -= dy
+  }
+
+  function handleScanStart(e) {
+    if (e.button != 0) return  // Do not catch right-clicks etc.
+    setMode(MOUSEPAN)
+    panStart(e.clientX, e.clientY)
+    e.preventDefault()
+    svg.addEventListener("mousemove", handleScanMove)
+  }
+
+  function handleScanMove(e) {
+    if (e.buttons == 0) {
+      // Missed an end event, perhaps because mouse moved outside window.
+      setMode(IDLE)
+      svg.removeEventListener("mousemove", handleScanMove)
+      return
+    }
+    if (mode == MOUSEPAN) panMove(e.clientX, e.clientY)
+  }
+
+  function handleScanEnd(e) {
+    if (mode == MOUSEPAN) panMove(e.clientX, e.clientY)
+    setMode(IDLE)
+    svg.removeEventListener("mousemove", handleScanMove)
+    if (!moved) clickHandler(e.target)
+  }
+
+  // Find touch object with specified identifier.
+  function findTouch(tlist, id) {
+    for (const t of tlist) {
+      if (t.identifier == id) return t
+    }
+    return null
+  }
+
+ // Return distance between two touch points
+  function touchGap(t1, t2) {
+    const dx = t1.clientX - t2.clientX
+    const dy = t1.clientY - t2.clientY
+    return Math.hypot(dx, dy)
+  }
+
+  function handleTouchStart(e) {
+    if (mode == IDLE && e.changedTouches.length == 1) {
+      // Start touch based panning
+      const t = e.changedTouches[0]
+      setMode(TOUCHPAN)
+      touchid = t.identifier
+      panStart(t.clientX, t.clientY)
+      e.preventDefault()
+    } else if (mode == TOUCHPAN && e.touches.length == 2) {
+      // Start pinch zooming
+      setMode(TOUCHZOOM)
+      const t1 = e.touches[0]
+      const t2 = e.touches[1]
+      touchid = t1.identifier
+      touchid2 = t2.identifier
+      initScale = currentScale
+      initGap = touchGap(t1, t2)
+      centerPoint = toSvg((t1.clientX + t2.clientX) / 2,
+                          (t1.clientY + t2.clientY) / 2)
+      e.preventDefault()
+    }
+  }
+
+  function handleTouchMove(e) {
+    if (mode == TOUCHPAN) {
+      const t = findTouch(e.changedTouches, touchid)
+      if (t == null) return
+      if (e.touches.length != 1) {
+        setMode(IDLE)
+        return
+      }
+      panMove(t.clientX, t.clientY)
+      e.preventDefault()
+    } else if (mode == TOUCHZOOM) {
+      // Get two touches; new gap; rescale to ratio.
+      const t1 = findTouch(e.touches, touchid)
+      const t2 = findTouch(e.touches, touchid2)
+      if (t1 == null || t2 == null) return
+      const gap = touchGap(t1, t2)
+      rescale(initScale * gap / initGap, centerPoint)
+      e.preventDefault()
+    }
+  }
+
+  function handleTouchEnd(e) {
+    if (mode == TOUCHPAN) {
+      const t = findTouch(e.changedTouches, touchid)
+      if (t == null) return
+      panMove(t.clientX, t.clientY)
+      setMode(IDLE)
+      e.preventDefault()
+      if (!moved) clickHandler(t.target)
+    } else if (mode == TOUCHZOOM) {
+      setMode(IDLE)
+      e.preventDefault()
+    }
+  }
+
+  svg.addEventListener("mousedown", handleScanStart)
+  svg.addEventListener("mouseup", handleScanEnd)
+  svg.addEventListener("touchstart", handleTouchStart)
+  svg.addEventListener("touchmove", handleTouchMove)
+  svg.addEventListener("touchend", handleTouchEnd)
+  svg.addEventListener("wheel", handleWheel, true)
+}
+
+function initMenus() {
+  'use strict';
+
+  let activeMenu = null;
+  let activeMenuHdr = null;
+
+  function cancelActiveMenu() {
+    if (activeMenu == null) return;
+    activeMenu.style.display = "none";
+    activeMenu = null;
+    activeMenuHdr = null;
+  }
+
+  // Set click handlers on every menu header.
+  for (const menu of document.getElementsByClassName("menu")) {
+    const hdr = menu.parentElement;
+    if (hdr == null) return;
+    function showMenu(e) {
+      // menu is a child of hdr, so this event can fire for clicks
+      // inside menu. Ignore such clicks.
+      if (e.target != hdr) return;
+      activeMenu = menu;
+      activeMenuHdr = hdr;
+      menu.style.display = "block";
+    }
+    hdr.addEventListener("mousedown", showMenu);
+    hdr.addEventListener("touchstart", showMenu);
+  }
+
+  // If there is an active menu and a down event outside, retract the menu.
+  for (const t of ["mousedown", "touchstart"]) {
+    document.addEventListener(t, (e) => {
+      // Note: to avoid unnecessary flicker, if the down event is inside
+      // the active menu header, do not retract the menu.
+      if (activeMenuHdr != e.target.closest(".menu-header")) {
+        cancelActiveMenu();
+      }
+    }, { passive: true, capture: true });
+  }
+
+  // If there is an active menu and an up event inside, retract the menu.
+  document.addEventListener("mouseup", (e) => {
+    if (activeMenu == e.target.closest(".menu")) {
+      cancelActiveMenu();
+    }
+  }, { passive: true, capture: true });
+}
+
+function viewer(baseUrl, nodes) {
+  'use strict';
+
+  // Elements
+  const search = document.getElementById("searchbox")
+  const graph0 = document.getElementById("graph0")
+  const svg = (graph0 == null ? null : graph0.parentElement)
+  const toptable = document.getElementById("toptable")
+
+  let regexpActive = false
+  let selected = new Map()
+  let origFill = new Map()
+  let searchAlarm = null
+  let buttonsEnabled = true
+
+  function handleDetails() {
+    const detailsText = document.getElementById("detailtext")
+    if (detailsText != null) detailsText.style.display = "block"
+  }
+
+  function handleCloseDetails() {
+    const detailsText = document.getElementById("detailtext")
+    if (detailsText != null) detailsText.style.display = "none"
+  }
+
+  function handleKey(e) {
+    if (e.keyCode != 13) return
+    window.location.href =
+        updateUrl(new URL({{.BaseURL}}, window.location.href), "f")
+    e.preventDefault()
+  }
+
+  function handleSearch() {
+    // Delay expensive processing so a flurry of key strokes is handled once.
+    if (searchAlarm != null) {
+      clearTimeout(searchAlarm)
+    }
+    searchAlarm = setTimeout(selectMatching, 300)
+
+    regexpActive = true
+    updateButtons()
+  }
+
+  function selectMatching() {
+    searchAlarm = null
+    let re = null
+    if (search.value != "") {
+      try {
+        re = new RegExp(search.value)
+      } catch (e) {
+        // TODO: Display error state in search box
+        return
+      }
+    }
+
+    function match(text) {
+      return re != null && re.test(text)
+    }
+
+    // drop currently selected items that do not match re.
+    selected.forEach(function(v, n) {
+      if (!match(nodes[n])) {
+        unselect(n, document.getElementById("node" + n))
+      }
+    })
+
+    // add matching items that are not currently selected.
+    for (let n = 0; n < nodes.length; n++) {
+      if (!selected.has(n) && match(nodes[n])) {
+        select(n, document.getElementById("node" + n))
+      }
+    }
+
+    updateButtons()
+  }
+
+  function toggleSvgSelect(elem) {
+    // Walk up to immediate child of graph0
+    while (elem != null && elem.parentElement != graph0) {
+      elem = elem.parentElement
+    }
+    if (!elem) return
+
+    // Disable regexp mode.
+    regexpActive = false
+
+    const n = nodeId(elem)
+    if (n < 0) return
+    if (selected.has(n)) {
+      unselect(n, elem)
+    } else {
+      select(n, elem)
+    }
+    updateButtons()
+  }
+
+  function unselect(n, elem) {
+    if (elem == null) return
+    selected.delete(n)
+    setBackground(elem, false)
+  }
+
+  function select(n, elem) {
+    if (elem == null) return
+    selected.set(n, true)
+    setBackground(elem, true)
+  }
+
+  function nodeId(elem) {
+    const id = elem.id
+    if (!id) return -1
+    if (!id.startsWith("node")) return -1
+    const n = parseInt(id.slice(4), 10)
+    if (isNaN(n)) return -1
+    if (n < 0 || n >= nodes.length) return -1
+    return n
+  }
+
+  function setBackground(elem, set) {
+    // Handle table row highlighting.
+    if (elem.nodeName == "TR") {
+      elem.classList.toggle("hilite", set)
+      return
+    }
+
+    // Handle svg element highlighting.
+    const p = findPolygon(elem)
+    if (p != null) {
+      if (set) {
+        origFill.set(p, p.style.fill)
+        p.style.fill = "#ccccff"
+      } else if (origFill.has(p)) {
+        p.style.fill = origFill.get(p)
+      }
+    }
+  }
+
+  function findPolygon(elem) {
+    if (elem.localName == "polygon") return elem
+    for (const c of elem.children) {
+      const p = findPolygon(c)
+      if (p != null) return p
+    }
+    return null
+  }
+
+  // convert a string to a regexp that matches that string.
+  function quotemeta(str) {
+    return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1')
+  }
+
+  // Update id's href to reflect current selection whenever it is
+  // liable to be followed.
+  function makeLinkDynamic(id) {
+    const elem = document.getElementById(id)
+    if (elem == null) return
+
+    // Most links copy current selection into the "f" parameter,
+    // but Refine menu links are different.
+    let param = "f"
+    if (id == "ignore") param = "i"
+    if (id == "hide") param = "h"
+    if (id == "show") param = "s"
+
+    // We update on mouseenter so middle-click/right-click work properly.
+    elem.addEventListener("mouseenter", updater)
+    elem.addEventListener("touchstart", updater)
+
+    function updater() {
+      elem.href = updateUrl(new URL(elem.href), param)
+    }
+  }
+
+  // Update URL to reflect current selection.
+  function updateUrl(url, param) {
+    url.hash = ""
+
+    // 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("|")
+
+    // 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])
+    }
+
+    if (re != "") {
+      // For focus/show, forget old parameter.  For others, add to re.
+      if (param != "f" && param != "s" && params.has(param)) {
+        const old = params.get(param)
+         if (old != "") {
+          re += "|" + old
+        }
+      }
+      params.set(param, re)
+    } else {
+      params.delete(param)
+    }
+
+    return url.toString()
+  }
+
+  function handleTopClick(e) {
+    // Walk back until we find TR and then get the Name column (index 5)
+    let elem = e.target
+    while (elem != null && elem.nodeName != "TR") {
+      elem = elem.parentElement
+    }
+    if (elem == null || elem.children.length < 6) return
+
+    e.preventDefault()
+    const tr = elem
+    const td = elem.children[5]
+    if (td.nodeName != "TD") return
+    const name = td.innerText
+    const index = nodes.indexOf(name)
+    if (index < 0) return
+
+    // Disable regexp mode.
+    regexpActive = false
+
+    if (selected.has(index)) {
+      unselect(index, elem)
+    } else {
+      select(index, elem)
+    }
+    updateButtons()
+  }
+
+  function updateButtons() {
+    const enable = (search.value != "" || selected.size != 0)
+    if (buttonsEnabled == enable) return
+    buttonsEnabled = enable
+    for (const id of ["focus", "ignore", "hide", "show"]) {
+      const link = document.getElementById(id)
+      if (link != null) {
+        link.classList.toggle("disabled", !enable)
+      }
+    }
+  }
+
+  // Initialize button states
+  updateButtons()
+
+  // Setup event handlers
+  initMenus()
+  if (svg != null) {
+    initPanAndZoom(svg, toggleSvgSelect)
+  }
+  if (toptable != null) {
+    toptable.addEventListener("mousedown", handleTopClick)
+    toptable.addEventListener("touchstart", handleTopClick)
+  }
+
+  const ids = ["topbtn", "graphbtn", "peek", "list", "disasm",
+               "focus", "ignore", "hide", "show"]
+  ids.forEach(makeLinkDynamic)
+
+  // Bind action to button with specified id.
+  function addAction(id, action) {
+    const btn = document.getElementById(id)
+    if (btn != null) {
+      btn.addEventListener("click", action)
+      btn.addEventListener("touchstart", action)
+    }
+  }
+
+  addAction("details", handleDetails)
+  addAction("closedetails", handleCloseDetails)
+
+  search.addEventListener("input", handleSearch)
+  search.addEventListener("keydown", handleKey)
+
+  // Give initial focus to main container so it can be scrolled using keys.
+  const main = document.getElementById("bodycontainer")
+  if (main) {
+    main.focus()
+  }
+}
+</script>
+{{end}}
+
+{{define "top" -}}
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>{{.Title}}</title>
+{{template "css" .}}
+<style type="text/css">
+</style>
+</head>
+<body>
+
+{{template "header" .}}
+
+<div id="bodycontainer">
+<table id="toptable">
+<tr>
+<th id="flathdr1">Flat
+<th id="flathdr2">Flat%
+<th>Sum%
+<th id="cumhdr1">Cum
+<th id="cumhdr2">Cum%
+<th id="namehdr">Name
+<th>Inlined?</tr>
+<tbody id="rows">
+</tbody>
+</table>
+</div>
+
+{{template "script" .}}
+<script>
+function makeTopTable(total, entries) {
+  const rows = document.getElementById("rows")
+  if (rows == null) return
+
+  // Store initial index in each entry so we have stable node ids for selection.
+  for (let i = 0; i < entries.length; i++) {
+    entries[i].Id = "node" + i
+  }
+
+  // Which column are we currently sorted by and in what order?
+  let currentColumn = ""
+  let descending = false
+  sortBy("Flat")
+
+  function sortBy(column) {
+    // Update sort criteria
+    if (column == currentColumn) {
+      descending = !descending  // Reverse order
+    } else {
+      currentColumn = column
+      descending = (column != "Name")
+    }
+
+    // Sort according to current criteria.
+    function cmp(a, b) {
+      const av = a[currentColumn]
+      const bv = b[currentColumn]
+      if (av < bv) return -1
+      if (av > bv) return +1
+      return 0
+    }
+    entries.sort(cmp)
+    if (descending) entries.reverse()
+
+    function addCell(tr, val) {
+      const td = document.createElement('td')
+      td.textContent = val
+      tr.appendChild(td)
+    }
+
+    function percent(v) {
+      return (v * 100.0 / total).toFixed(2) + "%"
+    }
+
+    // Generate rows
+    const fragment = document.createDocumentFragment()
+    let sum = 0
+    for (const row of entries) {
+      const tr = document.createElement('tr')
+      tr.id = row.Id
+      sum += row.Flat
+      addCell(tr, row.FlatFormat)
+      addCell(tr, percent(row.Flat))
+      addCell(tr, percent(sum))
+      addCell(tr, row.CumFormat)
+      addCell(tr, percent(row.Cum))
+      addCell(tr, row.Name)
+      addCell(tr, row.InlineLabel)
+      fragment.appendChild(tr)
+    }
+
+    rows.textContent = ''  // Remove old rows
+    rows.appendChild(fragment)
+  }
+
+  // Make different column headers trigger sorting.
+  function bindSort(id, column) {
+    const hdr = document.getElementById(id)
+    if (hdr == null) return
+    const fn = function() { sortBy(column) }
+    hdr.addEventListener("click", fn)
+    hdr.addEventListener("touch", fn)
+  }
+  bindSort("flathdr1", "Flat")
+  bindSort("flathdr2", "Flat")
+  bindSort("cumhdr1", "Cum")
+  bindSort("cumhdr2", "Cum")
+  bindSort("namehdr", "Name")
+}
+
+viewer({{.BaseURL}}, {{.Nodes}})
+makeTopTable({{.Total}}, {{.Top}})
+</script>
+</body>
+</html>
+{{end}}
+
+{{define "sourcelisting" -}}
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>{{.Title}}</title>
+{{template "css" .}}
+{{template "weblistcss" .}}
+{{template "weblistjs" .}}
+</head>
+<body>
+
+{{template "header" .}}
+
+<div id="bodycontainer">
+{{.HTMLBody}}
+</div>
+
+{{template "script" .}}
+<script>viewer({{.BaseURL}}, null)</script>
+</body>
+</html>
+{{end}}
+
+{{define "plaintext" -}}
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>{{.Title}}</title>
+{{template "css" .}}
+</head>
+<body>
+
+{{template "header" .}}
+
+<div id="bodycontainer">
+<pre>
+{{.TextBody}}
+</pre>
+</div>
+
+{{template "script" .}}
+<script>viewer({{.BaseURL}}, null)</script>
+</body>
+</html>
+{{end}}
+`))
+}
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
new file mode 100644 (file)
index 0000000..67ae262
--- /dev/null
@@ -0,0 +1,393 @@
+// Copyright 2017 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 (
+       "bytes"
+       "fmt"
+       "html/template"
+       "net"
+       "net/http"
+       gourl "net/url"
+       "os"
+       "os/exec"
+       "strconv"
+       "strings"
+       "time"
+
+       "github.com/google/pprof/internal/graph"
+       "github.com/google/pprof/internal/plugin"
+       "github.com/google/pprof/internal/report"
+       "github.com/google/pprof/profile"
+)
+
+// webInterface holds the state needed for serving a browser based interface.
+type webInterface struct {
+       prof      *profile.Profile
+       options   *plugin.Options
+       help      map[string]string
+       templates *template.Template
+}
+
+func makeWebInterface(p *profile.Profile, opt *plugin.Options) *webInterface {
+       templates := template.New("templategroup")
+       addTemplates(templates)
+       report.AddSourceTemplates(templates)
+       return &webInterface{
+               prof:      p,
+               options:   opt,
+               help:      make(map[string]string),
+               templates: templates,
+       }
+}
+
+// maxEntries is the maximum number of entries to print for text interfaces.
+const maxEntries = 50
+
+// errorCatcher is a UI that captures errors for reporting to the browser.
+type errorCatcher struct {
+       plugin.UI
+       errors []string
+}
+
+func (ec *errorCatcher) PrintErr(args ...interface{}) {
+       ec.errors = append(ec.errors, strings.TrimSuffix(fmt.Sprintln(args...), "\n"))
+       ec.UI.PrintErr(args...)
+}
+
+// webArgs contains arguments passed to templates in webhtml.go.
+type webArgs struct {
+       BaseURL  string
+       Title    string
+       Errors   []string
+       Total    int64
+       Legend   []string
+       Help     map[string]string
+       Nodes    []string
+       HTMLBody template.HTML
+       TextBody string
+       Top      []report.TextItem
+}
+
+func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) error {
+       host, portStr, err := net.SplitHostPort(hostport)
+       if err != nil {
+               return fmt.Errorf("could not split http address: %v", err)
+       }
+       port, err := strconv.Atoi(portStr)
+       if err != nil {
+               return fmt.Errorf("invalid port number: %v", err)
+       }
+       if host == "" {
+               host = "localhost"
+       }
+
+       interactiveMode = true
+       ui := makeWebInterface(p, o)
+       for n, c := range pprofCommands {
+               ui.help[n] = c.description
+       }
+       for n, v := range pprofVariables {
+               ui.help[n] = v.help
+       }
+       ui.help["details"] = "Show information about the profile and this view"
+       ui.help["graph"] = "Display profile as a directed graph"
+       ui.help["reset"] = "Show the entire profile"
+
+       server := o.HTTPServer
+       if server == nil {
+               server = defaultWebServer
+       }
+       args := &plugin.HTTPServerArgs{
+               Hostport: net.JoinHostPort(host, portStr),
+               Host:     host,
+               Port:     port,
+               Handlers: map[string]http.Handler{
+                       "/":       http.HandlerFunc(ui.dot),
+                       "/top":    http.HandlerFunc(ui.top),
+                       "/disasm": http.HandlerFunc(ui.disasm),
+                       "/source": http.HandlerFunc(ui.source),
+                       "/peek":   http.HandlerFunc(ui.peek),
+               },
+       }
+
+       go openBrowser("http://"+args.Hostport, o)
+       return server(args)
+}
+
+func defaultWebServer(args *plugin.HTTPServerArgs) error {
+       ln, err := net.Listen("tcp", args.Hostport)
+       if err != nil {
+               return err
+       }
+       isLocal := isLocalhost(args.Host)
+       handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               if isLocal {
+                       // Only allow local clients
+                       host, _, err := net.SplitHostPort(req.RemoteAddr)
+                       if err != nil || !isLocalhost(host) {
+                               http.Error(w, "permission denied", http.StatusForbidden)
+                               return
+                       }
+               }
+               h := args.Handlers[req.URL.Path]
+               if h == nil {
+                       // Fall back to default behavior
+                       h = http.DefaultServeMux
+               }
+               h.ServeHTTP(w, req)
+       })
+       s := &http.Server{Handler: handler}
+       return s.Serve(ln)
+}
+
+func isLocalhost(host string) bool {
+       for _, v := range []string{"localhost", "127.0.0.1", "[::1]", "::1"} {
+               if host == v {
+                       return true
+               }
+       }
+       return false
+}
+
+func openBrowser(url string, o *plugin.Options) {
+       // Construct URL.
+       u, _ := gourl.Parse(url)
+       q := u.Query()
+       for _, p := range []struct{ param, key string }{
+               {"f", "focus"},
+               {"s", "show"},
+               {"i", "ignore"},
+               {"h", "hide"},
+       } {
+               if v := pprofVariables[p.key].value; v != "" {
+                       q.Set(p.param, v)
+               }
+       }
+       u.RawQuery = q.Encode()
+
+       // Give server a little time to get ready.
+       time.Sleep(time.Millisecond * 500)
+
+       for _, b := range browsers() {
+               args := strings.Split(b, " ")
+               if len(args) == 0 {
+                       continue
+               }
+               viewer := exec.Command(args[0], append(args[1:], u.String())...)
+               viewer.Stderr = os.Stderr
+               if err := viewer.Start(); err == nil {
+                       return
+               }
+       }
+       // No visualizer succeeded, so just print URL.
+       o.UI.PrintErr(u.String())
+}
+
+func varsFromURL(u *gourl.URL) variables {
+       vars := pprofVariables.makeCopy()
+       vars["focus"].value = u.Query().Get("f")
+       vars["show"].value = u.Query().Get("s")
+       vars["ignore"].value = u.Query().Get("i")
+       vars["hide"].value = u.Query().Get("h")
+       return vars
+}
+
+// makeReport generates a report for the specified command.
+func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request,
+       cmd []string, vars ...string) (*report.Report, []string) {
+       v := varsFromURL(req.URL)
+       for i := 0; i+1 < len(vars); i += 2 {
+               v[vars[i]].value = vars[i+1]
+       }
+       catcher := &errorCatcher{UI: ui.options.UI}
+       options := *ui.options
+       options.UI = catcher
+       _, rpt, err := generateRawReport(ui.prof, cmd, v, &options)
+       if err != nil {
+               http.Error(w, err.Error(), http.StatusBadRequest)
+               ui.options.UI.PrintErr(err)
+               return nil, nil
+       }
+       return rpt, catcher.errors
+}
+
+// render generates html using the named template based on the contents of data.
+func (ui *webInterface) render(w http.ResponseWriter, baseURL, tmpl string,
+       rpt *report.Report, errList, legend []string, data webArgs) {
+       file := getFromLegend(legend, "File: ", "unknown")
+       profile := getFromLegend(legend, "Type: ", "unknown")
+       data.BaseURL = baseURL
+       data.Title = file + " " + profile
+       data.Errors = errList
+       data.Total = rpt.Total()
+       data.Legend = legend
+       data.Help = ui.help
+       html := &bytes.Buffer{}
+       if err := ui.templates.ExecuteTemplate(html, tmpl, data); err != nil {
+               http.Error(w, "internal template error", http.StatusInternalServerError)
+               ui.options.UI.PrintErr(err)
+               return
+       }
+       w.Header().Set("Content-Type", "text/html")
+       w.Write(html.Bytes())
+}
+
+// dot generates a web page containing an svg diagram.
+func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
+       rpt, errList := ui.makeReport(w, req, []string{"svg"})
+       if rpt == nil {
+               return // error already reported
+       }
+
+       // Generate dot graph.
+       g, config := report.GetDOT(rpt)
+       legend := config.Labels
+       config.Labels = nil
+       dot := &bytes.Buffer{}
+       graph.ComposeDot(dot, g, &graph.DotAttributes{}, config)
+
+       // Convert to svg.
+       svg, err := dotToSvg(dot.Bytes())
+       if err != nil {
+               http.Error(w, "Could not execute dot; may need to install graphviz.",
+                       http.StatusNotImplemented)
+               ui.options.UI.PrintErr("Failed to execute dot. Is Graphviz installed?\n", err)
+               return
+       }
+
+       // Get all node names into an array.
+       nodes := []string{""} // dot starts with node numbered 1
+       for _, n := range g.Nodes {
+               nodes = append(nodes, n.Info.Name)
+       }
+
+       ui.render(w, "/", "graph", rpt, errList, legend, webArgs{
+               HTMLBody: template.HTML(string(svg)),
+               Nodes:    nodes,
+       })
+}
+
+func dotToSvg(dot []byte) ([]byte, error) {
+       cmd := exec.Command("dot", "-Tsvg")
+       out := &bytes.Buffer{}
+       cmd.Stdin, cmd.Stdout, cmd.Stderr = bytes.NewBuffer(dot), out, os.Stderr
+       if err := cmd.Run(); err != nil {
+               return nil, err
+       }
+
+       // Fix dot bug related to unquoted amperands.
+       svg := bytes.Replace(out.Bytes(), []byte("&;"), []byte("&amp;;"), -1)
+
+       // Cleanup for embedding by dropping stuff before the <svg> start.
+       if pos := bytes.Index(svg, []byte("<svg")); pos >= 0 {
+               svg = svg[pos:]
+       }
+       return svg, nil
+}
+
+func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) {
+       rpt, errList := ui.makeReport(w, req, []string{"top"}, "nodecount", "500")
+       if rpt == nil {
+               return // error already reported
+       }
+       top, legend := report.TextItems(rpt)
+       var nodes []string
+       for _, item := range top {
+               nodes = append(nodes, item.Name)
+       }
+
+       ui.render(w, "/top", "top", rpt, errList, legend, webArgs{
+               Top:   top,
+               Nodes: nodes,
+       })
+}
+
+// disasm generates a web page containing disassembly.
+func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
+       args := []string{"disasm", req.URL.Query().Get("f")}
+       rpt, errList := ui.makeReport(w, req, args)
+       if rpt == nil {
+               return // error already reported
+       }
+
+       out := &bytes.Buffer{}
+       if err := report.PrintAssembly(out, rpt, ui.options.Obj, maxEntries); err != nil {
+               http.Error(w, err.Error(), http.StatusBadRequest)
+               ui.options.UI.PrintErr(err)
+               return
+       }
+
+       legend := report.ProfileLabels(rpt)
+       ui.render(w, "/disasm", "plaintext", rpt, errList, legend, webArgs{
+               TextBody: out.String(),
+       })
+
+}
+
+// source generates a web page containing source code annotated with profile
+// data.
+func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) {
+       args := []string{"weblist", req.URL.Query().Get("f")}
+       rpt, errList := ui.makeReport(w, req, args)
+       if rpt == nil {
+               return // error already reported
+       }
+
+       // Generate source listing.
+       var body bytes.Buffer
+       if err := report.PrintWebList(&body, rpt, ui.options.Obj, maxEntries); err != nil {
+               http.Error(w, err.Error(), http.StatusBadRequest)
+               ui.options.UI.PrintErr(err)
+               return
+       }
+
+       legend := report.ProfileLabels(rpt)
+       ui.render(w, "/source", "sourcelisting", rpt, errList, legend, webArgs{
+               HTMLBody: template.HTML(body.String()),
+       })
+}
+
+// peek generates a web page listing callers/callers.
+func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) {
+       args := []string{"peek", req.URL.Query().Get("f")}
+       rpt, errList := ui.makeReport(w, req, args, "lines", "t")
+       if rpt == nil {
+               return // error already reported
+       }
+
+       out := &bytes.Buffer{}
+       if err := report.Generate(out, rpt, ui.options.Obj); err != nil {
+               http.Error(w, err.Error(), http.StatusBadRequest)
+               ui.options.UI.PrintErr(err)
+               return
+       }
+
+       legend := report.ProfileLabels(rpt)
+       ui.render(w, "/peek", "plaintext", rpt, errList, legend, webArgs{
+               TextBody: out.String(),
+       })
+}
+
+// getFromLegend returns the suffix of an entry in legend that starts
+// with param.  It returns def if no such entry is found.
+func getFromLegend(legend []string, param, def string) string {
+       for _, s := range legend {
+               if strings.HasPrefix(s, param) {
+                       return s[len(param):]
+               }
+       }
+       return def
+}
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/webui_test.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/webui_test.go
new file mode 100644 (file)
index 0000000..76565eb
--- /dev/null
@@ -0,0 +1,232 @@
+// Copyright 2017 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 (
+       "fmt"
+       "io/ioutil"
+       "net"
+       "net/http"
+       "net/http/httptest"
+       "net/url"
+       "os/exec"
+       "regexp"
+       "sync"
+       "testing"
+
+       "github.com/google/pprof/internal/plugin"
+       "github.com/google/pprof/profile"
+       "runtime"
+)
+
+func TestWebInterface(t *testing.T) {
+       if runtime.GOOS == "nacl" {
+               t.Skip("test assumes tcp available")
+       }
+
+       prof := makeFakeProfile()
+
+       // Custom http server creator
+       var server *httptest.Server
+       serverCreated := make(chan bool)
+       creator := func(a *plugin.HTTPServerArgs) error {
+               server = httptest.NewServer(http.HandlerFunc(
+                       func(w http.ResponseWriter, r *http.Request) {
+                               if h := a.Handlers[r.URL.Path]; h != nil {
+                                       h.ServeHTTP(w, r)
+                               }
+                       }))
+               serverCreated <- true
+               return nil
+       }
+
+       // Start server and wait for it to be initialized
+       go serveWebInterface("unused:1234", prof, &plugin.Options{
+               Obj:        fakeObjTool{},
+               UI:         &stdUI{},
+               HTTPServer: creator,
+       })
+       <-serverCreated
+       defer server.Close()
+
+       haveDot := false
+       if _, err := exec.LookPath("dot"); err == nil {
+               haveDot = true
+       }
+
+       type testCase struct {
+               path    string
+               want    []string
+               needDot bool
+       }
+       testcases := []testCase{
+               {"/", []string{"F1", "F2", "F3", "testbin", "cpu"}, true},
+               {"/top", []string{`"Name":"F2","InlineLabel":"","Flat":200,"Cum":300,"FlatFormat":"200ms","CumFormat":"300ms"}`}, false},
+               {"/source?f=" + url.QueryEscape("F[12]"),
+                       []string{"F1", "F2", "300ms +line1"}, false},
+               {"/peek?f=" + url.QueryEscape("F[12]"),
+                       []string{"300ms.*F1", "200ms.*300ms.*F2"}, false},
+               {"/disasm?f=" + url.QueryEscape("F[12]"),
+                       []string{"f1:asm", "f2:asm"}, false},
+       }
+       for _, c := range testcases {
+               if c.needDot && !haveDot {
+                       t.Log("skipping", c.path, "since dot (graphviz) does not seem to be installed")
+                       continue
+               }
+
+               res, err := http.Get(server.URL + c.path)
+               if err != nil {
+                       t.Error("could not fetch", c.path, err)
+                       continue
+               }
+               data, err := ioutil.ReadAll(res.Body)
+               if err != nil {
+                       t.Error("could not read response", c.path, err)
+                       continue
+               }
+               result := string(data)
+               for _, w := range c.want {
+                       if match, _ := regexp.MatchString(w, result); !match {
+                               t.Errorf("response for %s does not match "+
+                                       "expected pattern '%s'; "+
+                                       "actual result:\n%s", c.path, w, result)
+                       }
+               }
+       }
+
+       // Also fetch all the test case URLs in parallel to test thread
+       // safety when run under the race detector.
+       var wg sync.WaitGroup
+       for _, c := range testcases {
+               if c.needDot && !haveDot {
+                       continue
+               }
+               path := server.URL + c.path
+               for count := 0; count < 2; count++ {
+                       wg.Add(1)
+                       go func() {
+                               http.Get(path)
+                               wg.Done()
+                       }()
+               }
+       }
+       wg.Wait()
+}
+
+// Implement fake object file support.
+
+const addrBase = 0x1000
+const fakeSource = "testdata/file1000.src"
+
+type fakeObj struct{}
+
+func (f fakeObj) Close() error    { return nil }
+func (f fakeObj) Name() string    { return "testbin" }
+func (f fakeObj) Base() uint64    { return 0 }
+func (f fakeObj) BuildID() string { return "" }
+func (f fakeObj) SourceLine(addr uint64) ([]plugin.Frame, error) {
+       return nil, fmt.Errorf("SourceLine unimplemented")
+}
+func (f fakeObj) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) {
+       return []*plugin.Sym{
+               {[]string{"F1"}, fakeSource, addrBase, addrBase + 10},
+               {[]string{"F2"}, fakeSource, addrBase + 10, addrBase + 20},
+               {[]string{"F3"}, fakeSource, addrBase + 20, addrBase + 30},
+       }, nil
+}
+
+type fakeObjTool struct{}
+
+func (obj fakeObjTool) Open(file string, start, limit, offset uint64) (plugin.ObjFile, error) {
+       return fakeObj{}, nil
+}
+
+func (obj fakeObjTool) Disasm(file string, start, end uint64) ([]plugin.Inst, error) {
+       return []plugin.Inst{
+               {Addr: addrBase + 0, Text: "f1:asm", Function: "F1"},
+               {Addr: addrBase + 10, Text: "f2:asm", Function: "F2"},
+               {Addr: addrBase + 20, Text: "d3:asm", Function: "F3"},
+       }, nil
+}
+
+func makeFakeProfile() *profile.Profile {
+       // Three functions: F1, F2, F3 with three lines, 11, 22, 33.
+       funcs := []*profile.Function{
+               {ID: 1, Name: "F1", Filename: fakeSource, StartLine: 3},
+               {ID: 2, Name: "F2", Filename: fakeSource, StartLine: 5},
+               {ID: 3, Name: "F3", Filename: fakeSource, StartLine: 7},
+       }
+       lines := []profile.Line{
+               {Function: funcs[0], Line: 11},
+               {Function: funcs[1], Line: 22},
+               {Function: funcs[2], Line: 33},
+       }
+       mapping := []*profile.Mapping{
+               {
+                       ID:             1,
+                       Start:          addrBase,
+                       Limit:          addrBase + 10,
+                       Offset:         0,
+                       File:           "testbin",
+                       HasFunctions:   true,
+                       HasFilenames:   true,
+                       HasLineNumbers: true,
+               },
+       }
+
+       // Three interesting addresses: base+{10,20,30}
+       locs := []*profile.Location{
+               {ID: 1, Address: addrBase + 10, Line: lines[0:1], Mapping: mapping[0]},
+               {ID: 2, Address: addrBase + 20, Line: lines[1:2], Mapping: mapping[0]},
+               {ID: 3, Address: addrBase + 30, Line: lines[2:3], Mapping: mapping[0]},
+       }
+
+       // Two stack traces.
+       return &profile.Profile{
+               PeriodType:    &profile.ValueType{Type: "cpu", Unit: "milliseconds"},
+               Period:        1,
+               DurationNanos: 10e9,
+               SampleType: []*profile.ValueType{
+                       {Type: "cpu", Unit: "milliseconds"},
+               },
+               Sample: []*profile.Sample{
+                       {
+                               Location: []*profile.Location{locs[2], locs[1], locs[0]},
+                               Value:    []int64{100},
+                       },
+                       {
+                               Location: []*profile.Location{locs[1], locs[0]},
+                               Value:    []int64{200},
+                       },
+               },
+               Location: locs,
+               Function: funcs,
+               Mapping:  mapping,
+       }
+}
+
+func TestIsLocalHost(t *testing.T) {
+       for _, s := range []string{"localhost:10000", "[::1]:10000", "127.0.0.1:10000"} {
+               host, _, err := net.SplitHostPort(s)
+               if err != nil {
+                       t.Error("unexpected error when splitting", s)
+                       continue
+               }
+               if !isLocalhost(host) {
+                       t.Errorf("host %s from %s not considered local", host, s)
+               }
+       }
+}
index c46272e8fca3dd249a8a0eca7aecca9dfc209575..9b238c5b87ec820f77faa39d9f20e23a2343c5dd 100644 (file)
@@ -131,7 +131,7 @@ func GetBuildID(binary io.ReaderAt) ([]byte, error) {
                                if buildID == nil {
                                        buildID = note.Desc
                                } else {
-                                       return nil, fmt.Errorf("multiple build ids found, don't know which to use!")
+                                       return nil, fmt.Errorf("multiple build ids found, don't know which to use")
                                }
                        }
                }
@@ -240,17 +240,22 @@ func GetBase(fh *elf.FileHeader, loadSegment *elf.ProgHeader, stextOffset *uint6
                }
                return start, nil
        case elf.ET_DYN:
-               if offset != 0 {
-                       if loadSegment == nil || loadSegment.Vaddr == 0 {
-                               return start - offset, nil
-                       }
-                       return 0, fmt.Errorf("Don't know how to handle mapping. Offset=%x, vaddr=%x",
-                               offset, loadSegment.Vaddr)
-               }
+               // The process mapping information, start = start of virtual address range,
+               // and offset = offset in the executable file of the start address, tells us
+               // that a runtime virtual address x maps to a file offset
+               // fx = x - start + offset.
                if loadSegment == nil {
-                       return start, nil
+                       return start - offset, nil
                }
-               return start - loadSegment.Vaddr, nil
+               // The program header, if not nil, indicates the offset in the file where
+               // the executable segment is located (loadSegment.Off), and the base virtual
+               // address where the first byte of the segment is loaded
+               // (loadSegment.Vaddr). A file offset fx maps to a virtual (symbol) address
+               // sx = fx - loadSegment.Off + loadSegment.Vaddr.
+               //
+               // Thus, a runtime virtual address x maps to a symbol address
+               // sx = x - start + offset - loadSegment.Off + loadSegment.Vaddr.
+               return start - offset + loadSegment.Off - loadSegment.Vaddr, nil
        }
        return 0, fmt.Errorf("Don't know how to handle FileHeader.Type %v", fh.Type)
 }
index b9f2a841a6da60f8cb2706fb872bc182fdbfe34a..c6b8fe4c22e7316ea2f48f88779d2da63989b941 100644 (file)
@@ -62,8 +62,9 @@ func TestGetBase(t *testing.T) {
                {"exec chromeos kernel 4", fhExec, kernelHeader, uint64p(0xffffffff81200198), 0x198, 0x100000, 0, 0x7ee00000, false},
                {"exec chromeos kernel unremapped", fhExec, kernelHeader, uint64p(0xffffffff810001c8), 0xffffffff834001c8, 0xffffffffc0000000, 0xffffffff834001c8, 0x2400000, false},
                {"dyn", fhDyn, nil, nil, 0x200000, 0x300000, 0, 0x200000, false},
-               {"dyn offset", fhDyn, lsOffset, nil, 0x0, 0x300000, 0, 0xFFFFFFFFFFC00000, false},
+               {"dyn map", fhDyn, lsOffset, nil, 0x0, 0x300000, 0, 0xFFFFFFFFFFE00000, false},
                {"dyn nomap", fhDyn, nil, nil, 0x0, 0x0, 0, 0, false},
+               {"dyn map+offset", fhDyn, lsOffset, nil, 0x900000, 0xa00000, 0x200000, 0x500000, false},
                {"rel", fhRel, nil, nil, 0x2000000, 0x3000000, 0, 0x2000000, false},
                {"rel nomap", fhRel, nil, nil, 0x0, ^uint64(0), 0, 0, false},
                {"rel offset", fhRel, nil, nil, 0x100000, 0x200000, 0x1, 0, true},
index c99e8992ded51ce30bf0ff233c08fa1a1672a321..4e5d12f6cdb8b6ace2c0e5f1b8fd6541216ece71 100644 (file)
@@ -42,15 +42,17 @@ type DotNodeAttributes struct {
 // DotConfig contains attributes about how a graph should be
 // constructed and how it should look.
 type DotConfig struct {
-       Title  string   // The title of the DOT graph
-       Labels []string // The labels for the DOT's legend
+       Title     string   // The title of the DOT graph
+       LegendURL string   // The URL to link to from the legend.
+       Labels    []string // The labels for the DOT's legend
 
-       FormatValue func(int64) string         // A formatting function for values
-       FormatTag   func(int64, string) string // A formatting function for numeric tags
-       Total       int64                      // The total weight of the graph, used to compute percentages
+       FormatValue func(int64) string // A formatting function for values
+       Total       int64              // The total weight of the graph, used to compute percentages
 }
 
-// Compose creates and writes a in the DOT format to the writer, using
+const maxNodelets = 4 // Number of nodelets for labels (both numeric and non)
+
+// ComposeDot creates and writes a in the DOT format to the writer, using
 // the configurations given.
 func ComposeDot(w io.Writer, g *Graph, a *DotAttributes, c *DotConfig) {
        builder := &builder{w, a, c}
@@ -120,11 +122,19 @@ func (b *builder) finish() {
 // addLegend generates a legend in DOT format.
 func (b *builder) addLegend() {
        labels := b.config.Labels
-       var title string
-       if len(labels) > 0 {
-               title = labels[0]
+       if len(labels) == 0 {
+               return
+       }
+       title := labels[0]
+       fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, title)
+       fmt.Fprintf(b, ` label="%s\l"`, strings.Join(labels, `\l`))
+       if b.config.LegendURL != "" {
+               fmt.Fprintf(b, ` URL="%s" target="_blank"`, b.config.LegendURL)
+       }
+       if b.config.Title != "" {
+               fmt.Fprintf(b, ` tooltip="%s"`, b.config.Title)
        }
-       fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16 label="%s\l"] }`+"\n", title, strings.Join(labels, `\l`))
+       fmt.Fprintf(b, "] }\n")
 }
 
 // addNode generates a graph node in DOT format.
@@ -176,8 +186,8 @@ func (b *builder) addNode(node *Node, nodeID int, maxFlat float64) {
        }
 
        // Create DOT attribute for node.
-       attr := fmt.Sprintf(`label="%s" fontsize=%d shape=%s tooltip="%s (%s)" color="%s" fillcolor="%s"`,
-               label, fontSize, shape, node.Info.PrintableName(), cumValue,
+       attr := fmt.Sprintf(`label="%s" id="node%d" fontsize=%d shape=%s tooltip="%s (%s)" color="%s" fillcolor="%s"`,
+               label, nodeID, fontSize, shape, node.Info.PrintableName(), cumValue,
                dotColor(float64(node.CumValue())/float64(abs64(b.config.Total)), false),
                dotColor(float64(node.CumValue())/float64(abs64(b.config.Total)), true))
 
@@ -204,13 +214,11 @@ func (b *builder) addNode(node *Node, nodeID int, maxFlat float64) {
 
 // addNodelets generates the DOT boxes for the node tags if they exist.
 func (b *builder) addNodelets(node *Node, nodeID int) bool {
-       const maxNodelets = 4    // Number of nodelets for alphanumeric labels
-       const maxNumNodelets = 4 // Number of nodelets for numeric labels
        var nodelets string
 
        // Populate two Tag slices, one for LabelTags and one for NumericTags.
        var ts []*Tag
-       lnts := make(map[string][]*Tag, 0)
+       lnts := make(map[string][]*Tag)
        for _, t := range node.LabelTags {
                ts = append(ts, t)
        }
@@ -239,15 +247,15 @@ func (b *builder) addNodelets(node *Node, nodeID int) bool {
                        continue
                }
                weight := b.config.FormatValue(w)
-               nodelets += fmt.Sprintf(`N%d_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", nodeID, i, t.Name, weight)
+               nodelets += fmt.Sprintf(`N%d_%d [label = "%s" id="N%d_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", nodeID, i, t.Name, nodeID, i, weight)
                nodelets += fmt.Sprintf(`N%d -> N%d_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"]`+"\n", nodeID, nodeID, i, weight, weight, weight)
                if nts := lnts[t.Name]; nts != nil {
-                       nodelets += b.numericNodelets(nts, maxNumNodelets, flatTags, fmt.Sprintf(`N%d_%d`, nodeID, i))
+                       nodelets += b.numericNodelets(nts, maxNodelets, flatTags, fmt.Sprintf(`N%d_%d`, nodeID, i))
                }
        }
 
        if nts := lnts[""]; nts != nil {
-               nodelets += b.numericNodelets(nts, maxNumNodelets, flatTags, fmt.Sprintf(`N%d`, nodeID))
+               nodelets += b.numericNodelets(nts, maxNodelets, flatTags, fmt.Sprintf(`N%d`, nodeID))
        }
 
        fmt.Fprint(b, nodelets)
@@ -266,7 +274,7 @@ func (b *builder) numericNodelets(nts []*Tag, maxNumNodelets int, flatTags bool,
                }
                if w != 0 {
                        weight := b.config.FormatValue(w)
-                       nodelets += fmt.Sprintf(`N%s_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", source, j, t.Name, weight)
+                       nodelets += fmt.Sprintf(`N%s_%d [label = "%s" id="N%s_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", source, j, t.Name, source, j, weight)
                        nodelets += fmt.Sprintf(`%s -> N%s_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"%s]`+"\n", source, source, j, weight, weight, weight, attr)
                }
        }
@@ -441,14 +449,9 @@ func tagDistance(t, u *Tag) float64 {
 }
 
 func (b *builder) tagGroupLabel(g []*Tag) (label string, flat, cum int64) {
-       formatTag := b.config.FormatTag
-       if formatTag == nil {
-               formatTag = measurement.Label
-       }
-
        if len(g) == 1 {
                t := g[0]
-               return formatTag(t.Value, t.Unit), t.FlatValue(), t.CumValue()
+               return measurement.Label(t.Value, t.Unit), t.FlatValue(), t.CumValue()
        }
        min := g[0]
        max := g[0]
@@ -472,7 +475,11 @@ func (b *builder) tagGroupLabel(g []*Tag) (label string, flat, cum int64) {
        if dc != 0 {
                c = c / dc
        }
-       return formatTag(min.Value, min.Unit) + ".." + formatTag(max.Value, max.Unit), f, c
+
+       // Tags are not scaled with the selected output unit because tags are often
+       // much smaller than other values which appear, so the range of tag sizes
+       // sometimes would appear to be "0..0" when scaled to the selected output unit.
+       return measurement.Label(min.Value, min.Unit) + ".." + measurement.Label(max.Value, max.Unit), f, c
 }
 
 func min64(a, b int64) int64 {
index 7f512697692e36f2abc2a2d87a6498ff1e99b412..b8368b8fa4f84d46b6c193f3938b15680aea7b31 100644 (file)
@@ -16,8 +16,10 @@ package graph
 
 import (
        "bytes"
+       "flag"
        "fmt"
        "io/ioutil"
+       "path/filepath"
        "reflect"
        "strconv"
        "strings"
@@ -26,7 +28,7 @@ import (
        "github.com/google/pprof/internal/proftest"
 )
 
-const path = "testdata/"
+var updateFlag = flag.Bool("update", false, "Update the golden files")
 
 func TestComposeWithStandardGraph(t *testing.T) {
        g := baseGraph()
@@ -35,12 +37,7 @@ func TestComposeWithStandardGraph(t *testing.T) {
        var buf bytes.Buffer
        ComposeDot(&buf, g, a, c)
 
-       want, err := ioutil.ReadFile(path + "compose1.dot")
-       if err != nil {
-               t.Fatalf("error reading test file: %v", err)
-       }
-
-       compareGraphs(t, buf.Bytes(), want)
+       compareGraphs(t, buf.Bytes(), "compose1.dot")
 }
 
 func TestComposeWithNodeAttributesAndZeroFlat(t *testing.T) {
@@ -64,12 +61,7 @@ func TestComposeWithNodeAttributesAndZeroFlat(t *testing.T) {
        var buf bytes.Buffer
        ComposeDot(&buf, g, a, c)
 
-       want, err := ioutil.ReadFile(path + "compose2.dot")
-       if err != nil {
-               t.Fatalf("error reading test file: %v", err)
-       }
-
-       compareGraphs(t, buf.Bytes(), want)
+       compareGraphs(t, buf.Bytes(), "compose2.dot")
 }
 
 func TestComposeWithTagsAndResidualEdge(t *testing.T) {
@@ -97,12 +89,7 @@ func TestComposeWithTagsAndResidualEdge(t *testing.T) {
        var buf bytes.Buffer
        ComposeDot(&buf, g, a, c)
 
-       want, err := ioutil.ReadFile(path + "compose3.dot")
-       if err != nil {
-               t.Fatalf("error reading test file: %v", err)
-       }
-
-       compareGraphs(t, buf.Bytes(), want)
+       compareGraphs(t, buf.Bytes(), "compose3.dot")
 }
 
 func TestComposeWithNestedTags(t *testing.T) {
@@ -127,12 +114,7 @@ func TestComposeWithNestedTags(t *testing.T) {
        var buf bytes.Buffer
        ComposeDot(&buf, g, a, c)
 
-       want, err := ioutil.ReadFile(path + "compose5.dot")
-       if err != nil {
-               t.Fatalf("error reading test file: %v", err)
-       }
-
-       compareGraphs(t, buf.Bytes(), want)
+       compareGraphs(t, buf.Bytes(), "compose5.dot")
 }
 
 func TestComposeWithEmptyGraph(t *testing.T) {
@@ -142,12 +124,18 @@ func TestComposeWithEmptyGraph(t *testing.T) {
        var buf bytes.Buffer
        ComposeDot(&buf, g, a, c)
 
-       want, err := ioutil.ReadFile(path + "compose4.dot")
-       if err != nil {
-               t.Fatalf("error reading test file: %v", err)
-       }
+       compareGraphs(t, buf.Bytes(), "compose4.dot")
+}
+
+func TestComposeWithStandardGraphAndURL(t *testing.T) {
+       g := baseGraph()
+       a, c := baseAttrsAndConfig()
+       c.LegendURL = "http://example.com"
+
+       var buf bytes.Buffer
+       ComposeDot(&buf, g, a, c)
 
-       compareGraphs(t, buf.Bytes(), want)
+       compareGraphs(t, buf.Bytes(), "compose6.dot")
 }
 
 func baseGraph() *Graph {
@@ -199,13 +187,78 @@ func baseAttrsAndConfig() (*DotAttributes, *DotConfig) {
        return a, c
 }
 
-func compareGraphs(t *testing.T, got, want []byte) {
+func compareGraphs(t *testing.T, got []byte, wantFile string) {
+       wantFile = filepath.Join("testdata", wantFile)
+       want, err := ioutil.ReadFile(wantFile)
+       if err != nil {
+               t.Fatalf("error reading test file %s: %v", wantFile, err)
+       }
+
        if string(got) != string(want) {
                d, err := proftest.Diff(got, want)
                if err != nil {
                        t.Fatalf("error finding diff: %v", err)
                }
                t.Errorf("Compose incorrectly wrote %s", string(d))
+               if *updateFlag {
+                       err := ioutil.WriteFile(wantFile, got, 0644)
+                       if err != nil {
+                               t.Errorf("failed to update the golden file %q: %v", wantFile, err)
+                       }
+               }
+       }
+}
+
+func TestNodeletCountCapping(t *testing.T) {
+       labelTags := make(TagMap)
+       for i := 0; i < 10; i++ {
+               name := fmt.Sprintf("tag-%d", i)
+               labelTags[name] = &Tag{
+                       Name: name,
+                       Flat: 10,
+                       Cum:  10,
+               }
+       }
+       numTags := make(TagMap)
+       for i := 0; i < 10; i++ {
+               name := fmt.Sprintf("num-tag-%d", i)
+               numTags[name] = &Tag{
+                       Name:  name,
+                       Unit:  "mb",
+                       Value: 16,
+                       Flat:  10,
+                       Cum:   10,
+               }
+       }
+       node1 := &Node{
+               Info:        NodeInfo{Name: "node1-with-tags"},
+               Flat:        10,
+               Cum:         10,
+               NumericTags: map[string]TagMap{"": numTags},
+               LabelTags:   labelTags,
+       }
+       node2 := &Node{
+               Info: NodeInfo{Name: "node2"},
+               Flat: 15,
+               Cum:  15,
+       }
+       node3 := &Node{
+               Info: NodeInfo{Name: "node3"},
+               Flat: 15,
+               Cum:  15,
+       }
+       g := &Graph{
+               Nodes: Nodes{
+                       node1,
+                       node2,
+                       node3,
+               },
+       }
+       for n := 1; n <= 3; n++ {
+               input := maxNodelets + n
+               if got, want := len(g.SelectTopNodes(input, true)), n; got != want {
+                       t.Errorf("SelectTopNodes(%d): got %d nodes, want %d", input, got, want)
+               }
        }
 }
 
@@ -240,19 +293,19 @@ func TestTagCollapse(t *testing.T) {
        }
 
        tagWant := [][]*Tag{
-               []*Tag{
+               {
                        makeTag("1B..2GB", "", 0, 2401, 2401),
                },
-               []*Tag{
+               {
                        makeTag("2GB", "", 0, 1000, 1000),
                        makeTag("1B..12MB", "", 0, 1401, 1401),
                },
-               []*Tag{
+               {
                        makeTag("2GB", "", 0, 1000, 1000),
                        makeTag("12MB", "", 0, 100, 100),
                        makeTag("1B..1MB", "", 0, 1301, 1301),
                },
-               []*Tag{
+               {
                        makeTag("2GB", "", 0, 1000, 1000),
                        makeTag("1MB", "", 0, 1000, 1000),
                        makeTag("2B..1kB", "", 0, 201, 201),
index 428e6257c760b8840ffe53c7ee5188e994ab0c39..cd72bf2ab13c46ef25ba1e177e8c679cfd75a56a 100644 (file)
@@ -240,6 +240,8 @@ type Edge struct {
        Inline bool
 }
 
+// WeightValue returns the weight value for this edge, normalizing if a
+// divisor is available.
 func (e *Edge) WeightValue() int64 {
        if e.WeightDiv == 0 {
                return e.Weight
@@ -327,7 +329,7 @@ func newGraph(prof *profile.Profile, o *Options) (*Graph, map[uint64]Nodes) {
                                // Add cum weight to all nodes in stack, avoiding double counting.
                                if _, ok := seenNode[n]; !ok {
                                        seenNode[n] = true
-                                       n.addSample(dw, w, labels, sample.NumLabel, o.FormatTag, false)
+                                       n.addSample(dw, w, labels, sample.NumLabel, sample.NumUnit, o.FormatTag, false)
                                }
                                // Update edge weights for all edges in stack, avoiding double counting.
                                if _, ok := seenEdge[nodePair{n, parent}]; !ok && parent != nil && n != parent {
@@ -340,7 +342,7 @@ func newGraph(prof *profile.Profile, o *Options) (*Graph, map[uint64]Nodes) {
                }
                if parent != nil && !residual {
                        // Add flat weight to leaf node.
-                       parent.addSample(dw, w, labels, sample.NumLabel, o.FormatTag, true)
+                       parent.addSample(dw, w, labels, sample.NumLabel, sample.NumUnit, o.FormatTag, true)
                }
        }
 
@@ -399,7 +401,7 @@ func newTree(prof *profile.Profile, o *Options) (g *Graph) {
                                if n == nil {
                                        continue
                                }
-                               n.addSample(dw, w, labels, sample.NumLabel, o.FormatTag, false)
+                               n.addSample(dw, w, labels, sample.NumLabel, sample.NumUnit, o.FormatTag, false)
                                if parent != nil {
                                        parent.AddToEdgeDiv(n, dw, w, false, lidx != len(lines)-1)
                                }
@@ -407,7 +409,7 @@ func newTree(prof *profile.Profile, o *Options) (g *Graph) {
                        }
                }
                if parent != nil {
-                       parent.addSample(dw, w, labels, sample.NumLabel, o.FormatTag, true)
+                       parent.addSample(dw, w, labels, sample.NumLabel, sample.NumUnit, o.FormatTag, true)
                }
        }
 
@@ -600,7 +602,7 @@ func (ns Nodes) Sum() (flat int64, cum int64) {
        return
 }
 
-func (n *Node) addSample(dw, w int64, labels string, numLabel map[string][]int64, format func(int64, string) string, flat bool) {
+func (n *Node) addSample(dw, w int64, labels string, numLabel map[string][]int64, numUnit map[string][]string, format func(int64, string) string, flat bool) {
        // Update sample value
        if flat {
                n.FlatDiv += dw
@@ -631,9 +633,15 @@ func (n *Node) addSample(dw, w int64, labels string, numLabel map[string][]int64
        if format == nil {
                format = defaultLabelFormat
        }
-       for key, nvals := range numLabel {
-               for _, v := range nvals {
-                       t := numericTags.findOrAddTag(format(v, key), key, v)
+       for k, nvals := range numLabel {
+               units := numUnit[k]
+               for i, v := range nvals {
+                       var t *Tag
+                       if len(units) > 0 {
+                               t = numericTags.findOrAddTag(format(v, units[i]), units[i], v)
+                       } else {
+                               t = numericTags.findOrAddTag(format(v, k), k, v)
+                       }
                        if flat {
                                t.FlatDiv += dw
                                t.Flat += w
@@ -800,7 +808,11 @@ func (g *Graph) selectTopNodes(maxNodes int, visualMode bool) Nodes {
                        // If generating a visual graph, count tags as nodes. Update
                        // maxNodes to account for them.
                        for i, n := range g.Nodes {
-                               if count += countTags(n) + 1; count >= maxNodes {
+                               tags := countTags(n)
+                               if tags > maxNodelets {
+                                       tags = maxNodelets
+                               }
+                               if count += tags + 1; count >= maxNodes {
                                        maxNodes = i + 1
                                        break
                                }
@@ -832,17 +844,6 @@ func countTags(n *Node) int {
        return count
 }
 
-// countEdges counts the number of edges below the specified cutoff.
-func countEdges(el EdgeMap, cutoff int64) int {
-       count := 0
-       for _, e := range el {
-               if e.Weight > cutoff {
-                       count++
-               }
-       }
-       return count
-}
-
 // RemoveRedundantEdges removes residual edges if the destination can
 // be reached through another path. This is done to simplify the graph
 // while preserving connectivity.
index c2848f8cf20990eab8b9ec6e9110ed2e17a17276..5657084cac06f71b548d999e0bc2e3f27a0bec17 100644 (file)
@@ -171,7 +171,7 @@ func createExpectedEdges(parent expectedNode, children ...expectedNode) {
        }
 }
 
-// createTestCase1 creates a test case that initally looks like:
+// createTestCase1 creates a test case that initially looks like:
 //     0
 //     |(5)
 //     1
@@ -255,7 +255,7 @@ func createTestCase2() trimTreeTestcase {
        }
 }
 
-// createTestCase3 creates an initally empty graph and expects an empty graph
+// createTestCase3 creates an initially empty graph and expects an empty graph
 // after trimming.
 func createTestCase3() trimTreeTestcase {
        graph := &Graph{make(Nodes, 0)}
index ceed025318a8023a912c3c83b2f64989e6bf34a4..da349a40a8b406fdcf567ac83a74d50f0afff19f 100644 (file)
@@ -1,7 +1,7 @@
 digraph "testtitle" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l"] }
-N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
-N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] }
+N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
+N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
 N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)"]
 }
index ee951fe3b1ce01fc48110ef6bd83f8c9b54a68ee..0c1a6ebaf12b1baefeb0f6d85e9b7c116c9cb652 100644 (file)
@@ -1,7 +1,7 @@
 digraph "testtitle" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l"] }
-N1 [label="SRC10 (10.00%)\nof 25 (25.00%)" fontsize=24 shape=folder tooltip="src (25)" color="#b23c00" fillcolor="#edddd5" style="bold,filled" peripheries=2 URL="www.google.com" target="_blank"]
-N2 [label="dest\n0 of 25 (25.00%)" fontsize=8 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] }
+N1 [label="SRC10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=24 shape=folder tooltip="src (25)" color="#b23c00" fillcolor="#edddd5" style="bold,filled" peripheries=2 URL="www.google.com" target="_blank"]
+N2 [label="dest\n0 of 25 (25.00%)" id="node2" fontsize=8 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
 N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)"]
 }
index 99a3119b82b12ceea7088bb0a66148242339538f..1b878b79df940c26377b7799d3eb52cafc269c6a 100644 (file)
@@ -1,11 +1,11 @@
 digraph "testtitle" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l"] }
-N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
-N1_0 [label = "tag1" fontsize=8 shape=box3d tooltip="10"]
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] }
+N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
+N1_0 [label = "tag1" id="N1_0" fontsize=8 shape=box3d tooltip="10"]
 N1 -> N1_0 [label=" 10" weight=100 tooltip="10" labeltooltip="10"]
-NN1_0 [label = "tag2" fontsize=8 shape=box3d tooltip="20"]
+NN1_0 [label = "tag2" id="NN1_0" fontsize=8 shape=box3d tooltip="20"]
 N1 -> NN1_0 [label=" 20" weight=100 tooltip="20" labeltooltip="20"]
-N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
+N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
 N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src ... dest (10)" labeltooltip="src ... dest (10)" style="dotted" minlen=2]
 }
index adc9cc6f68a83247887c3342f527bdb38b758666..302da8ce948edb47b0cbdfc55ee9612576877b8c 100644 (file)
@@ -1,4 +1,4 @@
 digraph "testtitle" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l"] }
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] }
 }
index 352975f5878c1cbbcef96786a8f055decaed4ba6..8876e337e669e8431e0568780f79f91fbf89bc1d 100644 (file)
@@ -1,11 +1,11 @@
 digraph "testtitle" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l"] }
-N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
-N1_0 [label = "tag1" fontsize=8 shape=box3d tooltip="10"]
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] }
+N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
+N1_0 [label = "tag1" id="N1_0" fontsize=8 shape=box3d tooltip="10"]
 N1 -> N1_0 [label=" 10" weight=100 tooltip="10" labeltooltip="10"]
-NN1_0_0 [label = "tag2" fontsize=8 shape=box3d tooltip="20"]
+NN1_0_0 [label = "tag2" id="NN1_0_0" fontsize=8 shape=box3d tooltip="20"]
 N1_0 -> NN1_0_0 [label=" 20" weight=100 tooltip="20" labeltooltip="20"]
-N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
+N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
 N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)" minlen=2]
 }
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/graph/testdata/compose6.dot b/src/cmd/vendor/github.com/google/pprof/internal/graph/testdata/compose6.dot
new file mode 100644 (file)
index 0000000..cf88439
--- /dev/null
@@ -0,0 +1,7 @@
+digraph "testtitle" {
+node [style=filled fillcolor="#f8f8f8"]
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" URL="http://example.com" target="_blank" tooltip="testtitle"] }
+N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
+N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
+N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)"]
+}
index 0f7a21d26a8bd345f3d845bfe478205684db7d24..0a60435644bff14f5adb84cf6b9f920d593168f4 100644 (file)
@@ -170,12 +170,16 @@ func memoryLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok
 
        switch fromUnit {
        case "byte", "b":
-       case "kilobyte", "kb":
+       case "kb", "kbyte", "kilobyte":
                value *= 1024
-       case "megabyte", "mb":
+       case "mb", "mbyte", "megabyte":
                value *= 1024 * 1024
-       case "gigabyte", "gb":
+       case "gb", "gbyte", "gigabyte":
                value *= 1024 * 1024 * 1024
+       case "tb", "tbyte", "terabyte":
+               value *= 1024 * 1024 * 1024 * 1024
+       case "pb", "pbyte", "petabyte":
+               value *= 1024 * 1024 * 1024 * 1024 * 1024
        default:
                return 0, "", false
        }
@@ -188,8 +192,12 @@ func memoryLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok
                        toUnit = "kb"
                case value < 1024*1024*1024:
                        toUnit = "mb"
-               default:
+               case value < 1024*1024*1024*1024:
                        toUnit = "gb"
+               case value < 1024*1024*1024*1024*1024:
+                       toUnit = "tb"
+               default:
+                       toUnit = "pb"
                }
        }
 
@@ -203,6 +211,10 @@ func memoryLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok
                output, toUnit = float64(value)/(1024*1024), "MB"
        case "gb", "gbyte", "gigabyte":
                output, toUnit = float64(value)/(1024*1024*1024), "GB"
+       case "tb", "tbyte", "terabyte":
+               output, toUnit = float64(value)/(1024*1024*1024*1024), "TB"
+       case "pb", "pbyte", "petabyte":
+               output, toUnit = float64(value)/(1024*1024*1024*1024*1024), "PB"
        }
        return output, toUnit, true
 }
@@ -289,7 +301,7 @@ func timeLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok bo
        case "week", "wk":
                output, toUnit = dd/float64(7*24*time.Hour), "wks"
        case "year", "yr":
-               output, toUnit = dd/float64(365*7*24*time.Hour), "yrs"
+               output, toUnit = dd/float64(365*24*time.Hour), "yrs"
        default:
                fallthrough
        case "sec", "second", "s":
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/measurement/measurement_test.go b/src/cmd/vendor/github.com/google/pprof/internal/measurement/measurement_test.go
new file mode 100644 (file)
index 0000000..155cafa
--- /dev/null
@@ -0,0 +1,47 @@
+// Copyright 2017 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 measurement
+
+import (
+       "testing"
+)
+
+func TestScale(t *testing.T) {
+       for _, tc := range []struct {
+               value            int64
+               fromUnit, toUnit string
+               wantValue        float64
+               wantUnit         string
+       }{
+               {1, "s", "ms", 1000, "ms"},
+               {1, "kb", "b", 1024, "B"},
+               {1, "kbyte", "b", 1024, "B"},
+               {1, "kilobyte", "b", 1024, "B"},
+               {1, "mb", "kb", 1024, "kB"},
+               {1, "gb", "mb", 1024, "MB"},
+               {1024, "gb", "tb", 1, "TB"},
+               {1024, "tb", "pb", 1, "PB"},
+               {2048, "mb", "auto", 2, "GB"},
+               {3.1536e7, "s", "auto", 1, "yrs"},
+               {-1, "s", "ms", -1000, "ms"},
+               {1, "foo", "count", 1, ""},
+               {1, "foo", "bar", 1, "bar"},
+       } {
+               if gotValue, gotUnit := Scale(tc.value, tc.fromUnit, tc.toUnit); gotValue != tc.wantValue || gotUnit != tc.wantUnit {
+                       t.Errorf("Scale(%d, %q, %q) = (%f, %q), want (%f, %q)",
+                               tc.value, tc.fromUnit, tc.toUnit, gotValue, gotUnit, tc.wantValue, tc.wantUnit)
+               }
+       }
+}
index d14ac2c215d902512cdf28e9a4a29fec5c62f7a4..e5878aed7046a0e52f9526d57c006f59260dab0c 100644 (file)
@@ -17,6 +17,7 @@ package plugin
 
 import (
        "io"
+       "net/http"
        "regexp"
        "time"
 
@@ -31,6 +32,16 @@ type Options struct {
        Sym     Symbolizer
        Obj     ObjTool
        UI      UI
+
+       // HTTPServer is a function that should block serving http requests,
+       // including the handlers specfied in args.  If non-nil, pprof will
+       // invoke this function if necessary to provide a web interface.
+       //
+       // If HTTPServer is nil, pprof will use its own internal HTTP server.
+       //
+       // A common use for a custom HTTPServer is to provide custom
+       // authentication checks.
+       HTTPServer func(args *HTTPServerArgs) error
 }
 
 // Writer provides a mechanism to write data under a certain name,
@@ -185,3 +196,17 @@ type UI interface {
        // the auto-completion of cmd, if the UI supports auto-completion at all.
        SetAutoComplete(complete func(string) string)
 }
+
+// HTTPServerArgs contains arguments needed by an HTTP server that
+// is exporting a pprof web interface.
+type HTTPServerArgs struct {
+       // Hostport contains the http server address (derived from flags).
+       Hostport string
+
+       Host string // Host portion of Hostport
+       Port int    // Port portion of Hostport
+
+       // Handlers maps from URL paths to the handler to invoke to
+       // serve that path.
+       Handlers map[string]http.Handler
+}
index 9767b2eedb6868fb30305e51f7b2c4af4c479d57..7f9dcab61a6cc158e07fca8736068b00226fad36 100644 (file)
@@ -22,6 +22,7 @@ import (
        "io/ioutil"
        "os"
        "os/exec"
+       "regexp"
        "testing"
 )
 
@@ -71,10 +72,14 @@ func EncodeJSON(x interface{}) []byte {
 }
 
 // TestUI implements the plugin.UI interface, triggering test failures
-// if more than Ignore errors are printed.
+// if more than Ignore errors not matching AllowRx are printed.
+// Also tracks the number of times the error matches AllowRx in
+// NumAllowRxMatches.
 type TestUI struct {
-       T      *testing.T
-       Ignore int
+       T                 *testing.T
+       Ignore            int
+       AllowRx           string
+       NumAllowRxMatches int
 }
 
 // ReadLine returns no input, as no input is expected during testing.
@@ -89,11 +94,24 @@ func (ui *TestUI) Print(args ...interface{}) {
 // PrintErr messages may trigger an error failure. A fixed number of
 // error messages are permitted when appropriate.
 func (ui *TestUI) PrintErr(args ...interface{}) {
+       if ui.AllowRx != "" {
+               if matched, err := regexp.MatchString(ui.AllowRx, fmt.Sprint(args...)); matched || err != nil {
+                       if err != nil {
+                               ui.T.Errorf("failed to match against regex %q: %v", ui.AllowRx, err)
+                       }
+                       ui.NumAllowRxMatches++
+                       return
+               }
+       }
        if ui.Ignore > 0 {
                ui.Ignore--
                return
        }
-       ui.T.Error(args)
+       // Stringify arguments with fmt.Sprint() to match what default UI
+       // implementation does. Without this Error() calls fmt.Sprintln() which
+       // _always_ adds spaces between arguments, unlike fmt.Sprint() which only
+       // adds them between arguments if neither is string.
+       ui.T.Error(fmt.Sprint(args...))
 }
 
 // IsTerminal indicates if the UI is an interactive terminal.
index ecfd6982b1480e951d869108304f02983b5e1e11..f434554dd961677c5725e877c9ed6f60a7bd72b2 100644 (file)
@@ -25,6 +25,7 @@ import (
        "sort"
        "strconv"
        "strings"
+       "text/tabwriter"
        "time"
 
        "github.com/google/pprof/internal/graph"
@@ -63,6 +64,8 @@ type Options struct {
        Ratio               float64
        Title               string
        ProfileLabels       []string
+       ActiveFilters       []string
+       NumLabelUnits       map[string]string
 
        NodeCount    int
        NodeFraction float64
@@ -125,6 +128,9 @@ func (rpt *Report) newTrimmedGraph() (g *graph.Graph, origCount, droppedNodes, d
        visualMode := o.OutputFormat == Dot
        cumSort := o.CumSort
 
+       // The call_tree option is only honored when generating visual representations of the callgraph.
+       callTree := o.CallTree && (o.OutputFormat == Dot || o.OutputFormat == Callgrind)
+
        // First step: Build complete graph to identify low frequency nodes, based on their cum weight.
        g = rpt.newGraph(nil)
        totalValue, _ := g.Nodes.Sum()
@@ -133,7 +139,7 @@ func (rpt *Report) newTrimmedGraph() (g *graph.Graph, origCount, droppedNodes, d
 
        // Filter out nodes with cum value below nodeCutoff.
        if nodeCutoff > 0 {
-               if o.CallTree {
+               if callTree {
                        if nodesKept := g.DiscardLowFrequencyNodePtrs(nodeCutoff); len(g.Nodes) != len(nodesKept) {
                                droppedNodes = len(g.Nodes) - len(nodesKept)
                                g.TrimTree(nodesKept)
@@ -154,7 +160,7 @@ func (rpt *Report) newTrimmedGraph() (g *graph.Graph, origCount, droppedNodes, d
                // Remove low frequency tags and edges as they affect selection.
                g.TrimLowFrequencyTags(nodeCutoff)
                g.TrimLowFrequencyEdges(edgeCutoff)
-               if o.CallTree {
+               if callTree {
                        if nodesKept := g.SelectTopNodePtrs(nodeCount, visualMode); len(g.Nodes) != len(nodesKept) {
                                g.TrimTree(nodesKept)
                                g.SortNodes(cumSort, visualMode)
@@ -236,15 +242,27 @@ func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph {
        for _, f := range prof.Function {
                f.Filename = trimPath(f.Filename)
        }
-       // Remove numeric tags not recognized by pprof.
+       // Removes all numeric tags except for the bytes tag prior
+       // to making graph.
+       // TODO: modify to select first numeric tag if no bytes tag
        for _, s := range prof.Sample {
                numLabels := make(map[string][]int64, len(s.NumLabel))
-               for k, v := range s.NumLabel {
+               numUnits := make(map[string][]string, len(s.NumLabel))
+               for k, vs := range s.NumLabel {
                        if k == "bytes" {
-                               numLabels[k] = append(numLabels[k], v...)
+                               unit := o.NumLabelUnits[k]
+                               numValues := make([]int64, len(vs))
+                               numUnit := make([]string, len(vs))
+                               for i, v := range vs {
+                                       numValues[i] = v
+                                       numUnit[i] = unit
+                               }
+                               numLabels[k] = append(numLabels[k], numValues...)
+                               numUnits[k] = append(numUnits[k], numUnit...)
                        }
                }
                s.NumLabel = numLabels
+               s.NumUnit = numUnits
        }
 
        formatTag := func(v int64, key string) string {
@@ -337,6 +355,11 @@ func (fm functionMap) FindOrAdd(ni graph.NodeInfo) *profile.Function {
 
 // printAssembly prints an annotated assembly listing.
 func printAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
+       return PrintAssembly(w, rpt, obj, -1)
+}
+
+// PrintAssembly prints annotated disasssembly of rpt to w.
+func PrintAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFuncs int) error {
        o := rpt.options
        prof := rpt.prof
 
@@ -352,12 +375,34 @@ func printAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
        fmt.Fprintln(w, "Total:", rpt.formatValue(rpt.total))
        symbols := symbolsFromBinaries(prof, g, o.Symbol, address, obj)
        symNodes := nodesPerSymbol(g.Nodes, symbols)
-       // Sort function names for printing.
-       var syms objSymbols
+
+       // Sort for printing.
+       var syms []*objSymbol
        for s := range symNodes {
                syms = append(syms, s)
        }
-       sort.Sort(syms)
+       byName := func(a, b *objSymbol) bool {
+               if na, nb := a.sym.Name[0], b.sym.Name[0]; na != nb {
+                       return na < nb
+               }
+               return a.sym.Start < b.sym.Start
+       }
+       if maxFuncs < 0 {
+               sort.Sort(orderSyms{syms, byName})
+       } else {
+               byFlatSum := func(a, b *objSymbol) bool {
+                       suma, _ := symNodes[a].Sum()
+                       sumb, _ := symNodes[b].Sum()
+                       if suma != sumb {
+                               return suma > sumb
+                       }
+                       return byName(a, b)
+               }
+               sort.Sort(orderSyms{syms, byFlatSum})
+               if len(syms) > maxFuncs {
+                       syms = syms[:maxFuncs]
+               }
+       }
 
        // Correlate the symbols from the binary with the profile samples.
        for _, s := range syms {
@@ -471,6 +516,7 @@ func symbolsFromBinaries(prof *profile.Profile, g *graph.Graph, rx *regexp.Regex
                                &objSymbol{
                                        sym:  ms,
                                        base: base,
+                                       file: f,
                                },
                        )
                }
@@ -485,25 +531,18 @@ func symbolsFromBinaries(prof *profile.Profile, g *graph.Graph, rx *regexp.Regex
 type objSymbol struct {
        sym  *plugin.Sym
        base uint64
+       file plugin.ObjFile
 }
 
-// objSymbols is a wrapper type to enable sorting of []*objSymbol.
-type objSymbols []*objSymbol
-
-func (o objSymbols) Len() int {
-       return len(o)
-}
-
-func (o objSymbols) Less(i, j int) bool {
-       if namei, namej := o[i].sym.Name[0], o[j].sym.Name[0]; namei != namej {
-               return namei < namej
-       }
-       return o[i].sym.Start < o[j].sym.Start
+// orderSyms is a wrapper type to sort []*objSymbol by a supplied comparator.
+type orderSyms struct {
+       v    []*objSymbol
+       less func(a, b *objSymbol) bool
 }
 
-func (o objSymbols) Swap(i, j int) {
-       o[i], o[j] = o[j], o[i]
-}
+func (o orderSyms) Len() int           { return len(o.v) }
+func (o orderSyms) Less(i, j int) bool { return o.less(o.v[i], o.v[j]) }
+func (o orderSyms) Swap(i, j int)      { o.v[i], o.v[j] = o.v[j], o.v[i] }
 
 // nodesPerSymbol classifies nodes into a group of symbols.
 func nodesPerSymbol(ns graph.Nodes, symbols []*objSymbol) map[*objSymbol]graph.Nodes {
@@ -528,6 +567,13 @@ type assemblyInstruction struct {
        line            int
        flat, cum       int64
        flatDiv, cumDiv int64
+       startsBlock     bool
+       inlineCalls     []callID
+}
+
+type callID struct {
+       file string
+       line int
 }
 
 func (a *assemblyInstruction) flatValue() int64 {
@@ -617,25 +663,24 @@ func printTags(w io.Writer, rpt *Report) error {
        for _, s := range p.Sample {
                for key, vals := range s.Label {
                        for _, val := range vals {
-                               if valueMap, ok := tagMap[key]; ok {
-                                       valueMap[val] = valueMap[val] + s.Value[0]
-                                       continue
+                               valueMap, ok := tagMap[key]
+                               if !ok {
+                                       valueMap = make(map[string]int64)
+                                       tagMap[key] = valueMap
                                }
-                               valueMap := make(map[string]int64)
-                               valueMap[val] = s.Value[0]
-                               tagMap[key] = valueMap
+                               valueMap[val] += o.SampleValue(s.Value)
                        }
                }
                for key, vals := range s.NumLabel {
+                       unit := o.NumLabelUnits[key]
                        for _, nval := range vals {
-                               val := formatTag(nval, key)
-                               if valueMap, ok := tagMap[key]; ok {
-                                       valueMap[val] = valueMap[val] + s.Value[0]
-                                       continue
+                               val := formatTag(nval, unit)
+                               valueMap, ok := tagMap[key]
+                               if !ok {
+                                       valueMap = make(map[string]int64)
+                                       tagMap[key] = valueMap
                                }
-                               valueMap := make(map[string]int64)
-                               valueMap[val] = s.Value[0]
-                               tagMap[key] = valueMap
+                               valueMap[val] += o.SampleValue(s.Value)
                        }
                }
        }
@@ -644,6 +689,7 @@ func printTags(w io.Writer, rpt *Report) error {
        for key := range tagMap {
                tagKeys = append(tagKeys, &graph.Tag{Name: key})
        }
+       tabw := tabwriter.NewWriter(w, 0, 0, 1, ' ', tabwriter.AlignRight)
        for _, tagKey := range graph.SortTags(tagKeys, true) {
                var total int64
                key := tagKey.Name
@@ -653,18 +699,19 @@ func printTags(w io.Writer, rpt *Report) error {
                        tags = append(tags, &graph.Tag{Name: t, Flat: c})
                }
 
-               fmt.Fprintf(w, "%s: Total %d\n", key, total)
+               f, u := measurement.Scale(total, o.SampleUnit, o.OutputUnit)
+               fmt.Fprintf(tabw, "%s:\t Total %.1f%s\n", key, f, u)
                for _, t := range graph.SortTags(tags, true) {
+                       f, u := measurement.Scale(t.FlatValue(), o.SampleUnit, o.OutputUnit)
                        if total > 0 {
-                               fmt.Fprintf(w, "  %8d (%s): %s\n", t.FlatValue(),
-                                       percentage(t.FlatValue(), total), t.Name)
+                               fmt.Fprintf(tabw, " \t%.1f%s (%s):\t %s\n", f, u, percentage(t.FlatValue(), total), t.Name)
                        } else {
-                               fmt.Fprintf(w, "  %8d: %s\n", t.FlatValue(), t.Name)
+                               fmt.Fprintf(tabw, " \t%.1f%s:\t %s\n", f, u, t.Name)
                        }
                }
-               fmt.Fprintln(w)
+               fmt.Fprintln(tabw)
        }
-       return nil
+       return tabw.Flush()
 }
 
 // printComments prints all freeform comments in the profile.
@@ -677,16 +724,22 @@ func printComments(w io.Writer, rpt *Report) error {
        return nil
 }
 
-// printText prints a flat text report for a profile.
-func printText(w io.Writer, rpt *Report) error {
+// TextItem holds a single text report entry.
+type TextItem struct {
+       Name                  string
+       InlineLabel           string // Not empty if inlined
+       Flat, Cum             int64  // Raw values
+       FlatFormat, CumFormat string // Formatted values
+}
+
+// TextItems returns a list of text items from the report and a list
+// of labels that describe the report.
+func TextItems(rpt *Report) ([]TextItem, []string) {
        g, origCount, droppedNodes, _ := rpt.newTrimmedGraph()
        rpt.selectOutputUnit(g)
+       labels := reportLabels(rpt, g, origCount, droppedNodes, 0, false)
 
-       fmt.Fprintln(w, strings.Join(reportLabels(rpt, g, origCount, droppedNodes, 0, false), "\n"))
-
-       fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n",
-               "flat", "flat", "sum", "cum", "cum")
-
+       var items []TextItem
        var flatSum int64
        for _, n := range g.Nodes {
                name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue()
@@ -700,22 +753,46 @@ func printText(w io.Writer, rpt *Report) error {
                        }
                }
 
+               var inl string
                if inline {
                        if noinline {
-                               name = name + " (partial-inline)"
+                               inl = "(partial-inline)"
                        } else {
-                               name = name + " (inline)"
+                               inl = "(inline)"
                        }
                }
 
                flatSum += flat
-               fmt.Fprintf(w, "%10s %s %s %10s %s  %s\n",
-                       rpt.formatValue(flat),
-                       percentage(flat, rpt.total),
+               items = append(items, TextItem{
+                       Name:        name,
+                       InlineLabel: inl,
+                       Flat:        flat,
+                       Cum:         cum,
+                       FlatFormat:  rpt.formatValue(flat),
+                       CumFormat:   rpt.formatValue(cum),
+               })
+       }
+       return items, labels
+}
+
+// printText prints a flat text report for a profile.
+func printText(w io.Writer, rpt *Report) error {
+       items, labels := TextItems(rpt)
+       fmt.Fprintln(w, strings.Join(labels, "\n"))
+       fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n",
+               "flat", "flat", "sum", "cum", "cum")
+       var flatSum int64
+       for _, item := range items {
+               inl := item.InlineLabel
+               if inl != "" {
+                       inl = " " + inl
+               }
+               flatSum += item.Flat
+               fmt.Fprintf(w, "%10s %s %s %10s %s  %s%s\n",
+                       item.FlatFormat, percentage(item.Flat, rpt.total),
                        percentage(flatSum, rpt.total),
-                       rpt.formatValue(cum),
-                       percentage(cum, rpt.total),
-                       name)
+                       item.CumFormat, percentage(item.Cum, rpt.total),
+                       item.Name, inl)
        }
        return nil
 }
@@ -749,6 +826,20 @@ func printTraces(w io.Writer, rpt *Report) error {
                }
                sort.Strings(labels)
                fmt.Fprint(w, strings.Join(labels, ""))
+
+               // Print any numeric labels for the sample
+               var numLabels []string
+               for key, vals := range sample.NumLabel {
+                       unit := o.NumLabelUnits[key]
+                       numValues := make([]string, len(vals))
+                       for i, vv := range vals {
+                               numValues[i] = measurement.Label(vv, unit)
+                       }
+                       numLabels = append(numLabels, fmt.Sprintf("%10s:  %s\n", key, strings.Join(numValues, " ")))
+               }
+               sort.Strings(numLabels)
+               fmt.Fprint(w, strings.Join(numLabels, ""))
+
                var d, v int64
                v = o.SampleValue(sample.Value)
                if o.SampleMeanDivisor != nil {
@@ -969,24 +1060,25 @@ func printTree(w io.Writer, rpt *Report) error {
        return nil
 }
 
-// printDOT prints an annotated callgraph in DOT format.
-func printDOT(w io.Writer, rpt *Report) error {
+// GetDOT returns a graph suitable for dot processing along with some
+// configuration information.
+func GetDOT(rpt *Report) (*graph.Graph, *graph.DotConfig) {
        g, origCount, droppedNodes, droppedEdges := rpt.newTrimmedGraph()
        rpt.selectOutputUnit(g)
        labels := reportLabels(rpt, g, origCount, droppedNodes, droppedEdges, true)
 
-       o := rpt.options
-       formatTag := func(v int64, key string) string {
-               return measurement.ScaledLabel(v, key, o.OutputUnit)
-       }
-
        c := &graph.DotConfig{
                Title:       rpt.options.Title,
                Labels:      labels,
                FormatValue: rpt.formatValue,
-               FormatTag:   formatTag,
                Total:       rpt.total,
        }
+       return g, c
+}
+
+// printDOT prints an annotated callgraph in DOT format.
+func printDOT(w io.Writer, rpt *Report) error {
+       g, c := GetDOT(rpt)
        graph.ComposeDot(w, g, &graph.DotAttributes{}, c)
        return nil
 }
@@ -1055,9 +1147,7 @@ func reportLabels(rpt *Report, g *graph.Graph, origCount, droppedNodes, droppedE
 
        var label []string
        if len(rpt.options.ProfileLabels) > 0 {
-               for _, l := range rpt.options.ProfileLabels {
-                       label = append(label, l)
-               }
+               label = append(label, rpt.options.ProfileLabels...)
        } else if fullHeaders || !rpt.options.CompactLabels {
                label = ProfileLabels(rpt)
        }
@@ -1067,6 +1157,11 @@ func reportLabels(rpt *Report, g *graph.Graph, origCount, droppedNodes, droppedE
                flatSum = flatSum + n.FlatValue()
        }
 
+       if len(rpt.options.ActiveFilters) > 0 {
+               activeFilters := legendActiveFilters(rpt.options.ActiveFilters)
+               label = append(label, activeFilters...)
+       }
+
        label = append(label, fmt.Sprintf("Showing nodes accounting for %s, %s of %s total", rpt.formatValue(flatSum), strings.TrimSpace(percentage(flatSum, rpt.total)), rpt.formatValue(rpt.total)))
 
        if rpt.total != 0 {
@@ -1086,6 +1181,18 @@ func reportLabels(rpt *Report, g *graph.Graph, origCount, droppedNodes, droppedE
        return label
 }
 
+func legendActiveFilters(activeFilters []string) []string {
+       legendActiveFilters := make([]string, len(activeFilters)+1)
+       legendActiveFilters[0] = "Active filters:"
+       for i, s := range activeFilters {
+               if len(s) > 80 {
+                       s = s[:80] + "…"
+               }
+               legendActiveFilters[i+1] = "   " + s
+       }
+       return legendActiveFilters
+}
+
 func genLabel(d int, n, l, f string) string {
        if d > 1 {
                n = n + "s"
@@ -1159,6 +1266,9 @@ type Report struct {
        formatValue func(int64) string
 }
 
+// Total returns the total number of samples in a report.
+func (rpt *Report) Total() int64 { return rpt.total }
+
 func abs64(i int64) int64 {
        if i < 0 {
                return -i
index 28cf6b4ce38e510de72cb623a3956a038a1ac4bb..e05cf5ad08dfca1f3a1c48cb1c5cf7ec35f7d2b3 100644 (file)
@@ -264,3 +264,24 @@ func TestFunctionMap(t *testing.T) {
                }
        }
 }
+
+func TestLegendActiveFilters(t *testing.T) {
+       activeFilterInput := []string{
+               "focus=123|456|789|101112|131415|161718|192021|222324|252627|282930|313233|343536|363738|acbdefghijklmnop",
+               "show=short filter",
+       }
+       expectedLegendActiveFilter := []string{
+               "Active filters:",
+               "   focus=123|456|789|101112|131415|161718|192021|222324|252627|282930|313233|343536…",
+               "   show=short filter",
+       }
+       legendActiveFilter := legendActiveFilters(activeFilterInput)
+       if len(legendActiveFilter) != len(expectedLegendActiveFilter) {
+               t.Errorf("wanted length %v got length %v", len(expectedLegendActiveFilter), len(legendActiveFilter))
+       }
+       for i := range legendActiveFilter {
+               if legendActiveFilter[i] != expectedLegendActiveFilter[i] {
+                       t.Errorf("%d: want \"%v\", got \"%v\"", i, expectedLegendActiveFilter[i], legendActiveFilter[i])
+               }
+       }
+}
index f5e3b6b9d71ae171df4c14c759d9e6b69249b77d..ce82ae55c474c24e67eb0c8eb4e191cf13f9ee49 100644 (file)
@@ -62,6 +62,7 @@ func printSource(w io.Writer, rpt *Report) error {
                }
                sourcePath = wd
        }
+       reader := newSourceReader(sourcePath)
 
        fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total))
        for _, fn := range functions {
@@ -94,7 +95,7 @@ func printSource(w io.Writer, rpt *Report) error {
                        fns := fileNodes[filename]
                        flatSum, cumSum := fns.Sum()
 
-                       fnodes, _, err := getSourceFromFile(filename, sourcePath, fns, 0, 0)
+                       fnodes, _, err := getSourceFromFile(filename, reader, fns, 0, 0)
                        fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, filename)
                        fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n",
                                rpt.formatValue(flatSum), rpt.formatValue(cumSum),
@@ -116,6 +117,16 @@ func printSource(w io.Writer, rpt *Report) error {
 // printWebSource prints an annotated source listing, include all
 // functions with samples that match the regexp rpt.options.symbol.
 func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
+       printHeader(w, rpt)
+       if err := PrintWebList(w, rpt, obj, -1); err != nil {
+               return err
+       }
+       printPageClosing(w)
+       return nil
+}
+
+// PrintWebList prints annotated source listing of rpt to w.
+func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) error {
        o := rpt.options
        g := rpt.newGraph(nil)
 
@@ -134,6 +145,7 @@ func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
                }
                sourcePath = wd
        }
+       reader := newSourceReader(sourcePath)
 
        type fileFunction struct {
                fileName, functionName string
@@ -167,7 +179,7 @@ func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
        }
 
        if len(fileNodes) == 0 {
-               return fmt.Errorf("No source information for %s\n", o.Symbol.String())
+               return fmt.Errorf("No source information for %s", o.Symbol.String())
        }
 
        sourceFiles := make(graph.Nodes, 0, len(fileNodes))
@@ -176,10 +188,18 @@ func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
                sNode.Flat, sNode.Cum = nodes.Sum()
                sourceFiles = append(sourceFiles, &sNode)
        }
-       sourceFiles.Sort(graph.FileOrder)
+
+       // Limit number of files printed?
+       if maxFiles < 0 {
+               sourceFiles.Sort(graph.FileOrder)
+       } else {
+               sourceFiles.Sort(graph.FlatNameOrder)
+               if maxFiles < len(sourceFiles) {
+                       sourceFiles = sourceFiles[:maxFiles]
+               }
+       }
 
        // Print each file associated with this function.
-       printHeader(w, rpt)
        for _, n := range sourceFiles {
                ff := fileFunction{n.Info.File, n.Info.Name}
                fns := fileNodes[ff]
@@ -187,18 +207,17 @@ func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
                asm := assemblyPerSourceLine(symbols, fns, ff.fileName, obj)
                start, end := sourceCoordinates(asm)
 
-               fnodes, path, err := getSourceFromFile(ff.fileName, sourcePath, fns, start, end)
+               fnodes, path, err := getSourceFromFile(ff.fileName, reader, fns, start, end)
                if err != nil {
                        fnodes, path = getMissingFunctionSource(ff.fileName, asm, start, end)
                }
 
                printFunctionHeader(w, ff.functionName, path, n.Flat, n.Cum, rpt)
                for _, fn := range fnodes {
-                       printFunctionSourceLine(w, fn, asm[fn.Info.Lineno], rpt)
+                       printFunctionSourceLine(w, fn, asm[fn.Info.Lineno], reader, rpt)
                }
                printFunctionClosing(w)
        }
-       printPageClosing(w)
        return nil
 }
 
@@ -236,11 +255,41 @@ func assemblyPerSourceLine(objSyms []*objSymbol, rs graph.Nodes, src string, obj
        srcBase := filepath.Base(src)
        anodes := annotateAssembly(insts, rs, o.base)
        var lineno = 0
+       var prevline = 0
        for _, an := range anodes {
-               if filepath.Base(an.file) == srcBase {
+               // Do not rely solely on the line number produced by Disasm
+               // since it is not what we want in the presence of inlining.
+               //
+               // E.g., suppose we are printing source code for F and this
+               // instruction is from H where F called G called H and both
+               // of those calls were inlined.  We want to use the line
+               // number from F, not from H (which is what Disasm gives us).
+               //
+               // So find the outer-most linenumber in the source file.
+               found := false
+               if frames, err := o.file.SourceLine(an.address + o.base); err == nil {
+                       for i := len(frames) - 1; i >= 0; i-- {
+                               if filepath.Base(frames[i].File) == srcBase {
+                                       for j := i - 1; j >= 0; j-- {
+                                               an.inlineCalls = append(an.inlineCalls, callID{frames[j].File, frames[j].Line})
+                                       }
+                                       lineno = frames[i].Line
+                                       found = true
+                                       break
+                               }
+                       }
+               }
+               if !found && filepath.Base(an.file) == srcBase {
                        lineno = an.line
                }
+
                if lineno != 0 {
+                       if lineno != prevline {
+                               // This instruction starts a new block
+                               // of contiguous instructions on this line.
+                               an.startsBlock = true
+                       }
+                       prevline = lineno
                        assembly[lineno] = append(assembly[lineno], an)
                }
        }
@@ -265,7 +314,15 @@ func findMatchingSymbol(objSyms []*objSymbol, ns graph.Nodes) *objSymbol {
 
 // printHeader prints the page header for a weblist report.
 func printHeader(w io.Writer, rpt *Report) {
-       fmt.Fprintln(w, weblistPageHeader)
+       fmt.Fprintln(w, `
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Pprof listing</title>`)
+       fmt.Fprintln(w, weblistPageCSS)
+       fmt.Fprintln(w, weblistPageScript)
+       fmt.Fprint(w, "</head>\n<body>\n\n")
 
        var labels []string
        for _, l := range ProfileLabels(rpt) {
@@ -290,30 +347,33 @@ func printFunctionHeader(w io.Writer, name, path string, flatSum, cumSum int64,
 }
 
 // printFunctionSourceLine prints a source line and the corresponding assembly.
-func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyInstruction, rpt *Report) {
+func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyInstruction, reader *sourceReader, rpt *Report) {
        if len(assembly) == 0 {
                fmt.Fprintf(w,
-                       "<span class=line> %6d</span> <span class=nop>  %10s %10s %s </span>\n",
+                       "<span class=line> %6d</span> <span class=nop>  %10s %10s %8s  %s </span>\n",
                        fn.Info.Lineno,
                        valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt),
-                       template.HTMLEscapeString(fn.Info.Name))
+                       "", template.HTMLEscapeString(fn.Info.Name))
                return
        }
 
        fmt.Fprintf(w,
-               "<span class=line> %6d</span> <span class=deadsrc>  %10s %10s %s </span>",
+               "<span class=line> %6d</span> <span class=deadsrc>  %10s %10s %8s  %s </span>",
                fn.Info.Lineno,
                valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt),
-               template.HTMLEscapeString(fn.Info.Name))
+               "", template.HTMLEscapeString(fn.Info.Name))
+       srcIndent := indentation(fn.Info.Name)
        fmt.Fprint(w, "<span class=asm>")
-       for _, an := range assembly {
+       var curCalls []callID
+       for i, an := range assembly {
+               if an.startsBlock && i != 0 {
+                       // Insert a separator between discontiguous blocks.
+                       fmt.Fprintf(w, " %8s %28s\n", "", "⋮")
+               }
+
                var fileline string
-               class := "disasmloc"
                if an.file != "" {
                        fileline = fmt.Sprintf("%s:%d", template.HTMLEscapeString(an.file), an.line)
-                       if an.line != fn.Info.Lineno {
-                               class = "unimportant"
-                       }
                }
                flat, cum := an.flat, an.cum
                if an.flatDiv != 0 {
@@ -322,11 +382,30 @@ func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyIns
                if an.cumDiv != 0 {
                        cum = cum / an.cumDiv
                }
-               fmt.Fprintf(w, " %8s %10s %10s %8x: %-48s <span class=%s>%s</span>\n", "",
-                       valueOrDot(flat, rpt), valueOrDot(cum, rpt),
-                       an.address,
-                       template.HTMLEscapeString(an.instruction),
-                       class,
+
+               // Print inlined call context.
+               for j, c := range an.inlineCalls {
+                       if j < len(curCalls) && curCalls[j] == c {
+                               // Skip if same as previous instruction.
+                               continue
+                       }
+                       curCalls = nil
+                       fname := trimPath(c.file)
+                       fline, ok := reader.line(fname, c.line)
+                       if !ok {
+                               fline = ""
+                       }
+                       text := strings.Repeat(" ", srcIndent+4+4*j) + strings.TrimSpace(fline)
+                       fmt.Fprintf(w, " %8s %10s %10s %8s  <span class=inlinesrc>%s</span> <span class=unimportant>%s:%d</span>\n",
+                               "", "", "", "",
+                               template.HTMLEscapeString(fmt.Sprintf("%-80s", text)),
+                               template.HTMLEscapeString(filepath.Base(fname)), c.line)
+               }
+               curCalls = an.inlineCalls
+               text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction
+               fmt.Fprintf(w, " %8s %10s %10s %8x: %s <span class=unimportant>%s</span>\n",
+                       "", valueOrDot(flat, rpt), valueOrDot(cum, rpt), an.address,
+                       template.HTMLEscapeString(fmt.Sprintf("%-80s", text)),
                        template.HTMLEscapeString(fileline))
        }
        fmt.Fprintln(w, "</span>")
@@ -345,14 +424,10 @@ func printPageClosing(w io.Writer) {
 // getSourceFromFile collects the sources of a function from a source
 // file and annotates it with the samples in fns. Returns the sources
 // as nodes, using the info.name field to hold the source code.
-func getSourceFromFile(file, sourcePath string, fns graph.Nodes, start, end int) (graph.Nodes, string, error) {
+func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) {
        file = trimPath(file)
-       f, err := openSourceFile(file, sourcePath)
-       if err != nil {
-               return nil, file, err
-       }
-
        lineNodes := make(map[int]graph.Nodes)
+
        // Collect source coordinates from profile.
        const margin = 5 // Lines before first/after last sample.
        if start == 0 {
@@ -382,36 +457,28 @@ func getSourceFromFile(file, sourcePath string, fns graph.Nodes, start, end int)
                }
                lineNodes[lineno] = append(lineNodes[lineno], n)
        }
+       if start < 1 {
+               start = 1
+       }
 
        var src graph.Nodes
-       buf := bufio.NewReader(f)
-       lineno := 1
-       for {
-               line, err := buf.ReadString('\n')
-               if err != nil {
-                       if err != io.EOF {
-                               return nil, file, err
-                       }
-                       if line == "" {
-                               break
-                       }
-               }
-               if lineno >= start {
-                       flat, cum := lineNodes[lineno].Sum()
-
-                       src = append(src, &graph.Node{
-                               Info: graph.NodeInfo{
-                                       Name:   strings.TrimRight(line, "\n"),
-                                       Lineno: lineno,
-                               },
-                               Flat: flat,
-                               Cum:  cum,
-                       })
-               }
-               lineno++
-               if lineno > end {
+       for lineno := start; lineno <= end; lineno++ {
+               line, ok := reader.line(file, lineno)
+               if !ok {
                        break
                }
+               flat, cum := lineNodes[lineno].Sum()
+               src = append(src, &graph.Node{
+                       Info: graph.NodeInfo{
+                               Name:   strings.TrimRight(line, "\n"),
+                               Lineno: lineno,
+                       },
+                       Flat: flat,
+                       Cum:  cum,
+               })
+       }
+       if err := reader.fileError(file); err != nil {
+               return nil, file, err
        }
        return src, file, nil
 }
@@ -446,6 +513,57 @@ func getMissingFunctionSource(filename string, asm map[int][]assemblyInstruction
        return fnodes, filename
 }
 
+// sourceReader provides access to source code with caching of file contents.
+type sourceReader struct {
+       searchPath string
+
+       // files maps from path name to a list of lines.
+       // files[*][0] is unused since line numbering starts at 1.
+       files map[string][]string
+
+       // errors collects errors encountered per file.  These errors are
+       // consulted before returning out of these module.
+       errors map[string]error
+}
+
+func newSourceReader(searchPath string) *sourceReader {
+       return &sourceReader{
+               searchPath,
+               make(map[string][]string),
+               make(map[string]error),
+       }
+}
+
+func (reader *sourceReader) fileError(path string) error {
+       return reader.errors[path]
+}
+
+func (reader *sourceReader) line(path string, lineno int) (string, bool) {
+       lines, ok := reader.files[path]
+       if !ok {
+               // Read and cache file contents.
+               lines = []string{""} // Skip 0th line
+               f, err := openSourceFile(path, reader.searchPath)
+               if err != nil {
+                       reader.errors[path] = err
+               } else {
+                       s := bufio.NewScanner(f)
+                       for s.Scan() {
+                               lines = append(lines, s.Text())
+                       }
+                       f.Close()
+                       if s.Err() != nil {
+                               reader.errors[path] = err
+                       }
+               }
+               reader.files[path] = lines
+       }
+       if lineno <= 0 || lineno >= len(lines) {
+               return "", false
+       }
+       return lines[lineno], true
+}
+
 // openSourceFile opens a source file from a name encoded in a
 // profile. File names in a profile after often relative paths, so
 // search them in each of the paths in searchPath (or CWD by default),
@@ -492,3 +610,20 @@ func trimPath(path string) string {
        }
        return path
 }
+
+func indentation(line string) int {
+       column := 0
+       for _, c := range line {
+               if c == ' ' {
+                       column++
+               } else if c == '\t' {
+                       column++
+                       for column%8 != 0 {
+                               column++
+                       }
+               } else {
+                       break
+               }
+       }
+       return column
+}
index 2bb81f20257b3e5e8cf73056ea2282ca84ec4942..02a6d772487108f1b959e92a634c5b6352e3f08d 100644 (file)
 
 package report
 
-const weblistPageHeader = `
-<!DOCTYPE html>
-<html>
-<head>
-<title>Pprof listing</title>
-<style type="text/css">
+import (
+       "html/template"
+)
+
+// AddSourceTemplates adds templates used by PrintWebList to t.
+func AddSourceTemplates(t *template.Template) {
+       template.Must(t.Parse(`{{define "weblistcss"}}` + weblistPageCSS + `{{end}}`))
+       template.Must(t.Parse(`{{define "weblistjs"}}` + weblistPageScript + `{{end}}`))
+}
+
+const weblistPageCSS = `<style type="text/css">
 body {
 font-family: sans-serif;
 }
@@ -30,17 +35,11 @@ h1 {
 .legend {
   font-size: 1.25em;
 }
-.line {
-color: #aaaaaa;
-}
-.nop {
-color: #aaaaaa;
+.line, .nop, .unimportant {
+  color: #aaaaaa;
 }
-.unimportant {
-color: #cccccc;
-}
-.disasmloc {
-color: #000000;
+.inlinesrc {
+  color: #000066;
 }
 .deadsrc {
 cursor: pointer;
@@ -59,8 +58,9 @@ background-color: #eeeeee;
 color: #008800;
 display: none;
 }
-</style>
-<script type="text/javascript">
+</style>`
+
+const weblistPageScript = `<script type="text/javascript">
 function pprof_toggle_asm(e) {
   var target;
   if (!e) e = window.event;
@@ -76,10 +76,7 @@ function pprof_toggle_asm(e) {
     }
   }
 }
-</script>
-</head>
-<body>
-`
+</script>`
 
 const weblistPageClosing = `
 </body>
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/report/source_test.go b/src/cmd/vendor/github.com/google/pprof/internal/report/source_test.go
new file mode 100644 (file)
index 0000000..9a2b5a2
--- /dev/null
@@ -0,0 +1,89 @@
+package report
+
+import (
+       "bytes"
+       "os"
+       "path/filepath"
+       "regexp"
+       "runtime"
+       "strings"
+       "testing"
+
+       "github.com/google/pprof/internal/binutils"
+       "github.com/google/pprof/profile"
+)
+
+func TestWebList(t *testing.T) {
+       if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
+               t.Skip("weblist only tested on x86-64 linux")
+       }
+
+       cpu := readProfile(filepath.Join("testdata", "sample.cpu"), t)
+       rpt := New(cpu, &Options{
+               OutputFormat: WebList,
+               Symbol:       regexp.MustCompile("busyLoop"),
+               SampleValue:  func(v []int64) int64 { return v[1] },
+               SampleUnit:   cpu.SampleType[1].Unit,
+       })
+       buf := bytes.NewBuffer(nil)
+       if err := Generate(buf, rpt, &binutils.Binutils{}); err != nil {
+               t.Fatalf("could not generate weblist: %v", err)
+       }
+       output := buf.String()
+
+       for _, expect := range []string{"func busyLoop", "callq", "math.Abs"} {
+               if !strings.Contains(output, expect) {
+                       t.Errorf("weblist output does not contain '%s':\n%s", expect, output)
+               }
+       }
+}
+
+func TestIndentation(t *testing.T) {
+       for _, c := range []struct {
+               str        string
+               wantIndent int
+       }{
+               {"", 0},
+               {"foobar", 0},
+               {"  foo", 2},
+               {"\tfoo", 8},
+               {"\t foo", 9},
+               {"  \tfoo", 8},
+               {"       \tfoo", 8},
+               {"        \tfoo", 16},
+       } {
+               if n := indentation(c.str); n != c.wantIndent {
+                       t.Errorf("indentation(%v): got %d, want %d", c.str, n, c.wantIndent)
+               }
+       }
+}
+
+func readProfile(fname string, t *testing.T) *profile.Profile {
+       file, err := os.Open(fname)
+       if err != nil {
+               t.Fatalf("%s: could not open profile: %v", fname, err)
+       }
+       defer file.Close()
+       p, err := profile.Parse(file)
+       if err != nil {
+               t.Fatalf("%s: could not parse profile: %v", fname, err)
+       }
+
+       // Fix file names so they do not include absolute path names.
+       fix := func(s string) string {
+               const testdir = "/internal/report/"
+               pos := strings.Index(s, testdir)
+               if pos == -1 {
+                       return s
+               }
+               return s[pos+len(testdir):]
+       }
+       for _, m := range p.Mapping {
+               m.File = fix(m.File)
+       }
+       for _, f := range p.Function {
+               f.Filename = fix(f.Filename)
+       }
+
+       return p
+}
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/report/testdata/README.md b/src/cmd/vendor/github.com/google/pprof/internal/report/testdata/README.md
new file mode 100644 (file)
index 0000000..2b60fcc
--- /dev/null
@@ -0,0 +1,10 @@
+sample/ contains a sample program that can be profiled.
+sample.bin is its x86-64 binary.
+sample.cpu is a profile generated by sample.bin.
+
+To update the binary and profile:
+
+```shell
+go build -o sample.bin ./sample
+./sample.bin -cpuprofile sample.cpu
+```
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/report/testdata/sample.bin b/src/cmd/vendor/github.com/google/pprof/internal/report/testdata/sample.bin
new file mode 100755 (executable)
index 0000000..25929e6
Binary files /dev/null and b/src/cmd/vendor/github.com/google/pprof/internal/report/testdata/sample.bin differ
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/report/testdata/sample.cpu b/src/cmd/vendor/github.com/google/pprof/internal/report/testdata/sample.cpu
new file mode 100644 (file)
index 0000000..50eea72
Binary files /dev/null and b/src/cmd/vendor/github.com/google/pprof/internal/report/testdata/sample.cpu differ
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/report/testdata/sample/sample.go b/src/cmd/vendor/github.com/google/pprof/internal/report/testdata/sample/sample.go
new file mode 100644 (file)
index 0000000..3c812dd
--- /dev/null
@@ -0,0 +1,41 @@
+// sample program that is used to produce some of the files in
+// pprof/internal/report/testdata.
+package main
+
+import (
+       "flag"
+       "fmt"
+       "log"
+       "math"
+       "os"
+       "runtime/pprof"
+)
+
+var cpuProfile = flag.String("cpuprofile", "", "where to write cpu profile")
+
+func main() {
+       flag.Parse()
+       f, err := os.Create(*cpuProfile)
+       if err != nil {
+               log.Fatal("could not create CPU profile: ", err)
+       }
+       if err := pprof.StartCPUProfile(f); err != nil {
+               log.Fatal("could not start CPU profile: ", err)
+       }
+       defer pprof.StopCPUProfile()
+       busyLoop()
+}
+
+func busyLoop() {
+       m := make(map[int]int)
+       for i := 0; i < 1000000; i++ {
+               m[i] = i + 10
+       }
+       var sum float64
+       for i := 0; i < 100; i++ {
+               for _, v := range m {
+                       sum += math.Abs(float64(v))
+               }
+       }
+       fmt.Println("Sum", sum)
+}
index 19746a4bafd3bcdc62b49b15ab447b376954c2c2..b67ca168c5c3d4cd053fae5c1d5658100f82a06a 100644 (file)
@@ -1,13 +1,13 @@
 digraph "unnamed" {
 node [style=filled fillcolor="#f8f8f8"]
 subgraph cluster_L { "Duration: 10s, Total samples = 11111 " [shape=box fontsize=16 label="Duration: 10s, Total samples = 11111 \lShowing nodes accounting for 11111, 100% of 11111 total\l"] }
-N1 [label="tee\nsource2:8\n10000 (90.00%)" fontsize=24 shape=box tooltip="tee testdata/source2:8 (10000)" color="#b20500" fillcolor="#edd6d5"]
-N2 [label="main\nsource1:2\n1 (0.009%)\nof 11111 (100%)" fontsize=9 shape=box tooltip="main testdata/source1:2 (11111)" color="#b20000" fillcolor="#edd5d5"]
-N3 [label="tee\nsource2:2\n1000 (9.00%)\nof 11000 (99.00%)" fontsize=14 shape=box tooltip="tee testdata/source2:2 (11000)" color="#b20000" fillcolor="#edd5d5"]
-N4 [label="tee\nsource2:8\n100 (0.9%)" fontsize=10 shape=box tooltip="tee testdata/source2:8 (100)" color="#b2b0aa" fillcolor="#edecec"]
-N5 [label="bar\nsource1:10\n10 (0.09%)" fontsize=9 shape=box tooltip="bar testdata/source1:10 (10)" color="#b2b2b1" fillcolor="#ededed"]
-N6 [label="bar\nsource1:10\n0 of 100 (0.9%)" fontsize=8 shape=box tooltip="bar testdata/source1:10 (100)" color="#b2b0aa" fillcolor="#edecec"]
-N7 [label="foo\nsource1:4\n0 of 10 (0.09%)" fontsize=8 shape=box tooltip="foo testdata/source1:4 (10)" color="#b2b2b1" fillcolor="#ededed"]
+N1 [label="tee\nsource2:8\n10000 (90.00%)" id="node1" fontsize=24 shape=box tooltip="tee testdata/source2:8 (10000)" color="#b20500" fillcolor="#edd6d5"]
+N2 [label="main\nsource1:2\n1 (0.009%)\nof 11111 (100%)" id="node2" fontsize=9 shape=box tooltip="main testdata/source1:2 (11111)" color="#b20000" fillcolor="#edd5d5"]
+N3 [label="tee\nsource2:2\n1000 (9.00%)\nof 11000 (99.00%)" id="node3" fontsize=14 shape=box tooltip="tee testdata/source2:2 (11000)" color="#b20000" fillcolor="#edd5d5"]
+N4 [label="tee\nsource2:8\n100 (0.9%)" id="node4" fontsize=10 shape=box tooltip="tee testdata/source2:8 (100)" color="#b2b0aa" fillcolor="#edecec"]
+N5 [label="bar\nsource1:10\n10 (0.09%)" id="node5" fontsize=9 shape=box tooltip="bar testdata/source1:10 (10)" color="#b2b2b1" fillcolor="#ededed"]
+N6 [label="bar\nsource1:10\n0 of 100 (0.9%)" id="node6" fontsize=8 shape=box tooltip="bar testdata/source1:10 (100)" color="#b2b0aa" fillcolor="#edecec"]
+N7 [label="foo\nsource1:4\n0 of 10 (0.09%)" id="node7" fontsize=8 shape=box tooltip="foo testdata/source1:4 (10)" color="#b2b2b1" fillcolor="#ededed"]
 N2 -> N3 [label=" 11000" weight=100 penwidth=5 color="#b20000" tooltip="main testdata/source1:2 -> tee testdata/source2:2 (11000)" labeltooltip="main testdata/source1:2 -> tee testdata/source2:2 (11000)"]
 N3 -> N1 [label=" 10000" weight=91 penwidth=5 color="#b20500" tooltip="tee testdata/source2:2 -> tee testdata/source2:8 (10000)" labeltooltip="tee testdata/source2:2 -> tee testdata/source2:8 (10000)"]
 N6 -> N4 [label=" 100" color="#b2b0aa" tooltip="bar testdata/source1:10 -> tee testdata/source2:8 (100)" labeltooltip="bar testdata/source1:10 -> tee testdata/source2:8 (100)"]
index 2c1c729ddfe4c239473bdfc5e8f25ad0f26764ef..727b7e8c56c55099d53dd9b771ea935bae767b33 100644 (file)
@@ -18,6 +18,7 @@
 package symbolizer
 
 import (
+       "crypto/tls"
        "fmt"
        "io/ioutil"
        "net/http"
@@ -41,21 +42,26 @@ type Symbolizer struct {
 // test taps for dependency injection
 var symbolzSymbolize = symbolz.Symbolize
 var localSymbolize = doLocalSymbolize
+var demangleFunction = Demangle
 
 // Symbolize attempts to symbolize profile p. First uses binutils on
 // local binaries; if the source is a URL it attempts to get any
 // missed entries using symbolz.
 func (s *Symbolizer) Symbolize(mode string, sources plugin.MappingSources, p *profile.Profile) error {
-       remote, local, force, demanglerMode := true, true, false, ""
+       remote, local, fast, force, demanglerMode := true, true, false, false, ""
        for _, o := range strings.Split(strings.ToLower(mode), ":") {
                switch o {
+               case "":
+                       continue
                case "none", "no":
                        return nil
-               case "local", "fastlocal":
+               case "local":
                        remote, local = false, true
+               case "fastlocal":
+                       remote, local, fast = false, true, true
                case "remote":
                        remote, local = true, false
-               case "", "force":
+               case "force":
                        force = true
                default:
                        switch d := strings.TrimPrefix(o, "demangle="); d {
@@ -74,29 +80,48 @@ func (s *Symbolizer) Symbolize(mode string, sources plugin.MappingSources, p *pr
        var err error
        if local {
                // Symbolize locally using binutils.
-               if err = localSymbolize(mode, p, s.Obj, s.UI); err != nil {
+               if err = localSymbolize(p, fast, force, s.Obj, s.UI); err != nil {
                        s.UI.PrintErr("local symbolization: " + err.Error())
                }
        }
        if remote {
-               if err = symbolzSymbolize(sources, postURL, p, s.UI); err != nil {
+               if err = symbolzSymbolize(p, force, sources, postURL, s.UI); err != nil {
                        return err // Ran out of options.
                }
        }
 
-       Demangle(p, force, demanglerMode)
+       demangleFunction(p, force, demanglerMode)
        return nil
 }
 
 // postURL issues a POST to a URL over HTTP.
 func postURL(source, post string) ([]byte, error) {
-       resp, err := http.Post(source, "application/octet-stream", strings.NewReader(post))
+       url, err := url.Parse(source)
+       if err != nil {
+               return nil, err
+       }
+
+       var tlsConfig *tls.Config
+       if url.Scheme == "https+insecure" {
+               tlsConfig = &tls.Config{
+                       InsecureSkipVerify: true,
+               }
+               url.Scheme = "https"
+               source = url.String()
+       }
+
+       client := &http.Client{
+               Transport: &http.Transport{
+                       TLSClientConfig: tlsConfig,
+               },
+       }
+       resp, err := client.Post(source, "application/octet-stream", strings.NewReader(post))
        if err != nil {
                return nil, fmt.Errorf("http post %s: %v", source, err)
        }
        defer resp.Body.Close()
        if resp.StatusCode != http.StatusOK {
-               return nil, statusCodeError(resp)
+               return nil, fmt.Errorf("http post %s: %v", source, statusCodeError(resp))
        }
        return ioutil.ReadAll(resp.Body)
 }
@@ -114,18 +139,10 @@ func statusCodeError(resp *http.Response) error {
 // doLocalSymbolize adds symbol and line number information to all locations
 // in a profile. mode enables some options to control
 // symbolization.
-func doLocalSymbolize(mode string, prof *profile.Profile, obj plugin.ObjTool, ui plugin.UI) error {
-       force := false
-       // Disable some mechanisms based on mode string.
-       for _, o := range strings.Split(strings.ToLower(mode), ":") {
-               switch {
-               case o == "force":
-                       force = true
-               case o == "fastlocal":
-                       if bu, ok := obj.(*binutils.Binutils); ok {
-                               bu.SetFastSymbolization(true)
-                       }
-               default:
+func doLocalSymbolize(prof *profile.Profile, fast, force bool, obj plugin.ObjTool, ui plugin.UI) error {
+       if fast {
+               if bu, ok := obj.(*binutils.Binutils); ok {
+                       bu.SetFastSymbolization(true)
                }
        }
 
index 66cad3eaa10b48ee541b12dd9253f99d887c0c27..956519d37ca00a73fbbd6b41090258459c7f0ea3 100644 (file)
@@ -17,6 +17,7 @@ package symbolizer
 import (
        "fmt"
        "regexp"
+       "sort"
        "strings"
        "testing"
 
@@ -101,9 +102,11 @@ func TestSymbolization(t *testing.T) {
        defer func() {
                symbolzSymbolize = sSym
                localSymbolize = lSym
+               demangleFunction = Demangle
        }()
        symbolzSymbolize = symbolzMock
        localSymbolize = localMock
+       demangleFunction = demangleMock
 
        type testcase struct {
                mode        string
@@ -117,19 +120,35 @@ func TestSymbolization(t *testing.T) {
        for i, tc := range []testcase{
                {
                        "local",
-                       "local=local",
+                       "local=[]",
                },
                {
                        "fastlocal",
-                       "local=fastlocal",
+                       "local=[fast]",
                },
                {
                        "remote",
-                       "symbolz",
+                       "symbolz=[]",
                },
                {
                        "",
-                       "local=:symbolz",
+                       "local=[]:symbolz=[]",
+               },
+               {
+                       "demangle=none",
+                       "demangle=[none]:force:local=[force]:symbolz=[force]",
+               },
+               {
+                       "remote:demangle=full",
+                       "demangle=[full]:force:symbolz=[force]",
+               },
+               {
+                       "local:demangle=templates",
+                       "demangle=[templates]:force:local=[force]",
+               },
+               {
+                       "force:remote",
+                       "force:symbolz=[force]",
                },
        } {
                prof := testProfile.Copy()
@@ -137,23 +156,44 @@ func TestSymbolization(t *testing.T) {
                        t.Errorf("symbolize #%d: %v", i, err)
                        continue
                }
+               sort.Strings(prof.Comments)
                if got, want := strings.Join(prof.Comments, ":"), tc.wantComment; got != want {
-                       t.Errorf("got %s, want %s", got, want)
+                       t.Errorf("%q: got %s, want %s", tc.mode, got, want)
                        continue
                }
        }
 }
 
-func symbolzMock(sources plugin.MappingSources, syms func(string, string) ([]byte, error), p *profile.Profile, ui plugin.UI) error {
-       p.Comments = append(p.Comments, "symbolz")
+func symbolzMock(p *profile.Profile, force bool, sources plugin.MappingSources, syms func(string, string) ([]byte, error), ui plugin.UI) error {
+       var args []string
+       if force {
+               args = append(args, "force")
+       }
+       p.Comments = append(p.Comments, "symbolz=["+strings.Join(args, ",")+"]")
        return nil
 }
 
-func localMock(mode string, p *profile.Profile, obj plugin.ObjTool, ui plugin.UI) error {
-       p.Comments = append(p.Comments, "local="+mode)
+func localMock(p *profile.Profile, fast, force bool, obj plugin.ObjTool, ui plugin.UI) error {
+       var args []string
+       if fast {
+               args = append(args, "fast")
+       }
+       if force {
+               args = append(args, "force")
+       }
+       p.Comments = append(p.Comments, "local=["+strings.Join(args, ",")+"]")
        return nil
 }
 
+func demangleMock(p *profile.Profile, force bool, mode string) {
+       if force {
+               p.Comments = append(p.Comments, "force")
+       }
+       if mode != "" {
+               p.Comments = append(p.Comments, "demangle=["+mode+"]")
+       }
+}
+
 func TestLocalSymbolization(t *testing.T) {
        prof := testProfile.Copy()
 
@@ -165,7 +205,7 @@ func TestLocalSymbolization(t *testing.T) {
        }
 
        b := mockObjTool{}
-       if err := localSymbolize("", prof, b, &proftest.TestUI{T: t}); err != nil {
+       if err := localSymbolize(prof, false, false, b, &proftest.TestUI{T: t}); err != nil {
                t.Fatalf("localSymbolize(): %v", err)
        }
 
@@ -207,11 +247,11 @@ func checkSymbolizedLocation(a uint64, got []profile.Line) error {
 }
 
 var mockAddresses = map[uint64][]plugin.Frame{
-       1000: []plugin.Frame{frame("fun11", "file11.src", 10)},
-       2000: []plugin.Frame{frame("fun21", "file21.src", 20), frame("fun22", "file22.src", 20)},
-       3000: []plugin.Frame{frame("fun31", "file31.src", 30), frame("fun32", "file32.src", 30), frame("fun33", "file33.src", 30)},
-       4000: []plugin.Frame{frame("fun41", "file41.src", 40), frame("fun42", "file42.src", 40), frame("fun43", "file43.src", 40), frame("fun44", "file44.src", 40)},
-       5000: []plugin.Frame{frame("fun51", "file51.src", 50), frame("fun52", "file52.src", 50), frame("fun53", "file53.src", 50), frame("fun54", "file54.src", 50), frame("fun55", "file55.src", 50)},
+       1000: {frame("fun11", "file11.src", 10)},
+       2000: {frame("fun21", "file21.src", 20), frame("fun22", "file22.src", 20)},
+       3000: {frame("fun31", "file31.src", 30), frame("fun32", "file32.src", 30), frame("fun33", "file33.src", 30)},
+       4000: {frame("fun41", "file41.src", 40), frame("fun42", "file42.src", 40), frame("fun43", "file43.src", 40), frame("fun44", "file44.src", 40)},
+       5000: {frame("fun51", "file51.src", 50), frame("fun52", "file52.src", 50), frame("fun53", "file53.src", 50), frame("fun54", "file54.src", 50), frame("fun55", "file55.src", 50)},
 }
 
 func frame(fname, file string, line int) plugin.Frame {
index e84765bb20140726b7a42d7f8d96fbecc82efd73..34c119c4c2fc09ecde29149580bc679b192cc27e 100644 (file)
@@ -36,12 +36,13 @@ var (
 
 // Symbolize symbolizes profile p by parsing data returned by a
 // symbolz handler. syms receives the symbolz query (hex addresses
-// separated by '+') and returns the symbolz output in a string. It
-// symbolizes all locations based on their addresses, regardless of
-// mapping.
-func Symbolize(sources plugin.MappingSources, syms func(string, string) ([]byte, error), p *profile.Profile, ui plugin.UI) error {
+// separated by '+') and returns the symbolz output in a string. If
+// force is false, it will only symbolize locations from mappings
+// not already marked as HasFunctions.
+func Symbolize(p *profile.Profile, force bool, sources plugin.MappingSources, syms func(string, string) ([]byte, error), ui plugin.UI) error {
        for _, m := range p.Mapping {
-               if m.HasFunctions {
+               if !force && m.HasFunctions {
+                       // Only check for HasFunctions as symbolz only populates function names.
                        continue
                }
                mappingSources := sources[m.File]
@@ -65,16 +66,13 @@ func Symbolize(sources plugin.MappingSources, syms func(string, string) ([]byte,
 // symbolz returns the corresponding symbolz source for a profile URL.
 func symbolz(source string) string {
        if url, err := url.Parse(source); err == nil && url.Host != "" {
-               if strings.Contains(url.Path, "/") {
-                       if dir := path.Dir(url.Path); dir == "/debug/pprof" {
-                               // For Go language profile handlers in net/http/pprof package.
-                               url.Path = "/debug/pprof/symbol"
-                       } else {
-                               url.Path = "/symbolz"
-                       }
-                       url.RawQuery = ""
-                       return url.String()
+               if strings.Contains(url.Path, "/debug/pprof/") {
+                       url.Path = path.Clean(url.Path + "/../symbol")
+               } else {
+                       url.Path = "/symbolz"
                }
+               url.RawQuery = ""
+               return url.String()
        }
 
        return ""
@@ -82,7 +80,7 @@ func symbolz(source string) string {
 
 // symbolizeMapping symbolizes locations belonging to a Mapping by querying
 // a symbolz handler. An offset is applied to all addresses to take care of
-// normalization occured for merged Mappings.
+// normalization occurred for merged Mappings.
 func symbolizeMapping(source string, offset int64, syms func(string, string) ([]byte, error), m *profile.Mapping, p *profile.Profile) error {
        // Construct query of addresses to symbolize.
        var a []string
index 641b5ca6b77382997e1179fdf492c0df2f81bee8..270a6198c9128327e81972b6c8001f4ad5abc6e4 100644 (file)
@@ -26,13 +26,14 @@ import (
 
 func TestSymbolzURL(t *testing.T) {
        for try, want := range map[string]string{
-               "http://host:8000/profilez":                        "http://host:8000/symbolz",
-               "http://host:8000/profilez?seconds=5":              "http://host:8000/symbolz",
-               "http://host:8000/profilez?seconds=5&format=proto": "http://host:8000/symbolz",
-               "http://host:8000/heapz?format=legacy":             "http://host:8000/symbolz",
-               "http://host:8000/debug/pprof/profile":             "http://host:8000/debug/pprof/symbol",
-               "http://host:8000/debug/pprof/profile?seconds=10":  "http://host:8000/debug/pprof/symbol",
-               "http://host:8000/debug/pprof/heap":                "http://host:8000/debug/pprof/symbol",
+               "http://host:8000/profilez":                                               "http://host:8000/symbolz",
+               "http://host:8000/profilez?seconds=5":                                     "http://host:8000/symbolz",
+               "http://host:8000/profilez?seconds=5&format=proto":                        "http://host:8000/symbolz",
+               "http://host:8000/heapz?format=legacy":                                    "http://host:8000/symbolz",
+               "http://host:8000/debug/pprof/profile":                                    "http://host:8000/debug/pprof/symbol",
+               "http://host:8000/debug/pprof/profile?seconds=10":                         "http://host:8000/debug/pprof/symbol",
+               "http://host:8000/debug/pprof/heap":                                       "http://host:8000/debug/pprof/symbol",
+               "http://some.host:8080/some/deeper/path/debug/pprof/endpoint?param=value": "http://some.host:8080/some/deeper/path/debug/pprof/symbol",
        } {
                if got := symbolz(try); got != want {
                        t.Errorf(`symbolz(%s)=%s, want "%s"`, try, got, want)
@@ -41,12 +42,49 @@ func TestSymbolzURL(t *testing.T) {
 }
 
 func TestSymbolize(t *testing.T) {
+       s := plugin.MappingSources{
+               "buildid": []struct {
+                       Source string
+                       Start  uint64
+               }{
+                       {Source: "http://localhost:80/profilez"},
+               },
+       }
+
+       for _, hasFunctions := range []bool{false, true} {
+               for _, force := range []bool{false, true} {
+                       p := testProfile(hasFunctions)
+
+                       if err := Symbolize(p, force, s, fetchSymbols, &proftest.TestUI{T: t}); err != nil {
+                               t.Errorf("symbolz: %v", err)
+                               continue
+                       }
+                       var wantSym, wantNoSym []*profile.Location
+                       if force || !hasFunctions {
+                               wantNoSym = p.Location[:1]
+                               wantSym = p.Location[1:]
+                       } else {
+                               wantNoSym = p.Location
+                       }
+
+                       if err := checkSymbolized(wantSym, true); err != nil {
+                               t.Errorf("symbolz hasFns=%v force=%v: %v", hasFunctions, force, err)
+                       }
+                       if err := checkSymbolized(wantNoSym, false); err != nil {
+                               t.Errorf("symbolz hasFns=%v force=%v: %v", hasFunctions, force, err)
+                       }
+               }
+       }
+}
+
+func testProfile(hasFunctions bool) *profile.Profile {
        m := []*profile.Mapping{
                {
-                       ID:      1,
-                       Start:   0x1000,
-                       Limit:   0x5000,
-                       BuildID: "buildid",
+                       ID:           1,
+                       Start:        0x1000,
+                       Limit:        0x5000,
+                       BuildID:      "buildid",
+                       HasFunctions: hasFunctions,
                },
        }
        p := &profile.Profile{
@@ -59,33 +97,25 @@ func TestSymbolize(t *testing.T) {
                Mapping: m,
        }
 
-       s := plugin.MappingSources{
-               "buildid": []struct {
-                       Source string
-                       Start  uint64
-               }{
-                       {Source: "http://localhost:80/profilez"},
-               },
-       }
-
-       if err := Symbolize(s, fetchSymbols, p, &proftest.TestUI{T: t}); err != nil {
-               t.Errorf("symbolz: %v", err)
-       }
-
-       if l := p.Location[0]; len(l.Line) != 0 {
-               t.Errorf("unexpected symbolization for %#x: %v", l.Address, l.Line)
-       }
+       return p
+}
 
-       for _, l := range p.Location[1:] {
-               if len(l.Line) != 1 {
-                       t.Errorf("failed to symbolize %#x", l.Address)
-                       continue
+func checkSymbolized(locs []*profile.Location, wantSymbolized bool) error {
+       for _, loc := range locs {
+               if !wantSymbolized && len(loc.Line) != 0 {
+                       return fmt.Errorf("unexpected symbolization for %#x: %v", loc.Address, loc.Line)
                }
-               address := l.Address - l.Mapping.Start
-               if got, want := l.Line[0].Function.Name, fmt.Sprintf("%#x", address); got != want {
-                       t.Errorf("symbolz %#x, got %s, want %s", address, got, want)
+               if wantSymbolized {
+                       if len(loc.Line) != 1 {
+                               return fmt.Errorf("expected symbolization for %#x: %v", loc.Address, loc.Line)
+                       }
+                       address := loc.Address - loc.Mapping.Start
+                       if got, want := loc.Line[0].Function.Name, fmt.Sprintf("%#x", address); got != want {
+                               return fmt.Errorf("symbolz %#x, got %s, want %s", address, got, want)
+                       }
                }
        }
+       return nil
 }
 
 func fetchSymbols(source, post string) ([]byte, error) {
index c64083a4009a8efbde5c66b64697b11665c847b5..622319484a6a27d88e359695bdf915d301762a13 100644 (file)
@@ -59,12 +59,19 @@ func (p *Profile) preEncode() {
                }
                sort.Strings(numKeys)
                for _, k := range numKeys {
+                       keyX := addString(strings, k)
                        vs := s.NumLabel[k]
-                       for _, v := range vs {
+                       units := s.NumUnit[k]
+                       for i, v := range vs {
+                               var unitX int64
+                               if len(units) != 0 {
+                                       unitX = addString(strings, units[i])
+                               }
                                s.labelX = append(s.labelX,
                                        label{
-                                               keyX: addString(strings, k),
-                                               numX: v,
+                                               keyX:  keyX,
+                                               numX:  v,
+                                               unitX: unitX,
                                        },
                                )
                        }
@@ -289,6 +296,7 @@ func (p *Profile) postDecode() error {
        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)
@@ -296,6 +304,14 @@ func (p *Profile) postDecode() error {
                                value, err = getString(p.stringTable, &l.strX, err)
                                labels[key] = append(labels[key], value)
                        } else if l.numX != 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)
                        }
                }
@@ -304,6 +320,12 @@ func (p *Profile) postDecode() error {
                }
                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.Location = make([]*Location, len(s.locationIDX))
                for i, lid := range s.locationIDX {
@@ -340,6 +362,15 @@ func (p *Profile) postDecode() error {
        return err
 }
 
+// padStringArray pads arr with enough empty strings to make arr
+// length l when arr's length is less than l.
+func padStringArray(arr []string, l int) []string {
+       if l <= len(arr) {
+               return arr
+       }
+       return append(arr, make([]string, l-len(arr))...)
+}
+
 func (p *ValueType) decoder() []decoder {
        return valueTypeDecoder
 }
@@ -392,6 +423,7 @@ func (p label) encode(b *buffer) {
        encodeInt64Opt(b, 1, p.keyX)
        encodeInt64Opt(b, 2, p.strX)
        encodeInt64Opt(b, 3, p.numX)
+       encodeInt64Opt(b, 4, p.unitX)
 }
 
 var labelDecoder = []decoder{
@@ -402,6 +434,8 @@ var labelDecoder = []decoder{
        func(b *buffer, m message) error { return decodeInt64(b, &m.(*label).strX) },
        // optional int64 num = 3
        func(b *buffer, m message) error { return decodeInt64(b, &m.(*label).numX) },
+       // optional int64 num = 4
+       func(b *buffer, m message) error { return decodeInt64(b, &m.(*label).unitX) },
 }
 
 func (p *Mapping) decoder() []decoder {
index 85361e879954febd59ee74679afc459e369c071c..f857fdf8f86e4e7b356393c4d4e0d80d9b36cef2 100644 (file)
@@ -41,10 +41,11 @@ func (p *Profile) FilterSamplesByName(focus, ignore, hide, show *regexp.Regexp)
                        }
                }
                if show != nil {
-                       hnm = true
                        l.Line = l.matchedLines(show)
                        if len(l.Line) == 0 {
                                hidden[l.ID] = true
+                       } else {
+                               hnm = true
                        }
                }
        }
index 7b40f5d24c69c56d1053d395ac44c3723477a592..06322e5d2c89116b57fe6726d5d8e92c45404219 100644 (file)
@@ -212,7 +212,10 @@ func parseJavaSamples(pType string, b []byte, p *Profile) ([]byte, map[uint64]*L
                        switch pType {
                        case "heap":
                                const javaHeapzSamplingRate = 524288 // 512K
-                               s.NumLabel = map[string][]int64{"bytes": []int64{s.Value[1] / s.Value[0]}}
+                               if s.Value[0] == 0 {
+                                       return nil, nil, fmt.Errorf("parsing sample %s: second value must be non-zero", line)
+                               }
+                               s.NumLabel = map[string][]int64{"bytes": {s.Value[1] / s.Value[0]}}
                                s.Value[0], s.Value[1] = scaleHeapSample(s.Value[0], s.Value[1], javaHeapzSamplingRate)
                        case "contention":
                                if period := p.Period; period != 0 {
index 2e9c2cd8af857d380fbd0282f16a3e6c500968a3..e00829cc3434e6138d0a51c29dfda9513e78b129 100644 (file)
@@ -85,6 +85,41 @@ func Merge(srcs []*Profile) (*Profile, error) {
        return p, nil
 }
 
+// Normalize normalizes the source profile by multiplying each value in profile by the
+// ratio of the sum of the base profile's values of that sample type to the sum of the
+// source profile's value of that sample type.
+func (p *Profile) Normalize(pb *Profile) error {
+
+       if err := p.compatible(pb); err != nil {
+               return err
+       }
+
+       baseVals := make([]int64, len(p.SampleType))
+       for _, s := range pb.Sample {
+               for i, v := range s.Value {
+                       baseVals[i] += v
+               }
+       }
+
+       srcVals := make([]int64, len(p.SampleType))
+       for _, s := range p.Sample {
+               for i, v := range s.Value {
+                       srcVals[i] += v
+               }
+       }
+
+       normScale := make([]float64, len(baseVals))
+       for i := range baseVals {
+               if srcVals[i] == 0 {
+                       normScale[i] = 0.0
+               } else {
+                       normScale[i] = float64(baseVals[i]) / float64(srcVals[i])
+               }
+       }
+       p.ScaleN(normScale)
+       return nil
+}
+
 func isZeroSample(s *Sample) bool {
        for _, v := range s.Value {
                if v != 0 {
@@ -120,6 +155,7 @@ func (pm *profileMerger) mapSample(src *Sample) *Sample {
                Value:    make([]int64, len(src.Value)),
                Label:    make(map[string][]string, len(src.Label)),
                NumLabel: make(map[string][]int64, len(src.NumLabel)),
+               NumUnit:  make(map[string][]string, len(src.NumLabel)),
        }
        for i, l := range src.Location {
                s.Location[i] = pm.mapLocation(l)
@@ -130,9 +166,13 @@ func (pm *profileMerger) mapSample(src *Sample) *Sample {
                s.Label[k] = vv
        }
        for k, v := range src.NumLabel {
+               u := src.NumUnit[k]
                vv := make([]int64, len(v))
+               uu := make([]string, len(u))
                copy(vv, v)
+               copy(uu, u)
                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
@@ -165,7 +205,7 @@ func (sample *Sample) key() sampleKey {
 
        numlabels := make([]string, 0, len(sample.NumLabel))
        for k, v := range sample.NumLabel {
-               numlabels = append(numlabels, fmt.Sprintf("%q%x", k, v))
+               numlabels = append(numlabels, fmt.Sprintf("%q%x%x", k, v, sample.NumUnit[k]))
        }
        sort.Strings(numlabels)
 
@@ -432,7 +472,6 @@ func (p *Profile) compatible(pb *Profile) error {
                        return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType)
                }
        }
-
        return nil
 }
 
index fb3d4fd4fbc0af8b8bc2cd595326cfa61e33441f..a0f53efe3ec091e8e322427b1aeae5d45a37e1e5 100644 (file)
@@ -26,6 +26,7 @@ import (
        "regexp"
        "sort"
        "strings"
+       "sync"
        "time"
 )
 
@@ -47,6 +48,10 @@ type Profile struct {
        PeriodType    *ValueType
        Period        int64
 
+       // The following fields are modified during encoding and copying,
+       // so are protected by a Mutex.
+       encodeMu sync.Mutex
+
        commentX           []int64
        dropFramesX        int64
        keepFramesX        int64
@@ -69,6 +74,7 @@ type Sample struct {
        Value    []int64
        Label    map[string][]string
        NumLabel map[string][]int64
+       NumUnit  map[string][]string
 
        locationIDX []uint64
        labelX      []label
@@ -80,6 +86,8 @@ type label struct {
        // Exactly one of the two following values must be set
        strX int64
        numX int64 // Integer value for this label
+       // can be set if numX has value
+       unitX int64
 }
 
 // Mapping corresponds to Profile.Mapping
@@ -296,21 +304,25 @@ func (p *Profile) updateLocationMapping(from, to *Mapping) {
        }
 }
 
-// Write writes the profile as a gzip-compressed marshaled protobuf.
-func (p *Profile) Write(w io.Writer) error {
+func serialize(p *Profile) []byte {
+       p.encodeMu.Lock()
        p.preEncode()
        b := marshal(p)
+       p.encodeMu.Unlock()
+       return b
+}
+
+// Write writes the profile as a gzip-compressed marshaled protobuf.
+func (p *Profile) Write(w io.Writer) error {
        zw := gzip.NewWriter(w)
        defer zw.Close()
-       _, err := zw.Write(b)
+       _, err := zw.Write(serialize(p))
        return err
 }
 
 // WriteUncompressed writes the profile as a marshaled protobuf.
 func (p *Profile) WriteUncompressed(w io.Writer) error {
-       p.preEncode()
-       b := marshal(p)
-       _, err := w.Write(b)
+       _, err := w.Write(serialize(p))
        return err
 }
 
@@ -325,8 +337,11 @@ func (p *Profile) CheckValid() error {
                return fmt.Errorf("missing sample type information")
        }
        for _, s := range p.Sample {
+               if s == nil {
+                       return fmt.Errorf("profile has nil sample")
+               }
                if len(s.Value) != sampleLen {
-                       return fmt.Errorf("mismatch: sample has: %d values vs. %d types", len(s.Value), len(p.SampleType))
+                       return fmt.Errorf("mismatch: sample has %d values vs. %d types", len(s.Value), len(p.SampleType))
                }
                for _, l := range s.Location {
                        if l == nil {
@@ -339,6 +354,9 @@ func (p *Profile) CheckValid() error {
        // Check that there are no duplicate ids
        mappings := make(map[uint64]*Mapping, len(p.Mapping))
        for _, m := range p.Mapping {
+               if m == nil {
+                       return fmt.Errorf("profile has nil mapping")
+               }
                if m.ID == 0 {
                        return fmt.Errorf("found mapping with reserved ID=0")
                }
@@ -349,6 +367,9 @@ func (p *Profile) CheckValid() error {
        }
        functions := make(map[uint64]*Function, len(p.Function))
        for _, f := range p.Function {
+               if f == nil {
+                       return fmt.Errorf("profile has nil function")
+               }
                if f.ID == 0 {
                        return fmt.Errorf("found function with reserved ID=0")
                }
@@ -359,6 +380,9 @@ func (p *Profile) CheckValid() error {
        }
        locations := make(map[uint64]*Location, len(p.Location))
        for _, l := range p.Location {
+               if l == nil {
+                       return fmt.Errorf("profile has nil location")
+               }
                if l.ID == 0 {
                        return fmt.Errorf("found location with reserved id=0")
                }
@@ -426,6 +450,70 @@ func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, address
        return p.CheckValid()
 }
 
+// NumLabelUnits returns a map of numeric label keys to the units
+// associated with those keys and a map of those keys to any units
+// that were encountered but not used.
+// Unit for a given key is the first encountered unit for that key. If multiple
+// units are encountered for values paired with a particular key, then the first
+// unit encountered is used and all other units are returned in sorted order
+// in map of ignored units.
+// If no units are encountered for a particular key, the unit is then inferred
+// based on the key.
+func (p *Profile) NumLabelUnits() (map[string]string, map[string][]string) {
+       numLabelUnits := map[string]string{}
+       ignoredUnits := map[string]map[string]bool{}
+       encounteredKeys := map[string]bool{}
+
+       // Determine units based on numeric tags for each sample.
+       for _, s := range p.Sample {
+               for k := range s.NumLabel {
+                       encounteredKeys[k] = true
+                       for _, unit := range s.NumUnit[k] {
+                               if unit == "" {
+                                       continue
+                               }
+                               if wantUnit, ok := numLabelUnits[k]; !ok {
+                                       numLabelUnits[k] = unit
+                               } else if wantUnit != unit {
+                                       if v, ok := ignoredUnits[k]; ok {
+                                               v[unit] = true
+                                       } else {
+                                               ignoredUnits[k] = map[string]bool{unit: true}
+                                       }
+                               }
+                       }
+               }
+       }
+       // Infer units for keys without any units associated with
+       // numeric tag values.
+       for key := range encounteredKeys {
+               unit := numLabelUnits[key]
+               if unit == "" {
+                       switch key {
+                       case "alignment", "request":
+                               numLabelUnits[key] = "bytes"
+                       default:
+                               numLabelUnits[key] = key
+                       }
+               }
+       }
+
+       // Copy ignored units into more readable format
+       unitsIgnored := make(map[string][]string, len(ignoredUnits))
+       for key, values := range ignoredUnits {
+               units := make([]string, len(values))
+               i := 0
+               for unit := range values {
+                       units[i] = unit
+                       i++
+               }
+               sort.Strings(units)
+               unitsIgnored[key] = units
+       }
+
+       return numLabelUnits, unitsIgnored
+}
+
 // String dumps a text representation of a profile. Intended mainly
 // for debugging purposes.
 func (p *Profile) String() string {
@@ -455,87 +543,132 @@ func (p *Profile) String() string {
        }
        ss = append(ss, strings.TrimSpace(sh1))
        for _, s := range p.Sample {
-               var sv string
-               for _, v := range s.Value {
-                       sv = fmt.Sprintf("%s %10d", sv, v)
-               }
-               sv = sv + ": "
-               for _, l := range s.Location {
-                       sv = sv + fmt.Sprintf("%d ", l.ID)
-               }
-               ss = append(ss, sv)
-               const labelHeader = "                "
-               if len(s.Label) > 0 {
-                       ls := []string{}
-                       for k, v := range s.Label {
-                               ls = append(ls, fmt.Sprintf("%s:%v", k, v))
-                       }
-                       sort.Strings(ls)
-                       ss = append(ss, labelHeader+strings.Join(ls, " "))
-               }
-               if len(s.NumLabel) > 0 {
-                       ls := []string{}
-                       for k, v := range s.NumLabel {
-                               ls = append(ls, fmt.Sprintf("%s:%v", k, v))
-                       }
-                       sort.Strings(ls)
-                       ss = append(ss, labelHeader+strings.Join(ls, " "))
-               }
+               ss = append(ss, s.string())
        }
 
        ss = append(ss, "Locations")
        for _, l := range p.Location {
-               locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address)
-               if m := l.Mapping; m != nil {
-                       locStr = locStr + fmt.Sprintf("M=%d ", m.ID)
-               }
-               if len(l.Line) == 0 {
-                       ss = append(ss, locStr)
-               }
-               for li := range l.Line {
-                       lnStr := "??"
-                       if fn := l.Line[li].Function; fn != nil {
-                               lnStr = fmt.Sprintf("%s %s:%d s=%d",
-                                       fn.Name,
-                                       fn.Filename,
-                                       l.Line[li].Line,
-                                       fn.StartLine)
-                               if fn.Name != fn.SystemName {
-                                       lnStr = lnStr + "(" + fn.SystemName + ")"
-                               }
-                       }
-                       ss = append(ss, locStr+lnStr)
-                       // Do not print location details past the first line
-                       locStr = "             "
-               }
+               ss = append(ss, l.string())
        }
 
        ss = append(ss, "Mappings")
        for _, m := range p.Mapping {
-               bits := ""
-               if m.HasFunctions {
-                       bits = bits + "[FN]"
-               }
-               if m.HasFilenames {
-                       bits = bits + "[FL]"
-               }
-               if m.HasLineNumbers {
-                       bits = bits + "[LN]"
-               }
-               if m.HasInlineFrames {
-                       bits = bits + "[IN]"
-               }
-               ss = append(ss, fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s",
-                       m.ID,
-                       m.Start, m.Limit, m.Offset,
-                       m.File,
-                       m.BuildID,
-                       bits))
+               ss = append(ss, m.string())
        }
 
        return strings.Join(ss, "\n") + "\n"
 }
 
+// string dumps a text representation of a mapping. Intended mainly
+// for debugging purposes.
+func (m *Mapping) string() string {
+       bits := ""
+       if m.HasFunctions {
+               bits = bits + "[FN]"
+       }
+       if m.HasFilenames {
+               bits = bits + "[FL]"
+       }
+       if m.HasLineNumbers {
+               bits = bits + "[LN]"
+       }
+       if m.HasInlineFrames {
+               bits = bits + "[IN]"
+       }
+       return fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s",
+               m.ID,
+               m.Start, m.Limit, m.Offset,
+               m.File,
+               m.BuildID,
+               bits)
+}
+
+// string dumps a text representation of a location. Intended mainly
+// for debugging purposes.
+func (l *Location) string() string {
+       ss := []string{}
+       locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address)
+       if m := l.Mapping; m != nil {
+               locStr = locStr + fmt.Sprintf("M=%d ", m.ID)
+       }
+       if len(l.Line) == 0 {
+               ss = append(ss, locStr)
+       }
+       for li := range l.Line {
+               lnStr := "??"
+               if fn := l.Line[li].Function; fn != nil {
+                       lnStr = fmt.Sprintf("%s %s:%d s=%d",
+                               fn.Name,
+                               fn.Filename,
+                               l.Line[li].Line,
+                               fn.StartLine)
+                       if fn.Name != fn.SystemName {
+                               lnStr = lnStr + "(" + fn.SystemName + ")"
+                       }
+               }
+               ss = append(ss, locStr+lnStr)
+               // Do not print location details past the first line
+               locStr = "             "
+       }
+       return strings.Join(ss, "\n")
+}
+
+// string dumps a text representation of a sample. Intended mainly
+// for debugging purposes.
+func (s *Sample) string() string {
+       ss := []string{}
+       var sv string
+       for _, v := range s.Value {
+               sv = fmt.Sprintf("%s %10d", sv, v)
+       }
+       sv = sv + ": "
+       for _, l := range s.Location {
+               sv = sv + fmt.Sprintf("%d ", l.ID)
+       }
+       ss = append(ss, sv)
+       const labelHeader = "                "
+       if len(s.Label) > 0 {
+               ss = append(ss, labelHeader+labelsToString(s.Label))
+       }
+       if len(s.NumLabel) > 0 {
+               ss = append(ss, labelHeader+numLabelsToString(s.NumLabel, s.NumUnit))
+       }
+       return strings.Join(ss, "\n")
+}
+
+// labelsToString returns a string representation of a
+// map representing labels.
+func labelsToString(labels map[string][]string) string {
+       ls := []string{}
+       for k, v := range labels {
+               ls = append(ls, fmt.Sprintf("%s:%v", k, v))
+       }
+       sort.Strings(ls)
+       return strings.Join(ls, " ")
+}
+
+// numLablesToString returns a string representation of a map
+// representing numeric labels.
+func numLabelsToString(numLabels map[string][]int64, numUnits map[string][]string) string {
+       ls := []string{}
+       for k, v := range numLabels {
+               units := numUnits[k]
+               var labelString string
+               if len(units) == len(v) {
+                       values := make([]string, len(v))
+                       for i, vv := range v {
+                               values[i] = fmt.Sprintf("%d %s", vv, units[i])
+                       }
+                       labelString = fmt.Sprintf("%s:%v", k, values)
+               } else {
+                       labelString = fmt.Sprintf("%s:%v", k, v)
+               }
+               ls = append(ls, labelString)
+       }
+       sort.Strings(ls)
+       return strings.Join(ls, " ")
+}
+
 // Scale multiplies all sample values in a profile by a constant.
 func (p *Profile) Scale(ratio float64) {
        if ratio == 1 {
@@ -596,19 +729,17 @@ func (p *Profile) HasFileLines() bool {
 }
 
 // Unsymbolizable returns true if a mapping points to a binary for which
-// locations can't be symbolized in principle, at least now.
+// locations can't be symbolized in principle, at least now. Examples are
+// "[vdso]", [vsyscall]" and some others, see the code.
 func (m *Mapping) Unsymbolizable() bool {
        name := filepath.Base(m.File)
-       return name == "[vdso]" || strings.HasPrefix(name, "linux-vdso") || name == "[heap]" || strings.HasPrefix(m.File, "/dev/dri/")
+       return strings.HasPrefix(name, "[") || strings.HasPrefix(name, "linux-vdso") || strings.HasPrefix(m.File, "/dev/dri/")
 }
 
 // Copy makes a fully independent copy of a profile.
 func (p *Profile) Copy() *Profile {
-       p.preEncode()
-       b := marshal(p)
-
        pp := &Profile{}
-       if err := unmarshal(b, pp); err != nil {
+       if err := unmarshal(serialize(p), pp); err != nil {
                panic(err)
        }
        if err := pp.postDecode(); err != nil {
index c2319a650026ac0adabcaa41dc8fca4fec33da96..bc2ab8bdd10afef7c4f299ca4c1d0d8c9e82f16f 100644 (file)
@@ -19,8 +19,10 @@ import (
        "fmt"
        "io/ioutil"
        "path/filepath"
+       "reflect"
        "regexp"
        "strings"
+       "sync"
        "testing"
 
        "github.com/google/pprof/internal/proftest"
@@ -91,7 +93,6 @@ func TestParse(t *testing.T) {
 }
 
 func TestParseError(t *testing.T) {
-
        testcases := []string{
                "",
                "garbage text",
@@ -107,6 +108,63 @@ func TestParseError(t *testing.T) {
        }
 }
 
+func TestCheckValid(t *testing.T) {
+       const path = "testdata/java.cpu"
+
+       inbytes, err := ioutil.ReadFile(path)
+       if err != nil {
+               t.Fatalf("failed to read profile file %q: %v", path, err)
+       }
+       p, err := Parse(bytes.NewBuffer(inbytes))
+       if err != nil {
+               t.Fatalf("failed to parse profile %q: %s", path, err)
+       }
+
+       for _, tc := range []struct {
+               mutateFn func(*Profile)
+               wantErr  string
+       }{
+               {
+                       mutateFn: func(p *Profile) { p.SampleType = nil },
+                       wantErr:  "missing sample type information",
+               },
+               {
+                       mutateFn: func(p *Profile) { p.Sample[0] = nil },
+                       wantErr:  "profile has nil sample",
+               },
+               {
+                       mutateFn: func(p *Profile) { p.Sample[0].Value = append(p.Sample[0].Value, 0) },
+                       wantErr:  "sample has 3 values vs. 2 types",
+               },
+               {
+                       mutateFn: func(p *Profile) { p.Sample[0].Location[0] = nil },
+                       wantErr:  "sample has nil location",
+               },
+               {
+                       mutateFn: func(p *Profile) { p.Location[0] = nil },
+                       wantErr:  "profile has nil location",
+               },
+               {
+                       mutateFn: func(p *Profile) { p.Mapping = append(p.Mapping, nil) },
+                       wantErr:  "profile has nil mapping",
+               },
+               {
+                       mutateFn: func(p *Profile) { p.Function[0] = nil },
+                       wantErr:  "profile has nil function",
+               },
+       } {
+               t.Run(tc.wantErr, func(t *testing.T) {
+                       p := p.Copy()
+                       tc.mutateFn(p)
+                       if err := p.CheckValid(); err == nil {
+                               t.Errorf("CheckValid(): got no error, want error %q", tc.wantErr)
+                       } else if !strings.Contains(err.Error(), tc.wantErr) {
+                               t.Errorf("CheckValid(): got error %v, want error %q", err, tc.wantErr)
+                       }
+               })
+       }
+}
+
 // leaveTempfile leaves |b| in a temporary file on disk and returns the
 // temp filename. This is useful to recover a profile when the test
 // fails.
@@ -217,7 +275,7 @@ var cpuL = []*Location{
        },
 }
 
-var testProfile = &Profile{
+var testProfile1 = &Profile{
        PeriodType:    &ValueType{Type: "cpu", Unit: "milliseconds"},
        Period:        1,
        DurationNanos: 10e9,
@@ -230,40 +288,181 @@ var testProfile = &Profile{
                        Location: []*Location{cpuL[0]},
                        Value:    []int64{1000, 1000},
                        Label: map[string][]string{
-                               "key1": []string{"tag1"},
-                               "key2": []string{"tag1"},
+                               "key1": {"tag1"},
+                               "key2": {"tag1"},
                        },
                },
                {
                        Location: []*Location{cpuL[1], cpuL[0]},
                        Value:    []int64{100, 100},
                        Label: map[string][]string{
-                               "key1": []string{"tag2"},
-                               "key3": []string{"tag2"},
+                               "key1": {"tag2"},
+                               "key3": {"tag2"},
                        },
                },
                {
                        Location: []*Location{cpuL[2], cpuL[0]},
                        Value:    []int64{10, 10},
                        Label: map[string][]string{
-                               "key1": []string{"tag3"},
-                               "key2": []string{"tag2"},
+                               "key1": {"tag3"},
+                               "key2": {"tag2"},
                        },
                },
                {
                        Location: []*Location{cpuL[3], cpuL[0]},
                        Value:    []int64{10000, 10000},
                        Label: map[string][]string{
-                               "key1": []string{"tag4"},
-                               "key2": []string{"tag1"},
+                               "key1": {"tag4"},
+                               "key2": {"tag1"},
+                       },
+               },
+               {
+                       Location: []*Location{cpuL[4], cpuL[0]},
+                       Value:    []int64{1, 1},
+                       Label: map[string][]string{
+                               "key1": {"tag4"},
+                               "key2": {"tag1"},
+                       },
+               },
+       },
+       Location: cpuL,
+       Function: cpuF,
+       Mapping:  cpuM,
+}
+
+var testProfile2 = &Profile{
+       PeriodType:    &ValueType{Type: "cpu", Unit: "milliseconds"},
+       Period:        1,
+       DurationNanos: 10e9,
+       SampleType: []*ValueType{
+               {Type: "samples", Unit: "count"},
+               {Type: "cpu", Unit: "milliseconds"},
+       },
+       Sample: []*Sample{
+               {
+                       Location: []*Location{cpuL[0]},
+                       Value:    []int64{70, 1000},
+                       Label: map[string][]string{
+                               "key1": {"tag1"},
+                               "key2": {"tag1"},
+                       },
+               },
+               {
+                       Location: []*Location{cpuL[1], cpuL[0]},
+                       Value:    []int64{60, 100},
+                       Label: map[string][]string{
+                               "key1": {"tag2"},
+                               "key3": {"tag2"},
+                       },
+               },
+               {
+                       Location: []*Location{cpuL[2], cpuL[0]},
+                       Value:    []int64{50, 10},
+                       Label: map[string][]string{
+                               "key1": {"tag3"},
+                               "key2": {"tag2"},
+                       },
+               },
+               {
+                       Location: []*Location{cpuL[3], cpuL[0]},
+                       Value:    []int64{40, 10000},
+                       Label: map[string][]string{
+                               "key1": {"tag4"},
+                               "key2": {"tag1"},
                        },
                },
                {
                        Location: []*Location{cpuL[4], cpuL[0]},
                        Value:    []int64{1, 1},
                        Label: map[string][]string{
-                               "key1": []string{"tag4"},
-                               "key2": []string{"tag1"},
+                               "key1": {"tag4"},
+                               "key2": {"tag1"},
+                       },
+               },
+       },
+       Location: cpuL,
+       Function: cpuF,
+       Mapping:  cpuM,
+}
+
+var testProfile3 = &Profile{
+       PeriodType:    &ValueType{Type: "cpu", Unit: "milliseconds"},
+       Period:        1,
+       DurationNanos: 10e9,
+       SampleType: []*ValueType{
+               {Type: "samples", Unit: "count"},
+       },
+       Sample: []*Sample{
+               {
+                       Location: []*Location{cpuL[0]},
+                       Value:    []int64{1000},
+                       Label: map[string][]string{
+                               "key1": {"tag1"},
+                               "key2": {"tag1"},
+                       },
+               },
+       },
+       Location: cpuL,
+       Function: cpuF,
+       Mapping:  cpuM,
+}
+
+var testProfile4 = &Profile{
+       PeriodType:    &ValueType{Type: "cpu", Unit: "milliseconds"},
+       Period:        1,
+       DurationNanos: 10e9,
+       SampleType: []*ValueType{
+               {Type: "samples", Unit: "count"},
+       },
+       Sample: []*Sample{
+               {
+                       Location: []*Location{cpuL[0]},
+                       Value:    []int64{1000},
+                       NumLabel: map[string][]int64{
+                               "key1": {10},
+                               "key2": {30},
+                       },
+                       NumUnit: map[string][]string{
+                               "key1": {"bytes"},
+                               "key2": {"bytes"},
+                       },
+               },
+       },
+       Location: cpuL,
+       Function: cpuF,
+       Mapping:  cpuM,
+}
+
+var testProfile5 = &Profile{
+       PeriodType:    &ValueType{Type: "cpu", Unit: "milliseconds"},
+       Period:        1,
+       DurationNanos: 10e9,
+       SampleType: []*ValueType{
+               {Type: "samples", Unit: "count"},
+       },
+       Sample: []*Sample{
+               {
+                       Location: []*Location{cpuL[0]},
+                       Value:    []int64{1000},
+                       NumLabel: map[string][]int64{
+                               "key1": {10},
+                               "key2": {30},
+                       },
+                       NumUnit: map[string][]string{
+                               "key1": {"bytes"},
+                               "key2": {"bytes"},
+                       },
+               },
+               {
+                       Location: []*Location{cpuL[0]},
+                       Value:    []int64{1000},
+                       NumLabel: map[string][]int64{
+                               "key1": {10},
+                               "key2": {30},
+                       },
+                       NumUnit: map[string][]string{
+                               "key1": {"kilobytes"},
+                               "key2": {"kilobytes"},
                        },
                },
        },
@@ -273,10 +472,10 @@ var testProfile = &Profile{
 }
 
 var aggTests = map[string]aggTest{
-       "precise":         aggTest{true, true, true, true, 5},
-       "fileline":        aggTest{false, true, true, true, 4},
-       "inline_function": aggTest{false, true, false, true, 3},
-       "function":        aggTest{false, true, false, false, 2},
+       "precise":         {true, true, true, true, 5},
+       "fileline":        {false, true, true, true, 4},
+       "inline_function": {false, true, false, true, 3},
+       "function":        {false, true, false, false, 2},
 }
 
 type aggTest struct {
@@ -287,7 +486,7 @@ type aggTest struct {
 const totalSamples = int64(11111)
 
 func TestAggregation(t *testing.T) {
-       prof := testProfile.Copy()
+       prof := testProfile1.Copy()
        for _, resolution := range []string{"precise", "fileline", "inline_function", "function"} {
                a := aggTests[resolution]
                if !a.precise {
@@ -362,7 +561,7 @@ func checkAggregation(prof *Profile, a *aggTest) error {
 
 // Test merge leaves the main binary in place.
 func TestMergeMain(t *testing.T) {
-       prof := testProfile.Copy()
+       prof := testProfile1.Copy()
        p1, err := Merge([]*Profile{prof})
        if err != nil {
                t.Fatalf("merge error: %v", err)
@@ -377,7 +576,7 @@ func TestMerge(t *testing.T) {
        // -2. Should end up with an empty profile (all samples for a
        // location should add up to 0).
 
-       prof := testProfile.Copy()
+       prof := testProfile1.Copy()
        p1, err := Merge([]*Profile{prof, prof})
        if err != nil {
                t.Errorf("merge error: %v", err)
@@ -409,7 +608,7 @@ func TestMergeAll(t *testing.T) {
        // Aggregate 10 copies of the profile.
        profs := make([]*Profile, 10)
        for i := 0; i < 10; i++ {
-               profs[i] = testProfile.Copy()
+               profs[i] = testProfile1.Copy()
        }
        prof, err := Merge(profs)
        if err != nil {
@@ -420,7 +619,7 @@ func TestMergeAll(t *testing.T) {
                tb := locationHash(s)
                samples[tb] = samples[tb] + s.Value[0]
        }
-       for _, s := range testProfile.Sample {
+       for _, s := range testProfile1.Sample {
                tb := locationHash(s)
                if samples[tb] != s.Value[0]*10 {
                        t.Errorf("merge got wrong value at %s : %d instead of %d", tb, samples[tb], s.Value[0]*10)
@@ -428,6 +627,140 @@ func TestMergeAll(t *testing.T) {
        }
 }
 
+func TestNumLabelMerge(t *testing.T) {
+       for _, tc := range []struct {
+               name          string
+               profs         []*Profile
+               wantNumLabels []map[string][]int64
+               wantNumUnits  []map[string][]string
+       }{
+               {
+                       name:  "different tag units not merged",
+                       profs: []*Profile{testProfile4.Copy(), testProfile5.Copy()},
+                       wantNumLabels: []map[string][]int64{
+                               {
+                                       "key1": {10},
+                                       "key2": {30},
+                               },
+                               {
+                                       "key1": {10},
+                                       "key2": {30},
+                               },
+                       },
+                       wantNumUnits: []map[string][]string{
+                               {
+                                       "key1": {"bytes"},
+                                       "key2": {"bytes"},
+                               },
+                               {
+                                       "key1": {"kilobytes"},
+                                       "key2": {"kilobytes"},
+                               },
+                       },
+               },
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       prof, err := Merge(tc.profs)
+                       if err != nil {
+                               t.Errorf("merge error: %v", err)
+                       }
+
+                       if want, got := len(tc.wantNumLabels), len(prof.Sample); want != got {
+                               t.Fatalf("got %d samples, want %d samples", got, want)
+                       }
+                       for i, wantLabels := range tc.wantNumLabels {
+                               numLabels := prof.Sample[i].NumLabel
+                               if !reflect.DeepEqual(wantLabels, numLabels) {
+                                       t.Errorf("got numeric labels %v, want %v", numLabels, wantLabels)
+                               }
+
+                               wantUnits := tc.wantNumUnits[i]
+                               numUnits := prof.Sample[i].NumUnit
+                               if !reflect.DeepEqual(wantUnits, numUnits) {
+                                       t.Errorf("got numeric labels %v, want %v", numUnits, wantUnits)
+                               }
+                       }
+               })
+       }
+}
+
+func TestNormalizeBySameProfile(t *testing.T) {
+       pb := testProfile1.Copy()
+       p := testProfile1.Copy()
+
+       if err := p.Normalize(pb); err != nil {
+               t.Fatal(err)
+       }
+
+       for i, s := range p.Sample {
+               for j, v := range s.Value {
+                       expectedSampleValue := testProfile1.Sample[i].Value[j]
+                       if v != expectedSampleValue {
+                               t.Errorf("For sample %d, value %d want %d got %d", i, j, expectedSampleValue, v)
+                       }
+               }
+       }
+}
+
+func TestNormalizeByDifferentProfile(t *testing.T) {
+       p := testProfile1.Copy()
+       pb := testProfile2.Copy()
+
+       if err := p.Normalize(pb); err != nil {
+               t.Fatal(err)
+       }
+
+       expectedSampleValues := [][]int64{
+               {19, 1000},
+               {1, 100},
+               {0, 10},
+               {198, 10000},
+               {0, 1},
+       }
+
+       for i, s := range p.Sample {
+               for j, v := range s.Value {
+                       if v != expectedSampleValues[i][j] {
+                               t.Errorf("For sample %d, value %d want %d got %d", i, j, expectedSampleValues[i][j], v)
+                       }
+               }
+       }
+}
+
+func TestNormalizeByMultipleOfSameProfile(t *testing.T) {
+       pb := testProfile1.Copy()
+       for i, s := range pb.Sample {
+               for j, v := range s.Value {
+                       pb.Sample[i].Value[j] = 10 * v
+               }
+       }
+
+       p := testProfile1.Copy()
+
+       err := p.Normalize(pb)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       for i, s := range p.Sample {
+               for j, v := range s.Value {
+                       expectedSampleValue := 10 * testProfile1.Sample[i].Value[j]
+                       if v != expectedSampleValue {
+                               t.Errorf("For sample %d, value %d, want %d got %d", i, j, expectedSampleValue, v)
+                       }
+               }
+       }
+}
+
+func TestNormalizeIncompatibleProfiles(t *testing.T) {
+       p := testProfile1.Copy()
+       pb := testProfile3.Copy()
+
+       if err := p.Normalize(pb); err == nil {
+               t.Errorf("Expected an error")
+       }
+}
+
 func TestFilter(t *testing.T) {
        // Perform several forms of filtering on the test profile.
 
@@ -437,12 +770,33 @@ func TestFilter(t *testing.T) {
        }
 
        for tx, tc := range []filterTestcase{
-               {nil, nil, nil, nil, true, false, false, false},
-               {regexp.MustCompile("notfound"), nil, nil, nil, false, false, false, false},
-               {nil, regexp.MustCompile("foo.c"), nil, nil, true, true, false, false},
-               {nil, nil, regexp.MustCompile("lib.so"), nil, true, false, true, false},
+               {
+                       fm: true, // nil focus matches every sample
+               },
+               {
+                       focus: regexp.MustCompile("notfound"),
+               },
+               {
+                       ignore: regexp.MustCompile("foo.c"),
+                       fm:     true,
+                       im:     true,
+               },
+               {
+                       hide: regexp.MustCompile("lib.so"),
+                       fm:   true,
+                       hm:   true,
+               },
+               {
+                       show: regexp.MustCompile("foo.c"),
+                       fm:   true,
+                       hnm:  true,
+               },
+               {
+                       show: regexp.MustCompile("notfound"),
+                       fm:   true,
+               },
        } {
-               prof := *testProfile.Copy()
+               prof := *testProfile1.Copy()
                gf, gi, gh, gnh := prof.FilterSamplesByName(tc.focus, tc.ignore, tc.hide, tc.show)
                if gf != tc.fm {
                        t.Errorf("Filter #%d, got fm=%v, want %v", tx, gf, tc.fm)
@@ -488,7 +842,7 @@ func TestTagFilter(t *testing.T) {
                {regexp.MustCompile("key1"), nil, true, false, 1},
                {nil, regexp.MustCompile("key[12]"), true, true, 1},
        } {
-               prof := testProfile.Copy()
+               prof := testProfile1.Copy()
                gim, gem := prof.FilterTagsByName(tc.include, tc.exclude)
                if gim != tc.im {
                        t.Errorf("Filter #%d, got include match=%v, want %v", tx, gim, tc.im)
@@ -513,9 +867,152 @@ func locationHash(s *Sample) string {
        return tb
 }
 
+func TestNumLabelUnits(t *testing.T) {
+       var tagFilterTests = []struct {
+               desc             string
+               tagVals          []map[string][]int64
+               tagUnits         []map[string][]string
+               wantUnits        map[string]string
+               wantIgnoredUnits map[string][]string
+       }{
+               {
+                       "One sample, multiple keys, different specified units",
+                       []map[string][]int64{{"key1": {131072}, "key2": {128}}},
+                       []map[string][]string{{"key1": {"bytes"}, "key2": {"kilobytes"}}},
+                       map[string]string{"key1": "bytes", "key2": "kilobytes"},
+                       map[string][]string{},
+               },
+               {
+                       "One sample, one key with one value, unit specified",
+                       []map[string][]int64{{"key1": {8}}},
+                       []map[string][]string{{"key1": {"bytes"}}},
+                       map[string]string{"key1": "bytes"},
+                       map[string][]string{},
+               },
+               {
+                       "One sample, one key with one value, empty unit specified",
+                       []map[string][]int64{{"key1": {8}}},
+                       []map[string][]string{{"key1": {""}}},
+                       map[string]string{"key1": "key1"},
+                       map[string][]string{},
+               },
+               {
+                       "Key bytes, unit not specified",
+                       []map[string][]int64{{"bytes": {8}}},
+                       []map[string][]string{nil},
+                       map[string]string{"bytes": "bytes"},
+                       map[string][]string{},
+               },
+               {
+                       "One sample, one key with one value, unit not specified",
+                       []map[string][]int64{{"kilobytes": {8}}},
+                       []map[string][]string{nil},
+                       map[string]string{"kilobytes": "kilobytes"},
+                       map[string][]string{},
+               },
+               {
+                       "Key request, unit not specified",
+                       []map[string][]int64{{"request": {8}}},
+                       []map[string][]string{nil},
+                       map[string]string{"request": "bytes"},
+                       map[string][]string{},
+               },
+               {
+                       "Key alignment, unit not specified",
+                       []map[string][]int64{{"alignment": {8}}},
+                       []map[string][]string{nil},
+                       map[string]string{"alignment": "bytes"},
+                       map[string][]string{},
+               },
+               {
+                       "One sample, one key with multiple values and two different units",
+                       []map[string][]int64{{"key1": {8, 8}}},
+                       []map[string][]string{{"key1": {"bytes", "kilobytes"}}},
+                       map[string]string{"key1": "bytes"},
+                       map[string][]string{"key1": {"kilobytes"}},
+               },
+               {
+                       "One sample, one key with multiple values and three different units",
+                       []map[string][]int64{{"key1": {8, 8}}},
+                       []map[string][]string{{"key1": {"bytes", "megabytes", "kilobytes"}}},
+                       map[string]string{"key1": "bytes"},
+                       map[string][]string{"key1": {"kilobytes", "megabytes"}},
+               },
+               {
+                       "Two samples, one key, different units specified",
+                       []map[string][]int64{{"key1": {8}}, {"key1": {8}}},
+                       []map[string][]string{{"key1": {"bytes"}}, {"key1": {"kilobytes"}}},
+                       map[string]string{"key1": "bytes"},
+                       map[string][]string{"key1": {"kilobytes"}},
+               },
+               {
+                       "Keys alignment, request, and bytes have units specified",
+                       []map[string][]int64{{
+                               "alignment": {8},
+                               "request":   {8},
+                               "bytes":     {8},
+                       }},
+                       []map[string][]string{{
+                               "alignment": {"seconds"},
+                               "request":   {"minutes"},
+                               "bytes":     {"hours"},
+                       }},
+                       map[string]string{
+                               "alignment": "seconds",
+                               "request":   "minutes",
+                               "bytes":     "hours",
+                       },
+                       map[string][]string{},
+               },
+       }
+       for _, test := range tagFilterTests {
+               p := &Profile{Sample: make([]*Sample, len(test.tagVals))}
+               for i, numLabel := range test.tagVals {
+                       s := Sample{
+                               NumLabel: numLabel,
+                               NumUnit:  test.tagUnits[i],
+                       }
+                       p.Sample[i] = &s
+               }
+               units, ignoredUnits := p.NumLabelUnits()
+               if !reflect.DeepEqual(test.wantUnits, units) {
+                       t.Errorf("%s: got %v units, want %v", test.desc, units, test.wantUnits)
+               }
+               if !reflect.DeepEqual(test.wantIgnoredUnits, ignoredUnits) {
+                       t.Errorf("%s: got %v ignored units, want %v", test.desc, ignoredUnits, test.wantIgnoredUnits)
+               }
+       }
+}
+
 func TestSetMain(t *testing.T) {
-       testProfile.massageMappings()
-       if testProfile.Mapping[0].File != mainBinary {
-               t.Errorf("got %s for main", testProfile.Mapping[0].File)
+       testProfile1.massageMappings()
+       if testProfile1.Mapping[0].File != mainBinary {
+               t.Errorf("got %s for main", testProfile1.Mapping[0].File)
+       }
+}
+
+// parallel runs n copies of fn in parallel.
+func parallel(n int, fn func()) {
+       var wg sync.WaitGroup
+       wg.Add(n)
+       for i := 0; i < n; i++ {
+               go func() {
+                       fn()
+                       wg.Done()
+               }()
        }
+       wg.Wait()
+}
+
+func TestThreadSafety(t *testing.T) {
+       src := testProfile1.Copy()
+       parallel(4, func() { src.Copy() })
+       parallel(4, func() {
+               var b bytes.Buffer
+               src.WriteUncompressed(&b)
+       })
+       parallel(4, func() {
+               var b bytes.Buffer
+               src.Write(&b)
+       })
 }
index 01b7f7ae181af4e703740df7dff22f77a7a0b9e6..e7df33ac2b8c1c0c18f093ebd89c1594fb5c9e10 100644 (file)
@@ -71,7 +71,7 @@ func encodeLength(b *buffer, tag int, len int) {
 
 func encodeUint64(b *buffer, tag int, x uint64) {
        // append varint to b.data
-       encodeVarint(b, uint64(tag)<<3|0)
+       encodeVarint(b, uint64(tag)<<3)
        encodeVarint(b, x)
 }
 
@@ -145,13 +145,6 @@ func encodeStrings(b *buffer, tag int, x []string) {
        }
 }
 
-func encodeStringOpt(b *buffer, tag int, x string) {
-       if x == "" {
-               return
-       }
-       encodeString(b, tag, x)
-}
-
 func encodeBool(b *buffer, tag int, x bool) {
        if x {
                encodeUint64(b, tag, 1)
@@ -161,10 +154,9 @@ func encodeBool(b *buffer, tag int, x bool) {
 }
 
 func encodeBoolOpt(b *buffer, tag int, x bool) {
-       if x == false {
-               return
+       if x {
+               encodeBool(b, tag, x)
        }
-       encodeBool(b, tag, x)
 }
 
 func encodeMessage(b *buffer, tag int, m message) {
index d2a351373e1d4330ff9686f2efd62fe14fce8999..e0832294ace98d1b9db255f58b882c261ddaae7f 100644 (file)
@@ -100,8 +100,8 @@ var all = &Profile{
                {
                        Location: []*Location{testL[0], testL[1], testL[2], testL[1], testL[1]},
                        Label: map[string][]string{
-                               "key1": []string{"value1"},
-                               "key2": []string{"value2"},
+                               "key1": {"value1"},
+                               "key2": {"value2"},
                        },
                        Value: []int64{10, 20},
                },
@@ -109,12 +109,19 @@ var all = &Profile{
                        Location: []*Location{testL[1], testL[2], testL[0], testL[1]},
                        Value:    []int64{30, 40},
                        Label: map[string][]string{
-                               "key1": []string{"value1"},
-                               "key2": []string{"value2"},
+                               "key1": {"value1"},
+                               "key2": {"value2"},
                        },
                        NumLabel: map[string][]int64{
-                               "key1": []int64{1, 2},
-                               "key2": []int64{3, 4},
+                               "key1":      {1, 2},
+                               "key2":      {3, 4},
+                               "bytes":     {3, 4},
+                               "requests":  {1, 1, 3, 4, 5},
+                               "alignment": {3, 4},
+                       },
+                       NumUnit: map[string][]string{
+                               "requests":  {"", "", "seconds", "", "s"},
+                               "alignment": {"kilobytes", "kilobytes"},
                        },
                },
        },
index cf9cbb3894a6fc49108f4eb32f0bdc7fd5a92fd1..02d21a8184639e07f7de8b6735f0a2d1a1b598eb 100644 (file)
@@ -22,6 +22,39 @@ import (
        "strings"
 )
 
+var (
+       reservedNames = []string{"(anonymous namespace)", "operator()"}
+       bracketRx     = func() *regexp.Regexp {
+               var quotedNames []string
+               for _, name := range append(reservedNames, "(") {
+                       quotedNames = append(quotedNames, regexp.QuoteMeta(name))
+               }
+               return regexp.MustCompile(strings.Join(quotedNames, "|"))
+       }()
+)
+
+// simplifyFunc does some primitive simplification of function names.
+func simplifyFunc(f string) string {
+       // Account for leading '.' on the PPC ELF v1 ABI.
+       funcName := strings.TrimPrefix(f, ".")
+       // Account for unsimplified names -- try  to remove the argument list by trimming
+       // starting from the first '(', but skipping reserved names that have '('.
+       for _, ind := range bracketRx.FindAllStringSubmatchIndex(funcName, -1) {
+               foundReserved := false
+               for _, res := range reservedNames {
+                       if funcName[ind[0]:ind[1]] == res {
+                               foundReserved = true
+                               break
+                       }
+               }
+               if !foundReserved {
+                       funcName = funcName[:ind[0]]
+                       break
+               }
+       }
+       return funcName
+}
+
 // Prune removes all nodes beneath a node matching dropRx, and not
 // matching keepRx. If the root node of a Sample matches, the sample
 // will have an empty stack.
@@ -33,12 +66,7 @@ func (p *Profile) Prune(dropRx, keepRx *regexp.Regexp) {
                var i int
                for i = len(loc.Line) - 1; i >= 0; i-- {
                        if fn := loc.Line[i].Function; fn != nil && fn.Name != "" {
-                               // Account for leading '.' on the PPC ELF v1 ABI.
-                               funcName := strings.TrimPrefix(fn.Name, ".")
-                               // Account for unsimplified names -- trim starting from the first '('.
-                               if index := strings.Index(funcName, "("); index > 0 {
-                                       funcName = funcName[:index]
-                               }
+                               funcName := simplifyFunc(fn.Name)
                                if dropRx.MatchString(funcName) {
                                        if keepRx == nil || !keepRx.MatchString(funcName) {
                                                break
@@ -126,12 +154,7 @@ func (p *Profile) PruneFrom(dropRx *regexp.Regexp) {
        for _, loc := range p.Location {
                for i := 0; i < len(loc.Line); i++ {
                        if fn := loc.Line[i].Function; fn != nil && fn.Name != "" {
-                               // Account for leading '.' on the PPC ELF v1 ABI.
-                               funcName := strings.TrimPrefix(fn.Name, ".")
-                               // Account for unsimplified names -- trim starting from the first '('.
-                               if index := strings.Index(funcName, "("); index > 0 {
-                                       funcName = funcName[:index]
-                               }
+                               funcName := simplifyFunc(fn.Name)
                                if dropRx.MatchString(funcName) {
                                        // Found matching entry to prune.
                                        pruneBeneath[loc.ID] = true
index 58fa25ee2d05168579a057d9eb2ea977fe8af902..75d7c6d4f7fabb651beb15a30827b5043557d998 100644 (file)
@@ -25,6 +25,7 @@ func TestPrune(t *testing.T) {
                want string
        }{
                {in1, out1},
+               {in2, out2},
        } {
                in := test.in.Copy()
                in.RemoveUninteresting()
@@ -50,6 +51,10 @@ var funs = []*Function{
        {ID: 4, Name: "fun3", SystemName: "fun3", Filename: "fun.c"},
        {ID: 5, Name: "fun4", SystemName: "fun4", Filename: "fun.c"},
        {ID: 6, Name: "fun5", SystemName: "fun5", Filename: "fun.c"},
+       {ID: 7, Name: "unsimplified_fun(int)", SystemName: "unsimplified_fun(int)", Filename: "fun.c"},
+       {ID: 8, Name: "Foo::(anonymous namespace)::Test::Bar", SystemName: "Foo::(anonymous namespace)::Test::Bar", Filename: "fun.c"},
+       {ID: 9, Name: "Hello::(anonymous namespace)::World(const Foo::(anonymous namespace)::Test::Bar)", SystemName: "Hello::(anonymous namespace)::World(const Foo::(anonymous namespace)::Test::Bar)", Filename: "fun.c"},
+       {ID: 10, Name: "Foo::operator()(::Bar)", SystemName: "Foo::operator()(::Bar)", Filename: "fun.c"},
 }
 
 var locs1 = []*Location{
@@ -137,3 +142,89 @@ Locations
      4: 0x0 fun5 fun.c:2 s=0
 Mappings
 `
+
+var locs2 = []*Location{
+       {
+               ID: 1,
+               Line: []Line{
+                       {Function: funs[0], Line: 1},
+               },
+       },
+       {
+               ID: 2,
+               Line: []Line{
+                       {Function: funs[6], Line: 1},
+               },
+       },
+       {
+               ID: 3,
+               Line: []Line{
+                       {Function: funs[7], Line: 1},
+               },
+       },
+       {
+               ID: 4,
+               Line: []Line{
+                       {Function: funs[8], Line: 1},
+               },
+       },
+       {
+               ID: 5,
+               Line: []Line{
+                       {Function: funs[9], Line: 1},
+               },
+       },
+}
+
+var in2 = &Profile{
+       PeriodType:    &ValueType{Type: "cpu", Unit: "milliseconds"},
+       Period:        1,
+       DurationNanos: 10e9,
+       SampleType: []*ValueType{
+               {Type: "samples", Unit: "count"},
+               {Type: "cpu", Unit: "milliseconds"},
+       },
+       Sample: []*Sample{
+               // Unsimplified name with parameters shouldn't match.
+               {
+                       Location: []*Location{locs2[1], locs2[0]},
+                       Value:    []int64{1, 1},
+               },
+               // .*Foo::.*::Bar.* should (and will be dropped) regardless of the anonymous namespace.
+               {
+                       Location: []*Location{locs2[2], locs2[0]},
+                       Value:    []int64{1, 1},
+               },
+               // .*Foo::.*::Bar.* shouldn't match inside the parameter list.
+               {
+                       Location: []*Location{locs2[3], locs2[0]},
+                       Value:    []int64{1, 1},
+               },
+               // .*operator\(\) should match, regardless of parameters.
+               {
+                       Location: []*Location{locs2[4], locs2[0]},
+                       Value:    []int64{1, 1},
+               },
+       },
+       Location:   locs2,
+       Function:   funs,
+       DropFrames: `unsimplified_fun\(int\)|.*Foo::.*::Bar.*|.*operator\(\)`,
+}
+
+const out2 = `PeriodType: cpu milliseconds
+Period: 1
+Duration: 10s
+Samples:
+samples/count cpu/milliseconds
+          1          1: 2 1
+          1          1: 1
+          1          1: 4 1
+          1          1: 1
+Locations
+     1: 0x0 main main.c:1 s=0
+     2: 0x0 unsimplified_fun(int) fun.c:1 s=0
+     3: 0x0 Foo::(anonymous namespace)::Test::Bar fun.c:1 s=0
+     4: 0x0 Hello::(anonymous namespace)::World(const Foo::(anonymous namespace)::Test::Bar) fun.c:1 s=0
+     5: 0x0 Foo::operator()(::Bar) fun.c:1 s=0
+Mappings
+`
index aa790e032ad7b7487b6a4405ceeebc6c729f1679..78caf4138b212c4f185c89a5986f7fd175c184d4 100644 (file)
@@ -109,6 +109,15 @@ message Label {
   // At most one of the following must be present
   int64 str = 2;   // Index into string table
   int64 num = 3;
+
+  // Should only be present when num is present.
+  // Specifies the units of num.
+  // Use arbitrary string (for example, "requests") as a custom count unit.
+  // If no unit is specified, consumer may apply heuristic to deduce the unit.
+  // Consumers may also  interpret units like "bytes" and "kilobytes" as memory
+  // units and units like "seconds" and "nanoseconds" as time units,
+  // and apply appropriate unit conversions to these.
+  int64 num_unit = 4;  // Index into string table
 }
 
 message Mapping {
diff --git a/src/cmd/vendor/github.com/google/pprof/test.sh b/src/cmd/vendor/github.com/google/pprof/test.sh
new file mode 100755 (executable)
index 0000000..81b9089
--- /dev/null
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+set -e
+MODE=atomic
+echo "mode: $MODE" > coverage.txt
+
+PKG=$(go list ./... | grep -v /vendor/)
+
+staticcheck $PKG
+unused $PKG
+go test -v $PKG
+
+for d in $PKG; do
+  go test -race -coverprofile=profile.out -covermode=$MODE $d
+  if [ -f profile.out ]; then
+    cat profile.out | grep -v "^mode: " >> coverage.txt
+    rm profile.out
+  fi
+done
index 6e8171df89571301b0c1b329fd485fa6d8302c00..36716cb9eb10d794c65519a94f136ffab3e0eab8 100644 (file)
@@ -9,8 +9,8 @@
                {
                        "canonical": "github.com/google/pprof",
                        "local": "github.com/google/pprof",
-                       "revision": "dec22b42d9eee442222c36c8da24ddc9905e7ee6",
-                       "revisionTime": "2017-03-01T19:58:13Z",
+                       "revision": "4fc39a00b6b8c1aad05260f01429ec70e127252c",
+                       "revisionTime": "2017-11-01T07:14:40Z",
                },
                {
                        "canonical": "golang.org/x/arch/x86/x86asm",