├── html
├── .gitignore
├── components
│ ├── gc-pause
│ │ ├── gc-pause.less
│ │ ├── gc-pause.html
│ │ └── gc-pause.js
│ ├── go-routines
│ │ ├── go-routines.less
│ │ ├── go-routines.html
│ │ └── go-routines.js
│ ├── memory-allocation
│ │ ├── memory-allocation.less
│ │ ├── memory-allocation.html
│ │ └── memory-allocation.js
│ └── flame-graph
│ │ ├── flame-graph.html
│ │ └── flame-graph.js
├── dev.index.html
├── README.md
├── index.html
├── util
│ ├── list-factory.js
│ └── socket-manager.js
├── less
│ └── styles.less
├── package.json
├── app
│ ├── app.js
│ ├── app-state.js
│ └── app.html
├── build.js
└── bindings
│ ├── flame
│ ├── flame.js
│ ├── flame.css
│ └── js
│ │ ├── d3-tip.js
│ │ └── d3.flameGraph.js
│ └── ko.linechart.js
├── images
├── go-gc.png
├── memory.png
└── go-flame.png
├── AUTHORS
├── tickerHandlers_test.go
├── LICENSE
├── flameHandlers.go
├── parse_test.go
├── examples
└── web
│ ├── ko.flame.js
│ ├── web.go
│ ├── d3.flameGraph.css
│ ├── ko.linechart.js
│ ├── index.html
│ └── d3.flameGraph.js
├── cpu_test.go
├── parse.go
├── LICENSE_internal_pkg
├── README.md
├── tickerHandlers.go
├── cpu.go
└── internal
└── profile
├── proto.go
├── encode.go
├── profile.go
└── legacy_profile.go
/html/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
--------------------------------------------------------------------------------
/html/components/gc-pause/gc-pause.less:
--------------------------------------------------------------------------------
1 | gc-pause {
2 | }
--------------------------------------------------------------------------------
/html/components/go-routines/go-routines.less:
--------------------------------------------------------------------------------
1 | go-routines {}
--------------------------------------------------------------------------------
/html/components/memory-allocation/memory-allocation.less:
--------------------------------------------------------------------------------
1 | memory-allocation {}
--------------------------------------------------------------------------------
/images/go-gc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wirelessregistry/goprofui/HEAD/images/go-gc.png
--------------------------------------------------------------------------------
/images/memory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wirelessregistry/goprofui/HEAD/images/memory.png
--------------------------------------------------------------------------------
/images/go-flame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wirelessregistry/goprofui/HEAD/images/go-flame.png
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | Srdjan Marinovic, srdjan.marinovic@gmail.com, @a-little-srdjan
2 | Ryan Day, ryan@ryanday.net, @rday
3 |
--------------------------------------------------------------------------------
/html/components/flame-graph/flame-graph.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/html/dev.index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Goprofui Dashboard
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/html/README.md:
--------------------------------------------------------------------------------
1 | # goprofui ui
2 | The UI for goprofui. Uses knockout, d3 and stealjs.
3 |
4 | ## To build
5 | Run `npm install` and then `node build`. Will transpile and minify all less/es6 to files in the `dist` directory.
6 |
7 | ## Debugging and Development
8 | Serve this directory and navigate to `/dev.index.html`. This runs stealjs in dev mode, which builds on the fly and uses source maps for js and less.
--------------------------------------------------------------------------------
/html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Goprofui Dashboard
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tickerHandlers_test.go:
--------------------------------------------------------------------------------
1 | package goprofui
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestCollectStats(t *testing.T) {
9 | reader := func() []byte {
10 | return []byte("hello")
11 | }
12 |
13 | c := CollectStats(reader)
14 |
15 | rCh := c(1*time.Millisecond, nil)
16 | if rCh == nil {
17 | t.Fatalf("Failed to get a result channel")
18 | }
19 |
20 | result := <-rCh
21 | if string(result) != string(reader()) {
22 | t.Errorf("Expected %v. Got %v.", string(reader()), string(result))
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/html/util/list-factory.js:
--------------------------------------------------------------------------------
1 | export default ListFactory
2 |
3 | function ListFactory (length) {
4 | this.length = length || 10;
5 | this.store = [];
6 | this.push = (item)=>{
7 | this.store.push(item);
8 | if(this.store.length > this.length) {
9 | this.store.shift();
10 | }
11 | if (item > this.max || this.max == undefined) {
12 | this.max = item;
13 | }
14 | if ((item < this.min || this.min == undefined) && item > 0) {
15 | this.min = item;
16 | }
17 | return this.store;
18 | }
19 | this.clear = ()=>{
20 | this.store = [];
21 | this.max = undefined;
22 | this.min = undefined;
23 | }
24 | }
--------------------------------------------------------------------------------
/html/less/styles.less:
--------------------------------------------------------------------------------
1 | .controls.affix {
2 | z-index: 1000;
3 | left: 0; right: 0;
4 | }
5 |
6 | .navbar-text.profiling {
7 | color: #31708f;
8 | }
9 | .navbar-text.monitoring {
10 | color: #3c763d;
11 | }
12 |
13 | .graph {
14 | display: inline-block;
15 | }
16 | .graph .chart {
17 | width: 950px;
18 | }
19 | .graph .axis {
20 | stroke-width: 1;
21 | }
22 |
23 | .graph .axis .tick line {
24 | stroke: black;
25 | }
26 |
27 | .graph .axis .tick text {
28 | fill: black;
29 | font-size: 0.7em;
30 | }
31 |
32 | .graph .axis .domain {
33 | fill: none;
34 | stroke: black;
35 | }
36 |
37 | .graph .group {
38 | fill: none;
39 | stroke: black;
40 | stroke-width: 1.5;
41 | }
--------------------------------------------------------------------------------
/html/components/go-routines/go-routines.html:
--------------------------------------------------------------------------------
1 |
2 |
Go Routines
3 |
4 |
5 |
6 |
7 | | Max: |
8 | Min: |
9 |
10 |
11 |
12 |
13 | |
14 |
15 |
16 |
17 |
18 |
21 |
--------------------------------------------------------------------------------
/html/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "goprofui",
3 | "version": "0.0.1",
4 | "description": "UI for the goprofui go profiling/monitoring tool",
5 | "main": "app/app.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "system": {
10 | "npmAlgorithm": "flat",
11 | "transpiler": "babel"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/wirelessregistry/goprofui.git"
16 | },
17 | "author": "",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/wirelessregistry/goprofui/issues"
21 | },
22 | "homepage": "https://github.com/wirelessregistry/goprofui#readme",
23 | "dependencies": {
24 | "bootbox": "^4.4.0",
25 | "bootstrap": "^3.3.6",
26 | "d3": "^3.5.17",
27 | "jquery": "^2.2.4",
28 | "knockout": "^3.4.0",
29 | "steal": "^0.16.21",
30 | "steal-tools": "^0.16.4",
31 | "system-text": "^0.1.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/html/components/gc-pause/gc-pause.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | GC Pause
4 |
5 | Max: μs
6 | Min: μs
7 |
8 |
9 |
10 |
11 |
12 |
13 | | Pause |
14 | Time Elapsed |
15 |
16 |
17 |
18 |
19 | |
20 | |
21 |
22 |
23 |
24 |
25 |
28 |
--------------------------------------------------------------------------------
/html/util/socket-manager.js:
--------------------------------------------------------------------------------
1 | import bootbox from "bootbox"
2 |
3 | export default function SocketManager() {
4 | var self = this;
5 |
6 | self.connections = [];
7 |
8 | self.DisconnectAll = function() {
9 | for (var ws of self.connections) {
10 | ws.close();
11 | }
12 | };
13 |
14 | self.Connect = function(url, messageCallback) {
15 | var defer = $.Deferred();
16 | var sock = wsConnect(url, defer, messageCallback);
17 | self.connections.push(sock);
18 |
19 | return defer;
20 | };
21 | }
22 |
23 | function wsConnect(url, defer, messageFn) {
24 | var ws = null;
25 |
26 | if (WebSocket === undefined) {
27 | alert('You will need WebSockets to run the monitoring tools');
28 | } else {
29 | ws = new WebSocket(url);
30 | ws.onopen = function (e) {
31 | defer.resolve();
32 | };
33 |
34 | ws.onerror = function(e) {
35 | defer.reject();
36 | bootbox.alert("Websocket error");
37 | }
38 |
39 | ws.onmessage = messageFn;
40 | }
41 |
42 | return ws;
43 | }
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 wirelessregistry
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/flameHandlers.go:
--------------------------------------------------------------------------------
1 | package goprofui
2 |
3 | import (
4 | "bytes"
5 | "net/http"
6 | "runtime/pprof"
7 |
8 | "github.com/wirelessregistry/goprofui/internal/profile"
9 | )
10 |
11 | var cpuProfileBuffer bytes.Buffer // the lock is in pprof
12 |
13 | func reply(w http.ResponseWriter, status int, bytes []byte) {
14 | w.WriteHeader(status)
15 | w.Write(bytes)
16 | }
17 |
18 | func StartCPUProfHandler(w http.ResponseWriter, r *http.Request) {
19 | err := pprof.StartCPUProfile(&cpuProfileBuffer)
20 | if err != nil {
21 | reply(w, 400, []byte(err.Error()))
22 | return
23 | }
24 |
25 | w.Header().Set("Access-Control-Allow-Origin", "*")
26 | reply(w, 202, []byte("CPU profiling started."))
27 | }
28 |
29 | func StopCPUProfHandler(w http.ResponseWriter, r *http.Request) {
30 | pprof.StopCPUProfile()
31 |
32 | p, err := profile.Parse(&cpuProfileBuffer)
33 | if err != nil {
34 | reply(w, 400, []byte(err.Error()))
35 | return
36 | }
37 | w.Header().Set("Access-Control-Allow-Origin", "*")
38 |
39 | fp := NewProfile(p)
40 |
41 | var buf bytes.Buffer
42 | err = fp.ParseForD3FlameGraph(&buf)
43 | if err != nil {
44 | reply(w, 400, []byte(err.Error()))
45 | return
46 | }
47 |
48 | reply(w, 200, buf.Bytes())
49 | }
50 |
--------------------------------------------------------------------------------
/parse_test.go:
--------------------------------------------------------------------------------
1 | package goprofui
2 |
3 | import "testing"
4 |
5 | func TestParse(t *testing.T) {
6 | stack1 := []string{
7 | "func1",
8 | "func2",
9 | "func3",
10 | "func4",
11 | }
12 |
13 | stack2 := []string{
14 | "func1",
15 | "func2",
16 | "func3",
17 | "func5",
18 | }
19 |
20 | stack3 := []string{
21 | "func1",
22 | "func2",
23 | "func6",
24 | "func7",
25 | }
26 |
27 | node := &Node{
28 | Name: "root",
29 | Value: 0,
30 | Children: make(map[string]*Node),
31 | }
32 |
33 | node.Add(stack1, 1)
34 | node.Add(stack2, 1)
35 | node.Add(stack3, 1)
36 |
37 | if node.Value != 3 {
38 | t.Error("Root node should have a value of 3, found", node.Value)
39 | }
40 |
41 | if node.Children["func1"].Value != 3 {
42 | t.Error("func1 node should have a value of 3, found", node.Children["func1"].Value)
43 | }
44 |
45 | func3 := node.Children["func1"].Children["func2"].Children["func3"]
46 | if func3.Value != 2 {
47 | t.Error("func3 node should have a value of 2, found", func3.Value)
48 | }
49 |
50 | func6 := node.Children["func1"].Children["func2"].Children["func6"]
51 | if func6.Value != 1 {
52 | t.Error("func6 node should have a value of 1, found", func6.Value)
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/html/app/app.js:
--------------------------------------------------------------------------------
1 | import ko from "knockout"
2 | import $ from "jquery"
3 | import "bootstrap/dist/js/bootstrap"
4 | import "bootstrap/dist/css/bootstrap.css!"
5 | import "goprofui/less/styles.less!"
6 | import template from "./app.html!system-text"
7 |
8 | import ViewModel from "goprofui/app/app-state"
9 | import * as GcPause from "goprofui/components/gc-pause/"
10 | import * as GoRoutines from "goprofui/components/go-routines/"
11 | import * as MemoryAllocation from "goprofui/components/memory-allocation/"
12 | import * as FlameGraph from "goprofui/components/flame-graph/"
13 |
14 | ko.components.register('memory-allocation', { viewModel: MemoryAllocation.ViewModel, template: MemoryAllocation.Template });
15 | ko.components.register('gc-pause', { viewModel: GcPause.ViewModel, template: GcPause.Template });
16 | ko.components.register('go-routines', { viewModel: GoRoutines.ViewModel, template: GoRoutines.Template });
17 | ko.components.register('flame-graph', { viewModel: FlameGraph.ViewModel, template: FlameGraph.Template });
18 |
19 | $(document).ready(()=>{
20 |
21 | $('body').html(template);
22 |
23 | $('.controls').affix({
24 | offset: {
25 | top: 10
26 | }
27 | });
28 |
29 | let appState = new ViewModel();
30 | ko.applyBindings(appState);
31 |
32 | });
--------------------------------------------------------------------------------
/html/components/flame-graph/flame-graph.js:
--------------------------------------------------------------------------------
1 | import ko from "knockout"
2 | import d3 from "d3"
3 | import "goprofui/bindings/flame/"
4 | import Template from "./flame-graph.html!system-text"
5 |
6 | class ViewModel {
7 | constructor(params) {
8 | this.monitorIp = params.monitorIp || ko.observable('');
9 | this.profiling = params.profiling || ko.observable(false);
10 | this.flameData = ko.observableArray([]);
11 | this.profilingSub = this.profiling.subscribe(this.setupProfiling.bind(this));
12 | }
13 |
14 | handleResponse(req) {
15 | return (e)=>{
16 | if (req.readyState === XMLHttpRequest.DONE) {
17 | if (this.profiling()===false) {
18 | this.flameData(JSON.parse(req.responseText));
19 | }
20 | }
21 | }
22 | }
23 |
24 | setupProfiling(profiling) {
25 | let req = new XMLHttpRequest();
26 | req.onreadystatechange = this.handleResponse(req);
27 |
28 | if (profiling === true) {
29 | req.open('GET', '//' + this.monitorIp() + '/cpuprofile/start');
30 | } else {
31 | req.open('GET', '//' + this.monitorIp() + '/cpuprofile/stop');
32 | }
33 |
34 | req.send(null);
35 | }
36 |
37 | dispose () {
38 | this.profilingSub.dispose()
39 | }
40 | }
41 |
42 | export { Template, ViewModel }
43 |
--------------------------------------------------------------------------------
/html/app/app-state.js:
--------------------------------------------------------------------------------
1 | import ko from 'knockout'
2 | import SocketManager from "goprofui/util/socket-manager"
3 |
4 | class ViewModel {
5 | constructor() {
6 | this.monitorIp = ko.observable('');
7 | this.monitoring = ko.observable(false);
8 | this.profiling = ko.observable(false);
9 | this.monitorCounter = ko.observable('');
10 | this.profileCounter = ko.observable('');
11 |
12 | this.profilingStatus = ko.pureComputed(function() {
13 | return this.profiling() ? 'Stop Profiling': 'Profile';
14 | }, this);
15 | this.monitoringStatus = ko.pureComputed(function() {
16 | return this.monitoring() ? 'Stop Monitor': 'Monitor';
17 | }, this);
18 | }
19 |
20 | toggleMonitor(){
21 | clearInterval(this.monitorInterval)
22 | this.monitoring(!this.monitoring())
23 | if(this.monitoring()) {
24 | let tick = 0;
25 | this.monitorInterval = setInterval(()=>{
26 | this.monitorCounter(tick+=1);
27 | },1000);
28 | }
29 | }
30 |
31 | toggleProfiling(){
32 | clearInterval(this.profileInterval)
33 | this.profiling(!this.profiling())
34 | if(this.profiling()) {
35 | let tick = 0;
36 | this.profileInterval = setInterval(()=>{
37 | this.profileCounter(tick+=1);
38 | },1000);
39 | }
40 | }
41 |
42 | }
43 |
44 | export default ViewModel
--------------------------------------------------------------------------------
/examples/web/ko.flame.js:
--------------------------------------------------------------------------------
1 | ko.bindingHandlers.flame = {
2 | init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
3 | var height = allBindings.get('height') || 740;
4 | var width = allBindings.get('width') || 960;
5 | var title = allBindings.get('title') || 'Flame Graph';
6 | var transitionDuration = allBindings.get('transitionDuration') || 750;
7 | var data = ko.unwrap(valueAccessor());
8 |
9 | bindingContext.flameGraph = d3.flameGraph()
10 | .height(height)
11 | .width(width)
12 | .cellHeight(18)
13 | .transitionDuration(transitionDuration)
14 | .transitionEase('cubic-in-out')
15 | .sort(true)
16 | .title("")
17 |
18 | var tip = d3.tip()
19 | .direction("s")
20 | .offset([8, 0])
21 | .attr('class', 'd3-flame-graph-tip')
22 | .html(function(d) { return "name: " + d.name + ", value: " + d.value; });
23 |
24 | bindingContext.flameGraph.tooltip(tip);
25 | },
26 | update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
27 | var data = ko.unwrap(valueAccessor());
28 |
29 | d3.select(element).select('*').remove();
30 | d3.select(element)
31 | .datum(data)
32 | .call(bindingContext.flameGraph);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/html/components/memory-allocation/memory-allocation.html:
--------------------------------------------------------------------------------
1 |
2 |
Memory
3 |
4 |
5 |
6 |
7 | |
8 | Max |
9 | Min |
10 |
11 |
12 |
13 |
14 | | Requested |
15 | |
16 | |
17 |
18 |
19 | | In Use |
20 | |
21 | |
22 |
23 |
24 | | Idle |
25 | |
26 | |
27 |
28 |
29 | | Released |
30 | |
31 | |
32 |
33 |
34 |
35 |
36 |
39 |
--------------------------------------------------------------------------------
/html/build.js:
--------------------------------------------------------------------------------
1 | var stealTools = require("steal-tools");
2 | var unicorn = ''
3 | +' /((((((\\\\\\\\\\\n'
4 | +' ((((((((\\\\\\\\\\\\\\\n'
5 | +' (( \\\\\\\\\\\\\\\n'
6 | +'=======((* _/ \\\\\\\\\\\n'
7 | +' \\ / \\ \\\\\\\\\\________________\n'
8 | +' | | | ((\\\\\\\\\\\\\n'
9 | +' o_| / / \\\\ \\\\\\\\ \\\\\\\\\\\n'
10 | +' | ._ ( JS is Slooow \\\\ \\\\\\\\\\\\\\\\\\\\\\\n'
11 | +' | / / / \\\\\\\\\\ \\\\\\\n'
12 | +' .______/\\/ / / / \\\\\\\\\n'
13 | +' / __.____/ _/ ________( /\\\n'
14 | +' / / / ________/`---------\' \\ / \\_\n'
15 | +' / / \\ \\ \\ \\ \\_ \\\n'
16 | +' ( < \\ \\ > / \\ \\\n'
17 | +' \\/ \\ \\_ / / > )\n'
18 | +' \\_| / / / /\n'
19 | +' _// _//\n'
20 | +' /_| /_|\'\n';
21 | console.log(unicorn);
22 |
23 |
24 | var buildPromise = stealTools.build({
25 | config: __dirname + "/package.json!npm"
26 | }, {
27 | bundleAssets: {
28 | infer: true
29 | },
30 | minify: true,
31 | verbose: false
32 | });
--------------------------------------------------------------------------------
/html/app/app.html:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/cpu_test.go:
--------------------------------------------------------------------------------
1 | package goprofui
2 |
3 | import (
4 | "runtime"
5 | "strings"
6 | "testing"
7 | "time"
8 |
9 | "github.com/wirelessregistry/goprofui/internal/profile"
10 | )
11 |
12 | func getCaller() uintptr {
13 | pc, _, _, _ := runtime.Caller(1)
14 | return pc
15 | }
16 |
17 | func func1() uintptr {
18 | return getCaller()
19 | }
20 |
21 | func func2() uintptr {
22 | return getCaller()
23 | }
24 |
25 | func func3() uintptr {
26 | return getCaller()
27 | }
28 |
29 | func TestLocationsToFuncnames(t *testing.T) {
30 | locOne := profile.Location{
31 | ID: 1,
32 | Address: uint64(func1()),
33 | }
34 |
35 | locTwo := profile.Location{
36 | ID: 2,
37 | Address: uint64(func2()),
38 | }
39 |
40 | locThree := profile.Location{
41 | ID: 3,
42 | Address: uint64(func3()),
43 | }
44 |
45 | locations := []*profile.Location{&locOne, &locTwo, &locThree}
46 | locationMap := LocationsToFuncNames(locations)
47 |
48 | if strings.HasSuffix(locationMap[1], "func1") == false {
49 | t.Error("Looking for func1, not found in", locationMap[1])
50 | }
51 |
52 | if strings.HasSuffix(locationMap[2], "func2") == false {
53 | t.Error("Looking for func2, not found in", locationMap[2])
54 | }
55 |
56 | if strings.HasSuffix(locationMap[3], "func3") == false {
57 | t.Error("Looking for func3, not found in", locationMap[3])
58 | }
59 | }
60 |
61 | func TestCPUProfile(t *testing.T) {
62 | res := <-CPUProfile(1 * time.Second)
63 | if res == nil {
64 | t.Error("Failed to start CPUProfile.")
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/parse.go:
--------------------------------------------------------------------------------
1 | package goprofui
2 |
3 | import (
4 | "encoding/json"
5 | )
6 |
7 | // Used to construct the d3 flame graph representation
8 | type Node struct {
9 | Name string
10 | Value int64
11 | Children map[string]*Node
12 | }
13 |
14 | // Add will add a folded stack of functions to the node. Based on the stack
15 | // convert node script which builds data for the d3 flame graph.
16 | func (n *Node) Add(funcs []string, value int64) {
17 | n.Value += value
18 |
19 | if len(funcs) > 0 {
20 | var child *Node
21 | head := funcs[0]
22 |
23 | child, _ = n.Children[head]
24 | if child == nil {
25 | child = &Node{
26 | Name: head,
27 | Value: 0,
28 | Children: make(map[string]*Node),
29 | }
30 |
31 | n.Children[head] = child
32 | }
33 |
34 | funcs = funcs[1:]
35 | child.Add(funcs, value)
36 | }
37 | }
38 |
39 | // MarshalText will return JSON data of the node tree in the format required by
40 | // the D3 flame graph. Note that the D3 library wants arrays of children.
41 | func (n *Node) MarshalText() ([]byte, error) {
42 | mapped := n.formatForD3()
43 |
44 | return json.Marshal(mapped)
45 | }
46 |
47 | func (n *Node) formatForD3() map[string]interface{} {
48 | obj := make(map[string]interface{})
49 | children := make([]map[string]interface{}, 0)
50 |
51 | for _, child := range n.Children {
52 | mapped := child.formatForD3()
53 | children = append(children, mapped)
54 | }
55 |
56 | obj["name"] = n.Name
57 | obj["value"] = n.Value
58 | obj["children"] = children
59 |
60 | return obj
61 | }
62 |
--------------------------------------------------------------------------------
/LICENSE_internal_pkg:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 The Go Authors. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above
11 | copyright notice, this list of conditions and the following disclaimer
12 | in the documentation and/or other materials provided with the
13 | distribution.
14 |
15 | * Neither the name of Google Inc. nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/html/bindings/flame/flame.js:
--------------------------------------------------------------------------------
1 | import ko from "knockout"
2 | import d3 from "d3"
3 | import tip from "./js/d3-tip"
4 | import flameGraph from "./js/d3.flameGraph"
5 | import "./flame.css!"
6 |
7 | d3.tip = tip;
8 | d3.flameGraph = flameGraph;
9 |
10 | ko.bindingHandlers.flame = {
11 | init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
12 | var height = allBindings.get('height') || 740;
13 | var width = allBindings.get('width') || 960;
14 | var title = allBindings.get('title') || 'Flame Graph';
15 | var transitionDuration = allBindings.get('transitionDuration') || 750;
16 | var data = ko.unwrap(valueAccessor());
17 |
18 | bindingContext.flameGraph = d3.flameGraph()
19 | .height(height)
20 | .width(width)
21 | .cellHeight(18)
22 | .transitionDuration(transitionDuration)
23 | .transitionEase('cubic-in-out')
24 | .sort(true)
25 | .title("")
26 |
27 | var tip = d3.tip()
28 | .direction("s")
29 | .offset([8, 0])
30 | .attr('class', 'd3-flame-graph-tip')
31 | .html(function(d) { return "name: " + d.name + ", value: " + d.value; });
32 |
33 | bindingContext.flameGraph.tooltip(tip);
34 | },
35 | update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
36 | var data = ko.unwrap(valueAccessor());
37 |
38 | d3.select(element).select('*').remove();
39 | d3.select(element)
40 | .datum(data)
41 | .call(bindingContext.flameGraph);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/examples/web/web.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/wirelessregistry/goprofui"
5 | "golang.org/x/net/websocket"
6 | "log"
7 | "net/http"
8 | "sort"
9 | "time"
10 | )
11 |
12 | func fib(pos int) int {
13 | if pos <= 1 {
14 | return 1
15 | }
16 |
17 | return fib(pos-1) + fib(pos-2)
18 | }
19 |
20 | func reverse(words []string) {
21 | for i, j := 0, len(words)-1; i < j; i, j = i+1, j-1 {
22 | words[i], words[j] = words[j], words[i]
23 | }
24 | }
25 |
26 | func sortInts(max int) {
27 | list := make([]int, max)
28 | for j := max - 1; j >= 0; j-- {
29 | list[j] = max - j
30 | }
31 | sort.Ints(list)
32 | }
33 |
34 | func main() {
35 | profileMux := http.NewServeMux()
36 | profileMux.HandleFunc("/cpuprofile/start", goprofui.StartCPUProfHandler)
37 | profileMux.HandleFunc("/cpuprofile/stop", goprofui.StopCPUProfHandler)
38 | profileMux.Handle("/memory", websocket.Handler(goprofui.MemoryHandler(1*time.Second)))
39 | profileMux.Handle("/gc", websocket.Handler(goprofui.GCHandler(1*time.Second)))
40 | profileMux.Handle("/goroutines", websocket.Handler(goprofui.GoRoutinesHandler(1*time.Second)))
41 |
42 | go func() {
43 | for {
44 | fib(25)
45 |
46 | sortInts(10000)
47 |
48 | for i := 0; i < 10000; i++ {
49 | reverse([]string{"qwertyiolkahsdlfnalhgslhldsbndsadasdsajdasasdjljk", "dsajkfhkadhfhlsahlghglafshgfakdgnadflhgbliadflnubfnbliadfuhbnlai"})
50 | }
51 | }
52 | }()
53 |
54 | go func() {
55 | for i := 0; i < 1000; i++ {
56 | go func(id int) {
57 | _ = make([]byte, 1000000)
58 | time.Sleep(10 * time.Second)
59 | }(i)
60 |
61 | time.Sleep(1 * time.Second)
62 | }
63 | }()
64 |
65 | if err := http.ListenAndServe(":6060", profileMux); err != nil {
66 | log.Fatalf("%s \n", err.Error())
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/html/components/go-routines/go-routines.js:
--------------------------------------------------------------------------------
1 | import ko from "knockout"
2 | import d3 from "d3"
3 | import SocketManager from "goprofui/util/socket-manager"
4 | import ListFactory from "goprofui/util/list-factory"
5 | import Template from "./go-routines.html!system-text"
6 | import "./go-routines.less!"
7 |
8 | class ViewModel {
9 | constructor(params) {
10 | this.monitorIp = params.monitorIp || ko.observable('');
11 | this.manager = new SocketManager();
12 | this.monitoring = params.monitoring || ko.observable(false);
13 | this.showCharts = ko.observable(true);
14 | this.goHistory = ko.observable(new ListFactory(6));
15 | this.goRoutineBucket = {};
16 | this.goRoutineGroups = {
17 | 'go': {
18 | key: 'Go Routines',
19 | value: 0,
20 | color: 'blue',
21 | data: d3.range(60).map(function() {
22 | return 0
23 | })
24 | }
25 | }
26 | this.goRoutineData = ko.observable({'go': 0});
27 |
28 | this.monitoringWatch = this.monitoring.subscribe((monitoring)=>{
29 | if (!monitoring) {
30 | this.manager.DisconnectAll();
31 |
32 | this.goRoutineGroups['go'].data = d3.range(60).map(function() {
33 | return 0
34 | });
35 |
36 | return;
37 | }
38 |
39 | this.showCharts(false);
40 | this.goHistory().clear();
41 |
42 | this.manager.Connect(`ws://${this.monitorIp()}/goroutines`, (e)=>{
43 | let splitData = e.data.split(',');
44 | let obj = {
45 | 'go': parseInt(splitData[0])
46 | }
47 | this.goHistory().push(obj.go);
48 | this.goHistory.valueHasMutated();
49 | this.goRoutineData(obj);
50 | });
51 |
52 | setTimeout(()=>this.showCharts(true), 100)
53 | });
54 | }
55 |
56 | dispose() {
57 | this.monitoringWatch.dispose();
58 | }
59 | }
60 |
61 | export { Template, ViewModel }
62 |
--------------------------------------------------------------------------------
/examples/web/d3.flameGraph.css:
--------------------------------------------------------------------------------
1 | .d3-flame-graph rect {
2 | stroke: #EEEEEE;
3 | fill-opacity: .8;
4 | }
5 |
6 | .d3-flame-graph rect:hover {
7 | stroke: #474747;
8 | stroke-width: 0.5;
9 | cursor: pointer;
10 | }
11 |
12 | .d3-flame-graph .label {
13 | pointer-events: none;
14 | white-space: nowrap;
15 | text-overflow: ellipsis;
16 | overflow: hidden;
17 | font-size: 12px;
18 | font-family: Verdana;
19 | margin-left: 4px;
20 | margin-right: 4px;
21 | line-height: 1.5;
22 | padding: 0 0 0;
23 | font-weight: 400;
24 | color: black;
25 | text-align: left;
26 | }
27 |
28 | .d3-flame-graph .fade {
29 | opacity: 0.6 !important;
30 | }
31 |
32 | .d3-flame-graph .title {
33 | font-size: 20px;
34 | font-family: Verdana;
35 | }
36 |
37 | .d3-flame-graph-tip {
38 | line-height: 1;
39 | font-family: Verdana;
40 | font-size: 12px;
41 | padding: 12px;
42 | background: rgba(0, 0, 0, 0.8);
43 | color: #fff;
44 | border-radius: 2px;
45 | pointer-events: none;
46 | }
47 |
48 | /* Creates a small triangle extender for the tooltip */
49 | .d3-flame-graph-tip:after {
50 | box-sizing: border-box;
51 | display: inline;
52 | font-size: 10px;
53 | width: 100%;
54 | line-height: 1;
55 | color: rgba(0, 0, 0, 0.8);
56 | position: absolute;
57 | pointer-events: none;
58 | }
59 |
60 | /* Northward tooltips */
61 | .d3-flame-graph-tip.n:after {
62 | content: "\25BC";
63 | margin: -1px 0 0 0;
64 | top: 100%;
65 | left: 0;
66 | text-align: center;
67 | }
68 |
69 | /* Eastward tooltips */
70 | .d3-flame-graph-tip.e:after {
71 | content: "\25C0";
72 | margin: -4px 0 0 0;
73 | top: 50%;
74 | left: -8px;
75 | }
76 |
77 | /* Southward tooltips */
78 | .d3-flame-graph-tip.s:after {
79 | content: "\25B2";
80 | margin: 0 0 1px 0;
81 | top: -8px;
82 | left: 0;
83 | text-align: center;
84 | }
85 |
86 | /* Westward tooltips */
87 | .d3-flame-graph-tip.w:after {
88 | content: "\25B6";
89 | margin: -4px 0 0 -1px;
90 | top: 50%;
91 | left: 100%;
92 | }
--------------------------------------------------------------------------------
/html/bindings/flame/flame.css:
--------------------------------------------------------------------------------
1 | .d3-flame-graph rect {
2 | stroke: #EEEEEE;
3 | fill-opacity: .8;
4 | }
5 |
6 | .d3-flame-graph rect:hover {
7 | stroke: #474747;
8 | stroke-width: 0.5;
9 | cursor: pointer;
10 | }
11 |
12 | .d3-flame-graph .label {
13 | pointer-events: none;
14 | white-space: nowrap;
15 | text-overflow: ellipsis;
16 | overflow: hidden;
17 | font-size: 12px;
18 | font-family: Verdana;
19 | margin-left: 4px;
20 | margin-right: 4px;
21 | line-height: 1.5;
22 | padding: 0 0 0;
23 | font-weight: 400;
24 | color: black;
25 | text-align: left;
26 | }
27 |
28 | .d3-flame-graph .fade {
29 | opacity: 0.6 !important;
30 | }
31 |
32 | .d3-flame-graph .title {
33 | font-size: 20px;
34 | font-family: Verdana;
35 | }
36 |
37 | .d3-flame-graph-tip {
38 | line-height: 1;
39 | font-family: Verdana;
40 | font-size: 12px;
41 | padding: 12px;
42 | background: rgba(0, 0, 0, 0.8);
43 | color: #fff;
44 | border-radius: 2px;
45 | pointer-events: none;
46 | }
47 |
48 | /* Creates a small triangle extender for the tooltip */
49 | .d3-flame-graph-tip:after {
50 | box-sizing: border-box;
51 | display: inline;
52 | font-size: 10px;
53 | width: 100%;
54 | line-height: 1;
55 | color: rgba(0, 0, 0, 0.8);
56 | position: absolute;
57 | pointer-events: none;
58 | }
59 |
60 | /* Northward tooltips */
61 | .d3-flame-graph-tip.n:after {
62 | content: "\25BC";
63 | margin: -1px 0 0 0;
64 | top: 100%;
65 | left: 0;
66 | text-align: center;
67 | }
68 |
69 | /* Eastward tooltips */
70 | .d3-flame-graph-tip.e:after {
71 | content: "\25C0";
72 | margin: -4px 0 0 0;
73 | top: 50%;
74 | left: -8px;
75 | }
76 |
77 | /* Southward tooltips */
78 | .d3-flame-graph-tip.s:after {
79 | content: "\25B2";
80 | margin: 0 0 1px 0;
81 | top: -8px;
82 | left: 0;
83 | text-align: center;
84 | }
85 |
86 | /* Westward tooltips */
87 | .d3-flame-graph-tip.w:after {
88 | content: "\25B6";
89 | margin: -4px 0 0 -1px;
90 | top: 50%;
91 | left: 100%;
92 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # goprofui
2 |
3 | A simple package for profiling _deployed_ binaries.
4 |
5 | Key features:
6 | * Does not require running any other scripts/tools, only the binary under monitoring.
7 | * Flame graphs for stack trace profiling.
8 | * Memory, GC pauses and goroutines line charts.
9 |
10 |
11 | ### Related Work
12 | * go-torch. _https://github.com/uber/go-torch_. go-torch does cpu profiling and represents it as a flame graph.
13 | We were directly inspired by this package. In contrast to goprofui, go-torch requires the go pprof tool and an external Perl script.
14 |
15 | * profile. _https://github.com/pkg/profile_. profile does cpu and memory profiling, and saves them into a file. It is easy to setup and use. However, the cpu profiles would have to be further parsed for a flame graph.
16 |
17 | * flame graphs. _http://www.brendangregg.com/flamegraphs.html_. Created by Brendan Gregg. An extremely succinct way to summarize stack traces.
18 |
19 | ### Installation
20 |
21 | ```
22 | go get github.com/wirelessregistry/goprofui
23 | ```
24 |
25 | ### Dependencies
26 |
27 | 1. Requires the net web-socket library _https://godoc.org/golang.org/x/net/websocket_.
28 |
29 | 2. The flame graph JS library is https://github.com/spiermar/d3-flame-graph.
30 |
31 | 3. The line chart JS library is D3's.
32 |
33 | 4. goprofui's internal package is forked from pprof's internal package. It is covered by the BSD-license in
34 | LICENSE_internal_pkg.
35 |
36 |
37 | ### Usage and Code Structure
38 |
39 | The quickest way to get started is to copy the handlers' setup from _examples/web/web.go_. Build the web.go application, and then serve the index.html file using a web server (_https://github.com/indexzero/http-server_ for example).
40 |
41 | When you visit the example using your web browser, you will see some graphs:
42 |
43 | * 
44 | * 
45 | * 
46 |
47 | The cpu profiling code is in _cpu.go_. The memory, GC latency and goroutines stats are obtained in tickerHandlers.go.
48 |
--------------------------------------------------------------------------------
/tickerHandlers.go:
--------------------------------------------------------------------------------
1 | package goprofui
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | "time"
7 |
8 | "golang.org/x/net/websocket"
9 | )
10 |
11 | type collectFn func(time.Duration, chan struct{}) chan []byte
12 |
13 | func MemoryHandler(freq time.Duration) func(*websocket.Conn) {
14 | return TickerHandler(freq, CollectMemStats())
15 | }
16 |
17 | func GCHandler(freq time.Duration) func(*websocket.Conn) {
18 | return TickerHandler(freq, CollectGCStats())
19 | }
20 |
21 | func CollectGCStats() collectFn {
22 | return CollectStats(
23 | func() []byte {
24 | var m runtime.MemStats
25 |
26 | runtime.ReadMemStats(&m)
27 | stats := fmt.Sprintf("%d", m.PauseNs[(m.NumGC+255)%256])
28 | return []byte(stats)
29 | })
30 | }
31 |
32 | func CollectMemStats() collectFn {
33 | return CollectStats(
34 | func() []byte {
35 | var m runtime.MemStats
36 |
37 | runtime.ReadMemStats(&m)
38 | stats := fmt.Sprintf("%d,%d,%d,%d", m.HeapSys, m.HeapAlloc, m.HeapIdle, m.HeapReleased)
39 | return []byte(stats)
40 | })
41 | }
42 |
43 | func GoRoutinesHandler(freq time.Duration) func(*websocket.Conn) {
44 | return TickerHandler(freq, CollectRoutinesStats())
45 | }
46 |
47 | func CollectRoutinesStats() collectFn {
48 | return CollectStats(
49 | func() []byte {
50 | n := runtime.NumGoroutine()
51 | stats := fmt.Sprintf("%d", n)
52 | return []byte(stats)
53 | })
54 | }
55 |
56 | func TickerHandler(freq time.Duration, fn collectFn) func(*websocket.Conn) {
57 | return func(ws *websocket.Conn) {
58 | doneCh := make(chan struct{}, 1)
59 | data := fn(freq, doneCh)
60 |
61 | for {
62 | d := <-data
63 | _, err := ws.Write(d)
64 |
65 | if err != nil {
66 | // client has moved on
67 | doneCh <- struct{}{}
68 | return
69 | }
70 | }
71 | }
72 | }
73 |
74 | func CollectStats(reader func() []byte) collectFn {
75 | return func(freq time.Duration, doneCh chan struct{}) chan []byte {
76 | ch := make(chan []byte, 1)
77 | ticker := time.NewTicker(freq)
78 |
79 | ch <- reader() // Send some data right away
80 | go func() {
81 | for {
82 | select {
83 |
84 | case <-ticker.C:
85 | ch <- reader()
86 |
87 | case <-doneCh:
88 | return
89 | }
90 | }
91 | }()
92 |
93 | return ch
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/html/components/gc-pause/gc-pause.js:
--------------------------------------------------------------------------------
1 | import ko from "knockout"
2 | import d3 from "d3"
3 | import SocketManager from "goprofui/util/socket-manager"
4 | import Template from "./gc-pause.html!system-text"
5 | import ListFactory from "goprofui/util/list-factory"
6 | import "./gc-pause.less!"
7 |
8 | class ViewModel {
9 | constructor(params) {
10 |
11 | this.monitorIp = params.monitorIp || ko.observable('');
12 | this.manager = new SocketManager();
13 | this.monitoring = params.monitoring || ko.observable(false);
14 | this.showCharts = ko.observable(true);
15 |
16 | this.gcHistory = ko.observable(new ListFactory(10));
17 | this.gcTimeHistory = ko.observable(new ListFactory(10));
18 |
19 | this.gcBucket = {};
20 | this.gcGroups = {
21 | 'gc': {
22 | key: 'GC Pause',
23 | value: 0,
24 | color: 'blue',
25 | data: d3.range(60).map(function() {
26 | return 0
27 | })
28 | }
29 | }
30 | this.gcData = ko.observable({'gc': 0});
31 |
32 | this.monitoringWatch = this.monitoring.subscribe((monitoring)=>{
33 |
34 | if (!monitoring) {
35 | this.manager.DisconnectAll();
36 | this.gcGroups['gc'].data = d3.range(60).map(function() {
37 | return 0
38 | });
39 | return;
40 | }
41 |
42 | this.showCharts(false);
43 | this.gcHistory().clear();
44 | this.gcTimeHistory().clear();
45 |
46 | let lastNotableGc = 0;
47 | let start = new Date().getTime();
48 | let end = false;
49 |
50 | this.manager.Connect(`ws://${this.monitorIp()}/gc`, (e)=>{
51 | let store = this.gcHistory().store;
52 | let splitData = e.data.split(',');
53 | let obj = {
54 | 'gc': parseInt(splitData[0])/1000
55 | }
56 |
57 | //if we get the same gc pause length
58 | //render a flat line
59 | if (obj.gc == lastNotableGc) {
60 | obj.gc = 0;
61 |
62 | //Otherwise render the new data point
63 | } else {
64 | lastNotableGc = obj.gc;
65 | this.gcHistory().push(obj.gc);
66 |
67 | if(!end) {
68 | end = new Date().getTime();
69 | this.gcTimeHistory().push('--');
70 | } else {
71 | end = new Date().getTime();
72 | this.gcTimeHistory().push(end - start);
73 | }
74 |
75 | this.gcHistory.valueHasMutated();
76 | this.gcTimeHistory.valueHasMutated();
77 |
78 | start = new Date().getTime();
79 | }
80 |
81 | this.gcData(obj);
82 | });
83 |
84 | setTimeout(()=>this.showCharts(true), 100)
85 | });
86 | }
87 |
88 | dispose() {
89 | this.monitoringWatch.dispose();
90 | }
91 | }
92 |
93 | export { Template, ViewModel }
94 |
--------------------------------------------------------------------------------
/html/components/memory-allocation/memory-allocation.js:
--------------------------------------------------------------------------------
1 | import ko from "knockout"
2 | import d3 from "d3"
3 | import ListFactory from "goprofui/util/list-factory"
4 | import SocketManager from "goprofui/util/socket-manager"
5 | import "goprofui/bindings/ko.linechart"
6 | import Template from "./memory-allocation.html!system-text"
7 | import "./memory-allocation.less!"
8 |
9 | class ViewModel {
10 | constructor(params) {
11 | this.monitorIp = params.monitorIp || ko.observable('');
12 | this.manager = new SocketManager();
13 | this.monitoring = params.monitoring || ko.observable(false);
14 | this.showCharts = ko.observable(true);
15 | this.memHistory = ko.observable({
16 | 'system': new ListFactory(6),
17 | 'allocated': new ListFactory(6),
18 | 'idle': new ListFactory(6),
19 | 'released': new ListFactory(6)
20 | });
21 |
22 | this.memoryBucket = {};
23 | this.memoryGroups = {
24 | 'system': {
25 | key: 'Requested',
26 | value: 0,
27 | color: 'blue',
28 | data: d3.range(60).map(function() {
29 | return 0
30 | })
31 | },
32 | 'allocated': {
33 | key: 'In Use',
34 | value: 0,
35 | color: 'red',
36 | data: d3.range(60).map(function() {
37 | return 0
38 | })
39 | },
40 | 'idle': {
41 | key: 'Idle',
42 | value: 0,
43 | color: 'grey',
44 | data: d3.range(60).map(function() {
45 | return 0
46 | })
47 | },
48 | 'released': {
49 | key: 'Released',
50 | value: 0,
51 | color: 'green',
52 | data: d3.range(60).map(function() {
53 | return 0
54 | })
55 | }
56 | };
57 | this.memoryData = ko.observable({
58 | 'system': 0,
59 | 'allocated': 0,
60 | 'idle': 0,
61 | 'released': 0
62 | });
63 |
64 | this.monitoringWatch = this.monitoring.subscribe((monitoring)=>{
65 | if (!monitoring) {
66 | this.manager.DisconnectAll();
67 | for (var k in this.memoryGroups) {
68 | this.memoryGroups[k].data = d3.range(60).map(function() {
69 | return 0
70 | })
71 | this.memoryGroups[k].value = 0;
72 | }
73 | return;
74 | }
75 |
76 | this.showCharts(false);
77 | this.memHistory().system.clear();
78 | this.memHistory().allocated.clear();
79 | this.memHistory().idle.clear();
80 | this.memHistory().released.clear();
81 |
82 |
83 | this.manager.Connect(`ws://${this.monitorIp()}/memory`, (e)=>{
84 | var splitData = e.data.split(',');
85 | var obj = {
86 | 'system': splitData[0]/1000,
87 | 'allocated': splitData[1]/1000,
88 | 'idle': splitData[2]/1000,
89 | 'released': splitData[3]/1000
90 | }
91 | this.memHistory().system.push(obj.system);
92 | this.memHistory().allocated.push(obj.allocated);
93 | this.memHistory().idle.push(obj.idle);
94 | this.memHistory().released.push(obj.released);
95 | this.memHistory.valueHasMutated();
96 | this.memoryData(obj);
97 | });
98 |
99 | setTimeout(()=>this.showCharts(true), 100)
100 | });
101 |
102 | }
103 |
104 | dispose () {
105 | this.monitoringWatch.dispose();
106 | }
107 | }
108 |
109 | export { Template, ViewModel }
110 |
--------------------------------------------------------------------------------
/cpu.go:
--------------------------------------------------------------------------------
1 | package goprofui
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "io"
7 | "runtime"
8 | "runtime/pprof"
9 | "time"
10 |
11 | "github.com/wirelessregistry/goprofui/internal/profile"
12 | )
13 |
14 | var ErrProfileFailed = errors.New("Could not start profiler")
15 |
16 | type Profile struct {
17 | TotalSamples uint32 // count of samples
18 | TotalFuncCalls uint32 // count of all funcs in all sample
19 | FuncFrequency map[string]uint32
20 | UniqueFuncFrequency map[string]uint32
21 | FuncDict map[uint64]string
22 | Prof *profile.Profile
23 | }
24 |
25 | func EmptyProfile(prof *profile.Profile) *Profile {
26 | return &Profile{
27 | TotalSamples: 0,
28 | TotalFuncCalls: 0,
29 | FuncFrequency: make(map[string]uint32),
30 | UniqueFuncFrequency: make(map[string]uint32),
31 | FuncDict: nil,
32 | Prof: prof,
33 | }
34 | }
35 |
36 | func NewProfile(p *profile.Profile) *Profile {
37 | fp := EmptyProfile(p)
38 |
39 | fp.FuncDict = LocationsToFuncNames(p.Location)
40 |
41 | for _, sample := range p.Sample {
42 | nSamples := uint32(sample.Value[0])
43 | fp.TotalSamples += nSamples
44 | fp.TotalFuncCalls += nSamples * uint32(len(sample.Location))
45 |
46 | seenFunc := make(map[string]bool)
47 |
48 | for _, loc := range sample.Location {
49 | fp.FuncFrequency[fp.FuncDict[loc.ID]] += nSamples
50 |
51 | if seenFunc[fp.FuncDict[loc.ID]] == false {
52 | fp.UniqueFuncFrequency[fp.FuncDict[loc.ID]] += 1
53 | seenFunc[fp.FuncDict[loc.ID]] = true
54 | }
55 | }
56 | }
57 |
58 | return fp
59 | }
60 |
61 | func LocationsToFuncNames(locations []*profile.Location) map[uint64]string {
62 | funcs := make(map[uint64]string)
63 |
64 | for _, loc := range locations {
65 | funcs[loc.ID] = runtime.FuncForPC(uintptr(loc.Address)).Name()
66 | }
67 |
68 | return funcs
69 | }
70 |
71 | /*
72 | The D3 Flame Graph is at: https://github.com/spiermar/d3-flame-graph
73 |
74 | The D3 lib expects the following tree struct:
75 | 1) root --> *children
76 | 2) child --> *children
77 |
78 | Invariant:
79 | If a Func name appears at the same stack level in two samples and
80 | the call stack prefix is also the same, then
81 | there is exactly one node at that level in the tree to denote Func.
82 | */
83 | func (p *Profile) ParseForD3FlameGraph(w io.Writer) error {
84 | root := &Node{
85 | Name: "root",
86 | Value: int64(0),
87 | Children: make(map[string]*Node),
88 | }
89 |
90 | for _, sample := range p.Prof.Sample {
91 | frames := make([]string, len(sample.Location))
92 | for i, loc := range sample.Location {
93 | frames[i] = p.FuncDict[loc.ID]
94 | }
95 |
96 | reverse(frames)
97 | root.Add(frames, sample.Value[0])
98 | }
99 |
100 | out, err := root.MarshalText()
101 | if err != nil {
102 | return err
103 | }
104 |
105 | w.Write(out)
106 | return nil
107 | }
108 |
109 | func reverse(words []string) {
110 | for i, j := 0, len(words)-1; i < j; i, j = i+1, j-1 {
111 | words[i], words[j] = words[j], words[i]
112 | }
113 | }
114 |
115 | // Non-blocking function to get a profile
116 | func CPUProfile(duration time.Duration) chan *Profile {
117 | var buf bytes.Buffer
118 | ch := make(chan *Profile, 1)
119 |
120 | go func() {
121 | err := pprof.StartCPUProfile(&buf)
122 | if err != nil {
123 | // StartCPUProfile failed, so no writes yet.
124 | // Can change header back to text content
125 | // and send error code.
126 | ch <- nil
127 | }
128 |
129 | time.Sleep(duration)
130 | pprof.StopCPUProfile()
131 | p, _ := profile.Parse(&buf)
132 | ch <- NewProfile(p)
133 | }()
134 |
135 | return ch
136 | }
137 |
--------------------------------------------------------------------------------
/examples/web/ko.linechart.js:
--------------------------------------------------------------------------------
1 | ko.bindingHandlers.linechart = {
2 | init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
3 | var data = ko.unwrap(valueAccessor());
4 | var bucket = allBindings.get('bucket');
5 |
6 | bucket.title = allBindings.get('title');
7 | bucket.groups = allBindings.get('groups');
8 | bucket.width = allBindings.get('width') || 750;
9 | bucket.height = allBindings.get('height') || 350;
10 | bucket.duration = allBindings.get('duration') || 1000;
11 | bucket.limit = 60;
12 | bucket.now = new Date(Date.now() - bucket.duration);
13 |
14 | bucket.x = d3.time.scale()
15 | .domain([bucket.now - (bucket.limit - 2), bucket.now - bucket.duration])
16 | .range([0, bucket.width]);
17 |
18 | bucket.maxY = 1;
19 | bucket.y = d3.scale.linear()
20 | .domain([0, bucket.maxY])
21 | .range([bucket.height, 0])
22 |
23 | bucket.line = d3.svg.line()
24 | .interpolate('basis')
25 | .x(function(d, i) {
26 | return bucket.x(bucket.now - (bucket.limit - 1 - i) * bucket.duration)
27 | })
28 | .y(function(d) {
29 | return bucket.y(d)
30 | })
31 |
32 | bucket.svg = d3.select(element).append('svg')
33 | .attr('class', 'chart')
34 | .attr('width', bucket.width)
35 | .attr('height', bucket.height + 50)
36 |
37 | bucket.axis = bucket.svg.append('g')
38 | .attr('class', 'x axis')
39 | .attr('transform', 'translate(0,' + bucket.height + ')')
40 | .call(bucket.x.axis = d3.svg.axis().scale(bucket.x).orient('bottom'))
41 |
42 | bucket.paths = bucket.svg.append('g')
43 | bucket.labels = bucket.svg.append('g')
44 |
45 | for (var name in bucket.groups) {
46 | var group = bucket.groups[name]
47 | group.data.push(data[name]);
48 | group.path = bucket.paths.append('path')
49 | .data([group.data])
50 | .attr('class', name + ' group')
51 | .style('stroke', group.color);
52 | }
53 | },
54 | update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
55 | var data = ko.unwrap(valueAccessor());
56 | var bucket = allBindings.get('bucket');
57 | bucket.now = new Date();
58 |
59 | // Add new values
60 | bucket.labels.selectAll('text').remove();
61 | var i = 0;
62 | for (var name in bucket.groups) {
63 | var group = bucket.groups[name]
64 | //group.data.push(group.value) // Real values arrive at irregular intervals
65 | if (data[name] > bucket.maxY) {
66 | bucket.maxY = data[name] * 1.25;
67 | bucket.y.domain([0, bucket.maxY]);
68 | }
69 |
70 | group.data.push(data[name])
71 | group.path.attr('d', bucket.line)
72 | group.name = bucket.labels.append('text')
73 | .attr("transform", "translate(20," + bucket.y(data[name]) + ")")
74 | .attr("x", bucket.width + 40)
75 | .attr("dy", "1.25em")
76 | .text(group.key + "(" + data[name] + ")")
77 | .style('color', group.color)
78 | i++;
79 | }
80 |
81 | // Shift domain
82 | bucket.x.domain([
83 | bucket.now - (bucket.limit - 2) * bucket.duration,
84 | bucket.now - bucket.duration
85 | ])
86 |
87 | // Slide x-axis left
88 | bucket.axis.transition()
89 | .duration(bucket.duration)
90 | .ease('linear')
91 | .call(bucket.x.axis)
92 |
93 | // Slide paths left
94 | bucket.paths.attr('transform', null)
95 | .transition()
96 | .duration(bucket.duration)
97 | .ease('linear')
98 | .attr('transform', 'translate(' + bucket.x(bucket.now - (bucket.limit - 1) * bucket.duration) + ')')
99 |
100 | // Remove oldest data point from each group
101 | for (var name in bucket.groups) {
102 | var group = bucket.groups[name]
103 | group.data.shift()
104 | }
105 | }
106 | };
107 |
--------------------------------------------------------------------------------
/html/bindings/ko.linechart.js:
--------------------------------------------------------------------------------
1 | import ko from "knockout"
2 | import d3 from "d3"
3 |
4 | ko.bindingHandlers.linechart = {
5 | init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
6 | var data = ko.unwrap(valueAccessor());
7 | var bucket = allBindings.get('bucket');
8 |
9 | bucket.title = allBindings.get('title');
10 | bucket.groups = allBindings.get('groups');
11 | bucket.width = (ko.unwrap(allBindings.get('width')) - 0) || 750;
12 | bucket.height = (ko.unwrap(allBindings.get('height')) - 0) || 350;
13 | bucket.duration = (ko.unwrap(allBindings.get('duration')) - 0) || 1000;
14 | bucket.limit = 60;
15 | bucket.now = new Date(Date.now() - bucket.duration);
16 |
17 | bucket.x = d3.time.scale()
18 | .domain([bucket.now - (bucket.limit - 2), bucket.now - bucket.duration])
19 | .range([0, bucket.width]);
20 |
21 | bucket.maxY = 1;
22 | bucket.y = d3.scale.linear()
23 | .domain([0, bucket.maxY])
24 | .range([bucket.height, 0])
25 |
26 | bucket.line = d3.svg.line()
27 | .interpolate('basis')
28 | .x(function(d, i) {
29 | return bucket.x(bucket.now - (bucket.limit - 1 - i) * bucket.duration)
30 | })
31 | .y(function(d) {
32 | return bucket.y(d)
33 | })
34 |
35 | bucket.svg = d3.select(element).append('svg')
36 | .attr('class', 'chart')
37 | .attr('width', bucket.width)
38 | .attr('height', bucket.height + 50)
39 |
40 | bucket.axis = bucket.svg.append('g')
41 | .attr('class', 'x axis')
42 | .attr('transform', 'translate(0,' + bucket.height + ')')
43 | .call(bucket.x.axis = d3.svg.axis().scale(bucket.x).orient('bottom'))
44 |
45 | bucket.paths = bucket.svg.append('g')
46 | bucket.labels = bucket.svg.append('g')
47 |
48 | for (var name in bucket.groups) {
49 | var group = bucket.groups[name]
50 | group.data.push(data[name]);
51 | group.path = bucket.paths.append('path')
52 | .data([group.data])
53 | .attr('class', name + ' group')
54 | .style('stroke', group.color);
55 | }
56 | },
57 | update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
58 | var data = ko.unwrap(valueAccessor());
59 | var bucket = allBindings.get('bucket');
60 | bucket.now = new Date();
61 |
62 | // Add new values
63 | bucket.labels.selectAll('text').remove();
64 | var i = 0;
65 | for (var name in bucket.groups) {
66 | var group = bucket.groups[name]
67 | //group.data.push(group.value) // Real values arrive at irregular intervals
68 | if (data[name] > bucket.maxY) {
69 | bucket.maxY = data[name] * 1.25;
70 | bucket.y.domain([0, bucket.maxY]);
71 | }
72 |
73 | group.data.push(data[name])
74 | group.path.attr('d', bucket.line)
75 | group.name = bucket.labels.append('text')
76 | .attr("transform", "translate(20," + bucket.y(data[name]) + ")")
77 | .attr("x", bucket.width + 40)
78 | .attr("dy", "1.25em")
79 | .text(group.key + "(" + data[name] + ")")
80 | .style('color', group.color)
81 | i++;
82 | }
83 |
84 | // Shift domain
85 | bucket.x.domain([
86 | bucket.now - (bucket.limit - 2) * bucket.duration,
87 | bucket.now - bucket.duration
88 | ])
89 |
90 | // Slide x-axis left
91 | bucket.axis.transition()
92 | .duration(bucket.duration)
93 | .ease('linear')
94 | .call(bucket.x.axis)
95 |
96 | // Slide paths left
97 | bucket.paths.attr('transform', null)
98 | .transition()
99 | .duration(bucket.duration)
100 | .ease('linear')
101 | .attr('transform', 'translate(' + bucket.x(bucket.now - (bucket.limit - 1) * bucket.duration) + ')')
102 |
103 | // Remove oldest data point from each group
104 | for (var name in bucket.groups) {
105 | var group = bucket.groups[name]
106 | group.data.shift()
107 | }
108 | }
109 | };
110 |
--------------------------------------------------------------------------------
/examples/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
64 |
65 |
66 |
67 |
68 |
69 |
75 |
78 |
81 |
84 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
248 |
249 |
250 |
--------------------------------------------------------------------------------
/html/bindings/flame/js/d3-tip.js:
--------------------------------------------------------------------------------
1 | // d3.tip
2 | // Copyright (c) 2013 Justin Palmer
3 | //
4 | // Tooltips for d3.js SVG visualizations
5 |
6 | (function (root, factory) {
7 | if (typeof define === 'function' && define.amd) {
8 | // AMD. Register as an anonymous module with d3 as a dependency.
9 | define(['d3'], factory)
10 | } else if (typeof module === 'object' && module.exports) {
11 | // CommonJS
12 | module.exports = function(d3) {
13 | d3.tip = factory(d3)
14 | return d3.tip
15 | }
16 | } else {
17 | // Browser global.
18 | root.d3.tip = factory(root.d3)
19 | }
20 | }(this, function (d3) {
21 |
22 | // Public - contructs a new tooltip
23 | //
24 | // Returns a tip
25 | return function() {
26 | var direction = d3_tip_direction,
27 | offset = d3_tip_offset,
28 | html = d3_tip_html,
29 | node = initNode(),
30 | svg = null,
31 | point = null,
32 | target = null
33 |
34 | function tip(vis) {
35 | svg = getSVGNode(vis)
36 | point = svg.createSVGPoint()
37 | document.body.appendChild(node)
38 | }
39 |
40 | // Public - show the tooltip on the screen
41 | //
42 | // Returns a tip
43 | tip.show = function() {
44 | var args = Array.prototype.slice.call(arguments)
45 | if(args[args.length - 1] instanceof SVGElement) target = args.pop()
46 |
47 | var content = html.apply(this, args),
48 | poffset = offset.apply(this, args),
49 | dir = direction.apply(this, args),
50 | nodel = d3.select(node),
51 | i = directions.length,
52 | coords,
53 | scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
54 | scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
55 |
56 | nodel.html(content)
57 | .style({ opacity: 1, 'pointer-events': 'all' })
58 |
59 | while(i--) nodel.classed(directions[i], false)
60 | coords = direction_callbacks.get(dir).apply(this)
61 | nodel.classed(dir, true).style({
62 | top: (coords.top + poffset[0]) + scrollTop + 'px',
63 | left: (coords.left + poffset[1]) + scrollLeft + 'px'
64 | })
65 |
66 | return tip
67 | }
68 |
69 | // Public - hide the tooltip
70 | //
71 | // Returns a tip
72 | tip.hide = function() {
73 | var nodel = d3.select(node)
74 | nodel.style({ opacity: 0, 'pointer-events': 'none' })
75 | return tip
76 | }
77 |
78 | // Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value.
79 | //
80 | // n - name of the attribute
81 | // v - value of the attribute
82 | //
83 | // Returns tip or attribute value
84 | tip.attr = function(n, v) {
85 | if (arguments.length < 2 && typeof n === 'string') {
86 | return d3.select(node).attr(n)
87 | } else {
88 | var args = Array.prototype.slice.call(arguments)
89 | d3.selection.prototype.attr.apply(d3.select(node), args)
90 | }
91 |
92 | return tip
93 | }
94 |
95 | // Public: Proxy style calls to the d3 tip container. Sets or gets a style value.
96 | //
97 | // n - name of the property
98 | // v - value of the property
99 | //
100 | // Returns tip or style property value
101 | tip.style = function(n, v) {
102 | if (arguments.length < 2 && typeof n === 'string') {
103 | return d3.select(node).style(n)
104 | } else {
105 | var args = Array.prototype.slice.call(arguments)
106 | d3.selection.prototype.style.apply(d3.select(node), args)
107 | }
108 |
109 | return tip
110 | }
111 |
112 | // Public: Set or get the direction of the tooltip
113 | //
114 | // v - One of n(north), s(south), e(east), or w(west), nw(northwest),
115 | // sw(southwest), ne(northeast) or se(southeast)
116 | //
117 | // Returns tip or direction
118 | tip.direction = function(v) {
119 | if (!arguments.length) return direction
120 | direction = v == null ? v : d3.functor(v)
121 |
122 | return tip
123 | }
124 |
125 | // Public: Sets or gets the offset of the tip
126 | //
127 | // v - Array of [x, y] offset
128 | //
129 | // Returns offset or
130 | tip.offset = function(v) {
131 | if (!arguments.length) return offset
132 | offset = v == null ? v : d3.functor(v)
133 |
134 | return tip
135 | }
136 |
137 | // Public: sets or gets the html value of the tooltip
138 | //
139 | // v - String value of the tip
140 | //
141 | // Returns html value or tip
142 | tip.html = function(v) {
143 | if (!arguments.length) return html
144 | html = v == null ? v : d3.functor(v)
145 |
146 | return tip
147 | }
148 |
149 | function d3_tip_direction() { return 'n' }
150 | function d3_tip_offset() { return [0, 0] }
151 | function d3_tip_html() { return ' ' }
152 |
153 | var direction_callbacks = d3.map({
154 | n: direction_n,
155 | s: direction_s,
156 | e: direction_e,
157 | w: direction_w,
158 | nw: direction_nw,
159 | ne: direction_ne,
160 | sw: direction_sw,
161 | se: direction_se
162 | }),
163 |
164 | directions = direction_callbacks.keys()
165 |
166 | function direction_n() {
167 | var bbox = getScreenBBox()
168 | return {
169 | top: bbox.n.y - node.offsetHeight,
170 | left: bbox.n.x - node.offsetWidth / 2
171 | }
172 | }
173 |
174 | function direction_s() {
175 | var bbox = getScreenBBox()
176 | return {
177 | top: bbox.s.y,
178 | left: bbox.s.x - node.offsetWidth / 2
179 | }
180 | }
181 |
182 | function direction_e() {
183 | var bbox = getScreenBBox()
184 | return {
185 | top: bbox.e.y - node.offsetHeight / 2,
186 | left: bbox.e.x
187 | }
188 | }
189 |
190 | function direction_w() {
191 | var bbox = getScreenBBox()
192 | return {
193 | top: bbox.w.y - node.offsetHeight / 2,
194 | left: bbox.w.x - node.offsetWidth
195 | }
196 | }
197 |
198 | function direction_nw() {
199 | var bbox = getScreenBBox()
200 | return {
201 | top: bbox.nw.y - node.offsetHeight,
202 | left: bbox.nw.x - node.offsetWidth
203 | }
204 | }
205 |
206 | function direction_ne() {
207 | var bbox = getScreenBBox()
208 | return {
209 | top: bbox.ne.y - node.offsetHeight,
210 | left: bbox.ne.x
211 | }
212 | }
213 |
214 | function direction_sw() {
215 | var bbox = getScreenBBox()
216 | return {
217 | top: bbox.sw.y,
218 | left: bbox.sw.x - node.offsetWidth
219 | }
220 | }
221 |
222 | function direction_se() {
223 | var bbox = getScreenBBox()
224 | return {
225 | top: bbox.se.y,
226 | left: bbox.e.x
227 | }
228 | }
229 |
230 | function initNode() {
231 | var node = d3.select(document.createElement('div'))
232 | node.style({
233 | position: 'absolute',
234 | top: 0,
235 | opacity: 0,
236 | 'pointer-events': 'none',
237 | 'box-sizing': 'border-box'
238 | })
239 |
240 | return node.node()
241 | }
242 |
243 | function getSVGNode(el) {
244 | el = el.node()
245 | if(el.tagName.toLowerCase() === 'svg')
246 | return el
247 |
248 | return el.ownerSVGElement
249 | }
250 |
251 | // Private - gets the screen coordinates of a shape
252 | //
253 | // Given a shape on the screen, will return an SVGPoint for the directions
254 | // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
255 | // sw(southwest).
256 | //
257 | // +-+-+
258 | // | |
259 | // + +
260 | // | |
261 | // +-+-+
262 | //
263 | // Returns an Object {n, s, e, w, nw, sw, ne, se}
264 | function getScreenBBox() {
265 | var targetel = target || d3.event.target;
266 |
267 | while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) {
268 | targetel = targetel.parentNode;
269 | }
270 |
271 | var bbox = {},
272 | matrix = targetel.getScreenCTM(),
273 | tbbox = targetel.getBBox(),
274 | width = tbbox.width,
275 | height = tbbox.height,
276 | x = tbbox.x,
277 | y = tbbox.y
278 |
279 | point.x = x
280 | point.y = y
281 | bbox.nw = point.matrixTransform(matrix)
282 | point.x += width
283 | bbox.ne = point.matrixTransform(matrix)
284 | point.y += height
285 | bbox.se = point.matrixTransform(matrix)
286 | point.x -= width
287 | bbox.sw = point.matrixTransform(matrix)
288 | point.y -= height / 2
289 | bbox.w = point.matrixTransform(matrix)
290 | point.x += width
291 | bbox.e = point.matrixTransform(matrix)
292 | point.x -= width / 2
293 | point.y -= height / 2
294 | bbox.n = point.matrixTransform(matrix)
295 | point.y += height
296 | bbox.s = point.matrixTransform(matrix)
297 |
298 | return bbox
299 | }
300 |
301 | return tip
302 | };
303 |
304 | }));
305 |
--------------------------------------------------------------------------------
/internal/profile/proto.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // This file is a simple protocol buffer encoder and decoder.
16 | // The format is described at
17 | // https://developers.google.com/protocol-buffers/docs/encoding
18 | //
19 | // A protocol message must implement the message interface:
20 | // decoder() []decoder
21 | // encode(*buffer)
22 | //
23 | // The decode method returns a slice indexed by field number that gives the
24 | // function to decode that field.
25 | // The encode method encodes its receiver into the given buffer.
26 | //
27 | // The two methods are simple enough to be implemented by hand rather than
28 | // by using a protocol compiler.
29 | //
30 | // See profile.go for examples of messages implementing this interface.
31 | //
32 | // There is no support for groups, message sets, or "has" bits.
33 |
34 | package profile
35 |
36 | import "errors"
37 |
38 | type buffer struct {
39 | field int // field tag
40 | typ int // proto wire type code for field
41 | u64 uint64
42 | data []byte
43 | tmp [16]byte
44 | }
45 |
46 | type decoder func(*buffer, message) error
47 |
48 | type message interface {
49 | decoder() []decoder
50 | encode(*buffer)
51 | }
52 |
53 | func marshal(m message) []byte {
54 | var b buffer
55 | m.encode(&b)
56 | return b.data
57 | }
58 |
59 | func encodeVarint(b *buffer, x uint64) {
60 | for x >= 128 {
61 | b.data = append(b.data, byte(x)|0x80)
62 | x >>= 7
63 | }
64 | b.data = append(b.data, byte(x))
65 | }
66 |
67 | func encodeLength(b *buffer, tag int, len int) {
68 | encodeVarint(b, uint64(tag)<<3|2)
69 | encodeVarint(b, uint64(len))
70 | }
71 |
72 | func encodeUint64(b *buffer, tag int, x uint64) {
73 | // append varint to b.data
74 | encodeVarint(b, uint64(tag)<<3|0)
75 | encodeVarint(b, x)
76 | }
77 |
78 | func encodeUint64s(b *buffer, tag int, x []uint64) {
79 | if len(x) > 2 {
80 | // Use packed encoding
81 | n1 := len(b.data)
82 | for _, u := range x {
83 | encodeVarint(b, u)
84 | }
85 | n2 := len(b.data)
86 | encodeLength(b, tag, n2-n1)
87 | n3 := len(b.data)
88 | copy(b.tmp[:], b.data[n2:n3])
89 | copy(b.data[n1+(n3-n2):], b.data[n1:n2])
90 | copy(b.data[n1:], b.tmp[:n3-n2])
91 | return
92 | }
93 | for _, u := range x {
94 | encodeUint64(b, tag, u)
95 | }
96 | }
97 |
98 | func encodeUint64Opt(b *buffer, tag int, x uint64) {
99 | if x == 0 {
100 | return
101 | }
102 | encodeUint64(b, tag, x)
103 | }
104 |
105 | func encodeInt64(b *buffer, tag int, x int64) {
106 | u := uint64(x)
107 | encodeUint64(b, tag, u)
108 | }
109 |
110 | func encodeInt64s(b *buffer, tag int, x []int64) {
111 | if len(x) > 2 {
112 | // Use packed encoding
113 | n1 := len(b.data)
114 | for _, u := range x {
115 | encodeVarint(b, uint64(u))
116 | }
117 | n2 := len(b.data)
118 | encodeLength(b, tag, n2-n1)
119 | n3 := len(b.data)
120 | copy(b.tmp[:], b.data[n2:n3])
121 | copy(b.data[n1+(n3-n2):], b.data[n1:n2])
122 | copy(b.data[n1:], b.tmp[:n3-n2])
123 | return
124 | }
125 | for _, u := range x {
126 | encodeInt64(b, tag, u)
127 | }
128 | }
129 |
130 | func encodeInt64Opt(b *buffer, tag int, x int64) {
131 | if x == 0 {
132 | return
133 | }
134 | encodeInt64(b, tag, x)
135 | }
136 |
137 | func encodeString(b *buffer, tag int, x string) {
138 | encodeLength(b, tag, len(x))
139 | b.data = append(b.data, x...)
140 | }
141 |
142 | func encodeStrings(b *buffer, tag int, x []string) {
143 | for _, s := range x {
144 | encodeString(b, tag, s)
145 | }
146 | }
147 |
148 | func encodeStringOpt(b *buffer, tag int, x string) {
149 | if x == "" {
150 | return
151 | }
152 | encodeString(b, tag, x)
153 | }
154 |
155 | func encodeBool(b *buffer, tag int, x bool) {
156 | if x {
157 | encodeUint64(b, tag, 1)
158 | } else {
159 | encodeUint64(b, tag, 0)
160 | }
161 | }
162 |
163 | func encodeBoolOpt(b *buffer, tag int, x bool) {
164 | if x == false {
165 | return
166 | }
167 | encodeBool(b, tag, x)
168 | }
169 |
170 | func encodeMessage(b *buffer, tag int, m message) {
171 | n1 := len(b.data)
172 | m.encode(b)
173 | n2 := len(b.data)
174 | encodeLength(b, tag, n2-n1)
175 | n3 := len(b.data)
176 | copy(b.tmp[:], b.data[n2:n3])
177 | copy(b.data[n1+(n3-n2):], b.data[n1:n2])
178 | copy(b.data[n1:], b.tmp[:n3-n2])
179 | }
180 |
181 | func unmarshal(data []byte, m message) (err error) {
182 | b := buffer{data: data, typ: 2}
183 | return decodeMessage(&b, m)
184 | }
185 |
186 | func le64(p []byte) uint64 {
187 | return uint64(p[0]) | uint64(p[1])<<8 | uint64(p[2])<<16 | uint64(p[3])<<24 | uint64(p[4])<<32 | uint64(p[5])<<40 | uint64(p[6])<<48 | uint64(p[7])<<56
188 | }
189 |
190 | func le32(p []byte) uint32 {
191 | return uint32(p[0]) | uint32(p[1])<<8 | uint32(p[2])<<16 | uint32(p[3])<<24
192 | }
193 |
194 | func decodeVarint(data []byte) (uint64, []byte, error) {
195 | var u uint64
196 | for i := 0; ; i++ {
197 | if i >= 10 || i >= len(data) {
198 | return 0, nil, errors.New("bad varint")
199 | }
200 | u |= uint64(data[i]&0x7F) << uint(7*i)
201 | if data[i]&0x80 == 0 {
202 | return u, data[i+1:], nil
203 | }
204 | }
205 | }
206 |
207 | func decodeField(b *buffer, data []byte) ([]byte, error) {
208 | x, data, err := decodeVarint(data)
209 | if err != nil {
210 | return nil, err
211 | }
212 | b.field = int(x >> 3)
213 | b.typ = int(x & 7)
214 | b.data = nil
215 | b.u64 = 0
216 | switch b.typ {
217 | case 0:
218 | b.u64, data, err = decodeVarint(data)
219 | if err != nil {
220 | return nil, err
221 | }
222 | case 1:
223 | if len(data) < 8 {
224 | return nil, errors.New("not enough data")
225 | }
226 | b.u64 = le64(data[:8])
227 | data = data[8:]
228 | case 2:
229 | var n uint64
230 | n, data, err = decodeVarint(data)
231 | if err != nil {
232 | return nil, err
233 | }
234 | if n > uint64(len(data)) {
235 | return nil, errors.New("too much data")
236 | }
237 | b.data = data[:n]
238 | data = data[n:]
239 | case 5:
240 | if len(data) < 4 {
241 | return nil, errors.New("not enough data")
242 | }
243 | b.u64 = uint64(le32(data[:4]))
244 | data = data[4:]
245 | default:
246 | return nil, errors.New("unknown wire type: " + string(b.typ))
247 | }
248 |
249 | return data, nil
250 | }
251 |
252 | func checkType(b *buffer, typ int) error {
253 | if b.typ != typ {
254 | return errors.New("type mismatch")
255 | }
256 | return nil
257 | }
258 |
259 | func decodeMessage(b *buffer, m message) error {
260 | if err := checkType(b, 2); err != nil {
261 | return err
262 | }
263 | dec := m.decoder()
264 | data := b.data
265 | for len(data) > 0 {
266 | // pull varint field# + type
267 | var err error
268 | data, err = decodeField(b, data)
269 | if err != nil {
270 | return err
271 | }
272 | if b.field >= len(dec) || dec[b.field] == nil {
273 | continue
274 | }
275 | if err := dec[b.field](b, m); err != nil {
276 | return err
277 | }
278 | }
279 | return nil
280 | }
281 |
282 | func decodeInt64(b *buffer, x *int64) error {
283 | if err := checkType(b, 0); err != nil {
284 | return err
285 | }
286 | *x = int64(b.u64)
287 | return nil
288 | }
289 |
290 | func decodeInt64s(b *buffer, x *[]int64) error {
291 | if b.typ == 2 {
292 | // Packed encoding
293 | data := b.data
294 | tmp := make([]int64, 0, len(data)) // Maximally sized
295 | for len(data) > 0 {
296 | var u uint64
297 | var err error
298 |
299 | if u, data, err = decodeVarint(data); err != nil {
300 | return err
301 | }
302 | tmp = append(tmp, int64(u))
303 | }
304 | *x = append(*x, tmp...)
305 | return nil
306 | }
307 | var i int64
308 | if err := decodeInt64(b, &i); err != nil {
309 | return err
310 | }
311 | *x = append(*x, i)
312 | return nil
313 | }
314 |
315 | func decodeUint64(b *buffer, x *uint64) error {
316 | if err := checkType(b, 0); err != nil {
317 | return err
318 | }
319 | *x = b.u64
320 | return nil
321 | }
322 |
323 | func decodeUint64s(b *buffer, x *[]uint64) error {
324 | if b.typ == 2 {
325 | data := b.data
326 | // Packed encoding
327 | tmp := make([]uint64, 0, len(data)) // Maximally sized
328 | for len(data) > 0 {
329 | var u uint64
330 | var err error
331 |
332 | if u, data, err = decodeVarint(data); err != nil {
333 | return err
334 | }
335 | tmp = append(tmp, u)
336 | }
337 | *x = append(*x, tmp...)
338 | return nil
339 | }
340 | var u uint64
341 | if err := decodeUint64(b, &u); err != nil {
342 | return err
343 | }
344 | *x = append(*x, u)
345 | return nil
346 | }
347 |
348 | func decodeString(b *buffer, x *string) error {
349 | if err := checkType(b, 2); err != nil {
350 | return err
351 | }
352 | *x = string(b.data)
353 | return nil
354 | }
355 |
356 | func decodeStrings(b *buffer, x *[]string) error {
357 | var s string
358 | if err := decodeString(b, &s); err != nil {
359 | return err
360 | }
361 | *x = append(*x, s)
362 | return nil
363 | }
364 |
365 | func decodeBool(b *buffer, x *bool) error {
366 | if err := checkType(b, 0); err != nil {
367 | return err
368 | }
369 | if int64(b.u64) == 0 {
370 | *x = false
371 | } else {
372 | *x = true
373 | }
374 | return nil
375 | }
376 |
--------------------------------------------------------------------------------
/html/bindings/flame/js/d3.flameGraph.js:
--------------------------------------------------------------------------------
1 |
2 | function flameGraph() {
3 |
4 | var w = 960, // graph width
5 | h = 540, // graph height
6 | c = 18, // cell height
7 | selection = null, // selection
8 | tooltip = true, // enable tooltip
9 | title = "", // graph title
10 | transitionDuration = 750,
11 | transitionEase = "cubic-in-out", // tooltip offset
12 | sort = true;
13 |
14 | var tip = d3.tip()
15 | .direction("s")
16 | .offset([8, 0])
17 | .attr('class', 'd3-flame-graph-tip')
18 | .html(function(d) { return label(d); });
19 |
20 | var labelFormat = function(d) {
21 | return d.name + " (" + d3.round(100 * d.dx, 3) + "%, " + d.value + " samples)";
22 | }
23 |
24 | function setDetails(t) {
25 | var details = document.getElementById("details");
26 | if (details)
27 | details.innerHTML = t;
28 | }
29 |
30 | function label(d) {
31 | if (!d.dummy) {
32 | return labelFormat(d);
33 | } else {
34 | return "";
35 | }
36 | }
37 |
38 | function name(d) {
39 | return d.name;
40 | }
41 |
42 | function generateHash(name) {
43 | // Return a vector (0.0->1.0) that is a hash of the input string.
44 | // The hash is computed to favor early characters over later ones, so
45 | // that strings with similar starts have similar vectors. Only the first
46 | // 6 characters are considered.
47 | var hash = 0, weight = 1, max_hash = 0, mod = 10, max_char = 6;
48 | if (name) {
49 | for (var i = 0; i < name.length; i++) {
50 | if (i > max_char) { break; }
51 | hash += weight * (name.charCodeAt(i) % mod);
52 | max_hash += weight * (mod - 1);
53 | weight *= 0.70;
54 | }
55 | if (max_hash > 0) { hash = hash / max_hash; }
56 | }
57 | return hash;
58 | }
59 |
60 | function colorHash(name) {
61 | // Return an rgb() color string that is a hash of the provided name,
62 | // and with a warm palette.
63 | var vector = 0;
64 | if (name) {
65 | name = name.replace(/.*`/, ""); // drop module name if present
66 | name = name.replace(/\(.*/, ""); // drop extra info
67 | vector = generateHash(name);
68 | }
69 | var r = 200 + Math.round(55 * vector);
70 | var g = 0 + Math.round(230 * (1 - vector));
71 | var b = 0 + Math.round(55 * (1 - vector));
72 | return "rgb(" + r + "," + g + "," + b + ")";
73 | }
74 |
75 | function augment(data) {
76 | // Augment partitioning layout with "dummy" nodes so that internal nodes'
77 | // values dictate their width. Annoying, but seems to be least painful
78 | // option. https://github.com/mbostock/d3/pull/574
79 | if (data.children && (data.children.length > 0)) {
80 | data.children.forEach(augment);
81 | var childValues = 0;
82 | data.children.forEach(function(child) {
83 | childValues += child.value;
84 | });
85 | if (childValues < data.value) {
86 | data.children.push(
87 | {
88 | "name": "",
89 | "value": data.value - childValues,
90 | "dummy": true
91 | }
92 | );
93 | }
94 | }
95 | }
96 |
97 | function hide(d) {
98 | if(!d.original) {
99 | d.original = d.value;
100 | }
101 | d.value = 0;
102 | if(d.children) {
103 | d.children.forEach(hide);
104 | }
105 | }
106 |
107 | function show(d) {
108 | d.fade = false;
109 | if(d.original) {
110 | d.value = d.original;
111 | }
112 | if(d.children) {
113 | d.children.forEach(show);
114 | }
115 | }
116 |
117 | function getSiblings(d) {
118 | var siblings = [];
119 | if (d.parent) {
120 | var me = d.parent.children.indexOf(d);
121 | siblings = d.parent.children.slice(0);
122 | siblings.splice(me, 1);
123 | }
124 | return siblings;
125 | }
126 |
127 | function hideSiblings(d) {
128 | var siblings = getSiblings(d);
129 | siblings.forEach(function(s) {
130 | hide(s);
131 | });
132 | if(d.parent) {
133 | hideSiblings(d.parent);
134 | }
135 | }
136 |
137 | function fadeAncestors(d) {
138 | if(d.parent) {
139 | d.parent.fade = true;
140 | fadeAncestors(d.parent);
141 | }
142 | }
143 |
144 | function getRoot(d) {
145 | if(d.parent) {
146 | return getRoot(d.parent);
147 | }
148 | return d;
149 | }
150 |
151 | function zoom(d) {
152 | tip.hide(d);
153 | hideSiblings(d);
154 | show(d);
155 | fadeAncestors(d);
156 | update();
157 | }
158 |
159 | function searchTree(d, term) {
160 | var re = new RegExp(term),
161 | label = d.name;
162 |
163 | if(d.children) {
164 | d.children.forEach(function(child) {
165 | searchTree(child, term);
166 | });
167 | }
168 |
169 | if (label.match(re)) {
170 | d.highlight = true;
171 | } else {
172 | d.highlight = false;
173 | }
174 | }
175 |
176 | function clear(d) {
177 | d.highlight = false;
178 | if(d.children) {
179 | d.children.forEach(function(child) {
180 | clear(child, term);
181 | });
182 | }
183 | }
184 |
185 | function doSort(a, b) {
186 | if (typeof sort === 'function') {
187 | return sort(a, b);
188 | } else if (sort) {
189 | return d3.ascending(a.name, b.name);
190 | } else {
191 | return 0;
192 | }
193 | }
194 |
195 | var partition = d3.layout.partition()
196 | .sort(doSort)
197 | .value(function(d) {return d.v || d.value;})
198 | .children(function(d) {return d.c || d.children;});
199 |
200 | function update() {
201 |
202 | selection.each(function(data) {
203 |
204 | var x = d3.scale.linear().range([0, w]),
205 | y = d3.scale.linear().range([0, c]);
206 |
207 | var nodes = partition(data);
208 |
209 | var kx = w / data.dx;
210 |
211 | var g = d3.select(this).select("svg").selectAll("g").data(nodes);
212 |
213 | g.transition()
214 | .duration(transitionDuration)
215 | .ease(transitionEase)
216 | .attr("transform", function(d) { return "translate(" + x(d.x) + "," + (h - y(d.depth) - c) + ")"; });
217 |
218 | g.select("rect").transition()
219 | .duration(transitionDuration)
220 | .ease(transitionEase)
221 | .attr("width", function(d) { return d.dx * kx; });
222 |
223 | var node = g.enter()
224 | .append("svg:g")
225 | .attr("transform", function(d) { return "translate(" + x(d.x) + "," + (h - y(d.depth) - c) + ")"; });
226 |
227 | node.append("svg:rect")
228 | .attr("width", function(d) { return d.dx * kx; });
229 |
230 | if (!tooltip)
231 | node.append("svg:title");
232 |
233 | node.append("foreignObject")
234 | .append("xhtml:div");
235 |
236 | g.attr("width", function(d) { return d.dx * kx; })
237 | .attr("height", function(d) { return c; })
238 | .attr("name", function(d) { return d.name; })
239 | .attr("class", function(d) { return d.fade ? "frame fade" : "frame"; });
240 |
241 | g.select("rect")
242 | .attr("height", function(d) { return c; })
243 | .attr("fill", function(d) {return d.highlight ? "#E600E6" : colorHash(d.name); })
244 | .style("visibility", function(d) {return d.dummy ? "hidden" : "visible";});
245 |
246 | if (!tooltip)
247 | g.select("title")
248 | .text(label);
249 |
250 | g.select("foreignObject")
251 | .attr("width", function(d) { return d.dx * kx; })
252 | .attr("height", function(d) { return c; })
253 | .select("div")
254 | .attr("class", "label")
255 | .style("display", function(d) { return (d.dx * kx < 35) || d.dummy ? "none" : "block";})
256 | .text(name);
257 |
258 | g.on('click', zoom);
259 |
260 |
261 |
262 | g.exit().remove();
263 |
264 | g.on('mouseover', function(d) {
265 | if(!d.dummy) {
266 | if (tooltip) tip.show(d);
267 | setDetails(label(d));
268 | }
269 | }).on('mouseout', function(d) {
270 | if(!d.dummy) {
271 | if (tooltip) tip.hide(d);
272 | setDetails("");
273 | }
274 | });
275 | });
276 | }
277 |
278 | function chart(s) {
279 |
280 | selection = s;
281 |
282 | if (!arguments.length) return chart;
283 |
284 | selection.each(function(data) {
285 |
286 | var svg = d3.select(this)
287 | .append("svg:svg")
288 | .attr("width", w)
289 | .attr("height", h)
290 | .attr("class", "partition d3-flame-graph")
291 | .call(tip);
292 |
293 | svg.append("svg:text")
294 | .attr("class", "title")
295 | .attr("text-anchor", "middle")
296 | .attr("y", "25")
297 | .attr("x", w/2)
298 | .attr("fill", "#808080")
299 | .text(title);
300 |
301 | augment(data);
302 |
303 | // "creative" fix for node ordering when partition is called for the first time
304 | partition(data);
305 |
306 | // first draw
307 | update();
308 |
309 | });
310 | }
311 |
312 | chart.height = function (_) {
313 | if (!arguments.length) { return h; }
314 | h = _;
315 | return chart;
316 | };
317 |
318 | chart.width = function (_) {
319 | if (!arguments.length) { return w; }
320 | w = _;
321 | return chart;
322 | };
323 |
324 | chart.cellHeight = function (_) {
325 | if (!arguments.length) { return c; }
326 | c = _;
327 | return chart;
328 | };
329 |
330 | chart.tooltip = function (_) {
331 | if (!arguments.length) { return tooltip; }
332 | if (typeof _ === "function") {
333 | tip = _;
334 | }
335 | tooltip = true;
336 | return chart;
337 | };
338 |
339 | chart.title = function (_) {
340 | if (!arguments.length) { return title; }
341 | title = _;
342 | return chart;
343 | };
344 |
345 | chart.transitionDuration = function (_) {
346 | if (!arguments.length) { return transitionDuration; }
347 | transitionDuration = _;
348 | return chart;
349 | };
350 |
351 | chart.transitionEase = function (_) {
352 | if (!arguments.length) { return transitionEase; }
353 | transitionEase = _;
354 | return chart;
355 | };
356 |
357 | chart.sort = function (_) {
358 | if (!arguments.length) { return sort; }
359 | sort = _;
360 | return chart;
361 | };
362 |
363 | chart.label = function(_) {
364 | if (!arguments.length) { return labelFormat; }
365 | labelFormat = _;
366 | return chart;
367 | }
368 |
369 | chart.search = function(term) {
370 | selection.each(function(data) {
371 | searchTree(data, term);
372 | update();
373 | });
374 | };
375 |
376 | chart.clear = function() {
377 | selection.each(function(data) {
378 | clear(data);
379 | update();
380 | });
381 | };
382 |
383 |
384 |
385 | return chart;
386 | }
387 |
388 |
389 | export default flameGraph;
390 |
391 |
--------------------------------------------------------------------------------
/examples/web/d3.flameGraph.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | function flameGraph() {
5 |
6 | var w = 960, // graph width
7 | h = 540, // graph height
8 | c = 18, // cell height
9 | selection = null, // selection
10 | tooltip = true, // enable tooltip
11 | title = "", // graph title
12 | transitionDuration = 750,
13 | transitionEase = "cubic-in-out", // tooltip offset
14 | sort = true,
15 | reversed = false, // reverse the graph direction
16 | clickHandler = null;
17 |
18 | var tip = d3.tip()
19 | .direction("s")
20 | .offset([8, 0])
21 | .attr('class', 'd3-flame-graph-tip')
22 | .html(function(d) { return label(d); });
23 |
24 | var labelFormat = function(d) {
25 | return d.name + " (" + d3.round(100 * d.dx, 3) + "%, " + d.value + " samples)";
26 | };
27 |
28 | function setDetails(t) {
29 | var details = document.getElementById("details");
30 | if (details)
31 | details.innerHTML = t;
32 | }
33 |
34 | function label(d) {
35 | if (!d.dummy) {
36 | return labelFormat(d);
37 | } else {
38 | return "";
39 | }
40 | }
41 |
42 | function name(d) {
43 | return d.name;
44 | }
45 |
46 | var colorMapper = function(d) {
47 | return d.highlight ? "#E600E6" : colorHash(d.name);
48 | };
49 |
50 | function generateHash(name) {
51 | // Return a vector (0.0->1.0) that is a hash of the input string.
52 | // The hash is computed to favor early characters over later ones, so
53 | // that strings with similar starts have similar vectors. Only the first
54 | // 6 characters are considered.
55 | var hash = 0, weight = 1, max_hash = 0, mod = 10, max_char = 6;
56 | if (name) {
57 | for (var i = 0; i < name.length; i++) {
58 | if (i > max_char) { break; }
59 | hash += weight * (name.charCodeAt(i) % mod);
60 | max_hash += weight * (mod - 1);
61 | weight *= 0.70;
62 | }
63 | if (max_hash > 0) { hash = hash / max_hash; }
64 | }
65 | return hash;
66 | }
67 |
68 | function colorHash(name) {
69 | // Return an rgb() color string that is a hash of the provided name,
70 | // and with a warm palette.
71 | var vector = 0;
72 | if (name) {
73 | name = name.replace(/.*`/, ""); // drop module name if present
74 | name = name.replace(/\(.*/, ""); // drop extra info
75 | vector = generateHash(name);
76 | }
77 | var r = 200 + Math.round(55 * vector);
78 | var g = 0 + Math.round(230 * (1 - vector));
79 | var b = 0 + Math.round(55 * (1 - vector));
80 | return "rgb(" + r + "," + g + "," + b + ")";
81 | }
82 |
83 | function augment(data) {
84 | // Augment partitioning layout with "dummy" nodes so that internal nodes'
85 | // values dictate their width. Annoying, but seems to be least painful
86 | // option. https://github.com/mbostock/d3/pull/574
87 | if (data.children && (data.children.length > 0)) {
88 | data.children.forEach(augment);
89 | var childValues = 0;
90 | data.children.forEach(function(child) {
91 | childValues += child.value;
92 | });
93 | if (childValues < data.value) {
94 | data.children.push(
95 | {
96 | "name": "",
97 | "value": data.value - childValues,
98 | "dummy": true
99 | }
100 | );
101 | }
102 | }
103 | }
104 |
105 | function hide(d) {
106 | if(!d.original) {
107 | d.original = d.value;
108 | }
109 | d.value = 0;
110 | if(d.children) {
111 | d.children.forEach(hide);
112 | }
113 | }
114 |
115 | function show(d) {
116 | d.fade = false;
117 | if(d.original) {
118 | d.value = d.original;
119 | }
120 | if(d.children) {
121 | d.children.forEach(show);
122 | }
123 | }
124 |
125 | function getSiblings(d) {
126 | var siblings = [];
127 | if (d.parent) {
128 | var me = d.parent.children.indexOf(d);
129 | siblings = d.parent.children.slice(0);
130 | siblings.splice(me, 1);
131 | }
132 | return siblings;
133 | }
134 |
135 | function hideSiblings(d) {
136 | var siblings = getSiblings(d);
137 | siblings.forEach(function(s) {
138 | hide(s);
139 | });
140 | if(d.parent) {
141 | hideSiblings(d.parent);
142 | }
143 | }
144 |
145 | function fadeAncestors(d) {
146 | if(d.parent) {
147 | d.parent.fade = true;
148 | fadeAncestors(d.parent);
149 | }
150 | }
151 |
152 | function getRoot(d) {
153 | if(d.parent) {
154 | return getRoot(d.parent);
155 | }
156 | return d;
157 | }
158 |
159 | function zoom(d) {
160 | tip.hide(d);
161 | hideSiblings(d);
162 | show(d);
163 | fadeAncestors(d);
164 | update();
165 | if (typeof clickHandler === 'function') {
166 | clickHandler(d);
167 | }
168 | }
169 |
170 | function searchTree(d, term) {
171 | var re = new RegExp(term),
172 | searchResults = [];
173 |
174 | function searchInner(d) {
175 | var label = d.name;
176 |
177 | if (d.children) {
178 | d.children.forEach(function (child) {
179 | searchInner(child);
180 | });
181 | }
182 |
183 | if (label.match(re)) {
184 | d.highlight = true;
185 | searchResults.push(d);
186 | } else {
187 | d.highlight = false;
188 | }
189 | }
190 |
191 | searchInner(d);
192 | return searchResults;
193 | }
194 |
195 | function clear(d) {
196 | d.highlight = false;
197 | if(d.children) {
198 | d.children.forEach(function(child) {
199 | clear(child, term);
200 | });
201 | }
202 | }
203 |
204 | function doSort(a, b) {
205 | if (typeof sort === 'function') {
206 | return sort(a, b);
207 | } else if (sort) {
208 | return d3.ascending(a.name, b.name);
209 | } else {
210 | return 0;
211 | }
212 | }
213 |
214 | var partition = d3.layout.partition()
215 | .sort(doSort)
216 | .value(function(d) {return d.v || d.value;})
217 | .children(function(d) {return d.c || d.children;});
218 |
219 | function update() {
220 |
221 | selection.each(function(data) {
222 |
223 | var x = d3.scale.linear().range([0, w]),
224 | y = d3.scale.linear().range([0, c]);
225 |
226 | var nodes = partition(data);
227 |
228 | var kx = w / data.dx;
229 |
230 | var g = d3.select(this).select("svg").selectAll("g").data(nodes);
231 |
232 | g.transition()
233 | .duration(transitionDuration)
234 | .ease(transitionEase)
235 | .attr("transform", function(d) { return "translate(" + x(d.x) + ","
236 | + (reversed ? y(d.depth) : (h - y(d.depth) - c)) + ")"; });
237 |
238 | g.select("rect").transition()
239 | .duration(transitionDuration)
240 | .ease(transitionEase)
241 | .attr("width", function(d) { return d.dx * kx; });
242 |
243 | var node = g.enter()
244 | .append("svg:g")
245 | .attr("transform", function(d) { return "translate(" + x(d.x) + ","
246 | + (reversed ? y(d.depth) : (h - y(d.depth) - c)) + ")"; });
247 |
248 | node.append("svg:rect")
249 | .attr("width", function(d) { return d.dx * kx; });
250 |
251 | if (!tooltip)
252 | node.append("svg:title");
253 |
254 | node.append("foreignObject")
255 | .append("xhtml:div");
256 |
257 | g.attr("width", function(d) { return d.dx * kx; })
258 | .attr("height", function(d) { return c; })
259 | .attr("name", function(d) { return d.name; })
260 | .attr("class", function(d) { return d.fade ? "frame fade" : "frame"; });
261 |
262 | g.select("rect")
263 | .attr("height", function(d) { return c; })
264 | .attr("fill", function(d) { return colorMapper(d); })
265 | .style("visibility", function(d) {return d.dummy ? "hidden" : "visible";});
266 |
267 | if (!tooltip)
268 | g.select("title")
269 | .text(label);
270 |
271 | g.select("foreignObject")
272 | .attr("width", function(d) { return d.dx * kx; })
273 | .attr("height", function(d) { return c; })
274 | .select("div")
275 | .attr("class", "label")
276 | .style("display", function(d) { return (d.dx * kx < 35) || d.dummy ? "none" : "block";})
277 | .text(name);
278 |
279 | g.on('click', zoom);
280 |
281 | g.exit().remove();
282 |
283 | g.on('mouseover', function(d) {
284 | if(!d.dummy) {
285 | if (tooltip) tip.show(d);
286 | setDetails(label(d));
287 | }
288 | }).on('mouseout', function(d) {
289 | if(!d.dummy) {
290 | if (tooltip) tip.hide(d);
291 | setDetails("");
292 | }
293 | });
294 | });
295 | }
296 |
297 | function merge(data, samples) {
298 | samples.forEach(function (sample) {
299 | var node = _.find(data, function (element) {
300 | return element.name === sample.name;
301 | });
302 |
303 | if (node) {
304 | node.value += sample.value;
305 | if (sample.children) {
306 | if (!node.children) {
307 | node.children = [];
308 | }
309 | merge(node.children, sample.children)
310 | }
311 | } else {
312 | data.push(sample);
313 | }
314 | });
315 | }
316 |
317 | function chart(s) {
318 |
319 | selection = s;
320 |
321 | if (!arguments.length) return chart;
322 |
323 | selection.each(function(data) {
324 |
325 | var svg = d3.select(this)
326 | .append("svg:svg")
327 | .attr("width", w)
328 | .attr("height", h)
329 | .attr("class", "partition d3-flame-graph")
330 | .call(tip);
331 |
332 | svg.append("svg:text")
333 | .attr("class", "title")
334 | .attr("text-anchor", "middle")
335 | .attr("y", "25")
336 | .attr("x", w/2)
337 | .attr("fill", "#808080")
338 | .text(title);
339 |
340 | augment(data);
341 |
342 | // "creative" fix for node ordering when partition is called for the first time
343 | partition(data);
344 |
345 | });
346 |
347 | // first draw
348 | update();
349 | }
350 |
351 | chart.height = function (_) {
352 | if (!arguments.length) { return h; }
353 | h = _;
354 | return chart;
355 | };
356 |
357 | chart.width = function (_) {
358 | if (!arguments.length) { return w; }
359 | w = _;
360 | return chart;
361 | };
362 |
363 | chart.cellHeight = function (_) {
364 | if (!arguments.length) { return c; }
365 | c = _;
366 | return chart;
367 | };
368 |
369 | chart.tooltip = function (_) {
370 | if (!arguments.length) { return tooltip; }
371 | if (typeof _ === "function") {
372 | tip = _;
373 | }
374 | tooltip = true;
375 | return chart;
376 | };
377 |
378 | chart.title = function (_) {
379 | if (!arguments.length) { return title; }
380 | title = _;
381 | return chart;
382 | };
383 |
384 | chart.transitionDuration = function (_) {
385 | if (!arguments.length) { return transitionDuration; }
386 | transitionDuration = _;
387 | return chart;
388 | };
389 |
390 | chart.transitionEase = function (_) {
391 | if (!arguments.length) { return transitionEase; }
392 | transitionEase = _;
393 | return chart;
394 | };
395 |
396 | chart.sort = function (_) {
397 | if (!arguments.length) { return sort; }
398 | sort = _;
399 | return chart;
400 | };
401 |
402 | chart.reversed = function (_) {
403 | if (!arguments.length) { return reversed; }
404 | reversed = _;
405 | return chart;
406 | };
407 |
408 | chart.label = function(_) {
409 | if (!arguments.length) { return labelFormat; }
410 | labelFormat = _;
411 | return chart;
412 | };
413 |
414 | chart.search = function(term) {
415 | var searchResults = [];
416 | selection.each(function(data) {
417 | searchResults = searchTree(data, term);
418 | update();
419 | });
420 | return searchResults;
421 | };
422 |
423 | chart.clear = function() {
424 | selection.each(function(data) {
425 | clear(data);
426 | update();
427 | });
428 | };
429 |
430 | chart.zoomTo = function(d) {
431 | zoom(d);
432 | };
433 |
434 | chart.resetZoom = function() {
435 | selection.each(function (data) {
436 | zoom(data); // zoom to root
437 | });
438 | };
439 |
440 | chart.onClick = function(_) {
441 | if (!arguments.length) {
442 | return clickHandler;
443 | }
444 | clickHandler = _;
445 | return chart;
446 | };
447 |
448 | chart.merge = function(samples) {
449 | selection.each(function (data) {
450 | merge([data], [samples]);
451 | augment(data);
452 | });
453 | update();
454 | }
455 |
456 | chart.color = function(_) {
457 | if (!arguments.length) { return colorMapper; }
458 | colorMapper = _;
459 | return chart;
460 | };
461 |
462 | return chart;
463 | }
464 |
465 | if (typeof module !== 'undefined' && module.exports){
466 | module.exports = flameGraph;
467 | }
468 | else {
469 | d3.flameGraph = flameGraph;
470 | }
471 | })();
472 |
--------------------------------------------------------------------------------
/internal/profile/encode.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package profile
16 |
17 | import (
18 | "errors"
19 | "sort"
20 | )
21 |
22 | func (p *Profile) decoder() []decoder {
23 | return profileDecoder
24 | }
25 |
26 | // preEncode populates the unexported fields to be used by encode
27 | // (with suffix X) from the corresponding exported fields. The
28 | // exported fields are cleared up to facilitate testing.
29 | func (p *Profile) preEncode() {
30 | strings := make(map[string]int)
31 | addString(strings, "")
32 |
33 | for _, st := range p.SampleType {
34 | st.typeX = addString(strings, st.Type)
35 | st.unitX = addString(strings, st.Unit)
36 | }
37 |
38 | for _, s := range p.Sample {
39 | s.labelX = nil
40 | var keys []string
41 | for k := range s.Label {
42 | keys = append(keys, k)
43 | }
44 | sort.Strings(keys)
45 | for _, k := range keys {
46 | vs := s.Label[k]
47 | for _, v := range vs {
48 | s.labelX = append(s.labelX,
49 | label{
50 | keyX: addString(strings, k),
51 | strX: addString(strings, v),
52 | },
53 | )
54 | }
55 | }
56 | var numKeys []string
57 | for k := range s.NumLabel {
58 | numKeys = append(numKeys, k)
59 | }
60 | sort.Strings(numKeys)
61 | for _, k := range numKeys {
62 | vs := s.NumLabel[k]
63 | for _, v := range vs {
64 | s.labelX = append(s.labelX,
65 | label{
66 | keyX: addString(strings, k),
67 | numX: v,
68 | },
69 | )
70 | }
71 | }
72 | s.locationIDX = make([]uint64, len(s.Location))
73 | for i, loc := range s.Location {
74 | s.locationIDX[i] = loc.ID
75 | }
76 | }
77 |
78 | for _, m := range p.Mapping {
79 | m.fileX = addString(strings, m.File)
80 | m.buildIDX = addString(strings, m.BuildID)
81 | }
82 |
83 | for _, l := range p.Location {
84 | for i, ln := range l.Line {
85 | if ln.Function != nil {
86 | l.Line[i].functionIDX = ln.Function.ID
87 | } else {
88 | l.Line[i].functionIDX = 0
89 | }
90 | }
91 | if l.Mapping != nil {
92 | l.mappingIDX = l.Mapping.ID
93 | } else {
94 | l.mappingIDX = 0
95 | }
96 | }
97 | for _, f := range p.Function {
98 | f.nameX = addString(strings, f.Name)
99 | f.systemNameX = addString(strings, f.SystemName)
100 | f.filenameX = addString(strings, f.Filename)
101 | }
102 |
103 | p.dropFramesX = addString(strings, p.DropFrames)
104 | p.keepFramesX = addString(strings, p.KeepFrames)
105 |
106 | if pt := p.PeriodType; pt != nil {
107 | pt.typeX = addString(strings, pt.Type)
108 | pt.unitX = addString(strings, pt.Unit)
109 | }
110 |
111 | p.commentX = nil
112 | for _, c := range p.Comments {
113 | p.commentX = append(p.commentX, addString(strings, c))
114 | }
115 |
116 | p.defaultSampleTypeX = addString(strings, p.DefaultSampleType)
117 |
118 | p.stringTable = make([]string, len(strings))
119 | for s, i := range strings {
120 | p.stringTable[i] = s
121 | }
122 | }
123 |
124 | func (p *Profile) encode(b *buffer) {
125 | for _, x := range p.SampleType {
126 | encodeMessage(b, 1, x)
127 | }
128 | for _, x := range p.Sample {
129 | encodeMessage(b, 2, x)
130 | }
131 | for _, x := range p.Mapping {
132 | encodeMessage(b, 3, x)
133 | }
134 | for _, x := range p.Location {
135 | encodeMessage(b, 4, x)
136 | }
137 | for _, x := range p.Function {
138 | encodeMessage(b, 5, x)
139 | }
140 | encodeStrings(b, 6, p.stringTable)
141 | encodeInt64Opt(b, 7, p.dropFramesX)
142 | encodeInt64Opt(b, 8, p.keepFramesX)
143 | encodeInt64Opt(b, 9, p.TimeNanos)
144 | encodeInt64Opt(b, 10, p.DurationNanos)
145 | if pt := p.PeriodType; pt != nil && (pt.typeX != 0 || pt.unitX != 0) {
146 | encodeMessage(b, 11, p.PeriodType)
147 | }
148 | encodeInt64Opt(b, 12, p.Period)
149 | encodeInt64s(b, 13, p.commentX)
150 | encodeInt64(b, 14, p.defaultSampleTypeX)
151 | }
152 |
153 | var profileDecoder = []decoder{
154 | nil, // 0
155 | // repeated ValueType sample_type = 1
156 | func(b *buffer, m message) error {
157 | x := new(ValueType)
158 | pp := m.(*Profile)
159 | pp.SampleType = append(pp.SampleType, x)
160 | return decodeMessage(b, x)
161 | },
162 | // repeated Sample sample = 2
163 | func(b *buffer, m message) error {
164 | x := new(Sample)
165 | pp := m.(*Profile)
166 | pp.Sample = append(pp.Sample, x)
167 | return decodeMessage(b, x)
168 | },
169 | // repeated Mapping mapping = 3
170 | func(b *buffer, m message) error {
171 | x := new(Mapping)
172 | pp := m.(*Profile)
173 | pp.Mapping = append(pp.Mapping, x)
174 | return decodeMessage(b, x)
175 | },
176 | // repeated Location location = 4
177 | func(b *buffer, m message) error {
178 | x := new(Location)
179 | x.Line = make([]Line, 0, 8) // Pre-allocate Line buffer
180 | pp := m.(*Profile)
181 | pp.Location = append(pp.Location, x)
182 | err := decodeMessage(b, x)
183 | var tmp []Line
184 | x.Line = append(tmp, x.Line...) // Shrink to allocated size
185 | return err
186 | },
187 | // repeated Function function = 5
188 | func(b *buffer, m message) error {
189 | x := new(Function)
190 | pp := m.(*Profile)
191 | pp.Function = append(pp.Function, x)
192 | return decodeMessage(b, x)
193 | },
194 | // repeated string string_table = 6
195 | func(b *buffer, m message) error {
196 | err := decodeStrings(b, &m.(*Profile).stringTable)
197 | if err != nil {
198 | return err
199 | }
200 | if m.(*Profile).stringTable[0] != "" {
201 | return errors.New("string_table[0] must be ''")
202 | }
203 | return nil
204 | },
205 | // int64 drop_frames = 7
206 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).dropFramesX) },
207 | // int64 keep_frames = 8
208 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).keepFramesX) },
209 | // int64 time_nanos = 9
210 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).TimeNanos) },
211 | // int64 duration_nanos = 10
212 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).DurationNanos) },
213 | // ValueType period_type = 11
214 | func(b *buffer, m message) error {
215 | x := new(ValueType)
216 | pp := m.(*Profile)
217 | pp.PeriodType = x
218 | return decodeMessage(b, x)
219 | },
220 | // int64 period = 12
221 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).Period) },
222 | // repeated int64 comment = 13
223 | func(b *buffer, m message) error { return decodeInt64s(b, &m.(*Profile).commentX) },
224 | // int64 defaultSampleType = 14
225 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).defaultSampleTypeX) },
226 | }
227 |
228 | // postDecode takes the unexported fields populated by decode (with
229 | // suffix X) and populates the corresponding exported fields.
230 | // The unexported fields are cleared up to facilitate testing.
231 | func (p *Profile) postDecode() error {
232 | var err error
233 | mappings := make(map[uint64]*Mapping, len(p.Mapping))
234 | mappingIds := make([]*Mapping, len(p.Mapping)+1)
235 | for _, m := range p.Mapping {
236 | m.File, err = getString(p.stringTable, &m.fileX, err)
237 | m.BuildID, err = getString(p.stringTable, &m.buildIDX, err)
238 | if m.ID < uint64(len(mappingIds)) {
239 | mappingIds[m.ID] = m
240 | } else {
241 | mappings[m.ID] = m
242 | }
243 | }
244 |
245 | functions := make(map[uint64]*Function, len(p.Function))
246 | functionIds := make([]*Function, len(p.Function)+1)
247 | for _, f := range p.Function {
248 | f.Name, err = getString(p.stringTable, &f.nameX, err)
249 | f.SystemName, err = getString(p.stringTable, &f.systemNameX, err)
250 | f.Filename, err = getString(p.stringTable, &f.filenameX, err)
251 | if f.ID < uint64(len(functionIds)) {
252 | functionIds[f.ID] = f
253 | } else {
254 | functions[f.ID] = f
255 | }
256 | }
257 |
258 | locations := make(map[uint64]*Location, len(p.Location))
259 | locationIds := make([]*Location, len(p.Location)+1)
260 | for _, l := range p.Location {
261 | if id := l.mappingIDX; id < uint64(len(mappingIds)) {
262 | l.Mapping = mappingIds[id]
263 | } else {
264 | l.Mapping = mappings[id]
265 | }
266 | l.mappingIDX = 0
267 | for i, ln := range l.Line {
268 | if id := ln.functionIDX; id != 0 {
269 | l.Line[i].functionIDX = 0
270 | if id < uint64(len(functionIds)) {
271 | l.Line[i].Function = functionIds[id]
272 | } else {
273 | l.Line[i].Function = functions[id]
274 | }
275 | }
276 | }
277 | if l.ID < uint64(len(locationIds)) {
278 | locationIds[l.ID] = l
279 | } else {
280 | locations[l.ID] = l
281 | }
282 | }
283 |
284 | for _, st := range p.SampleType {
285 | st.Type, err = getString(p.stringTable, &st.typeX, err)
286 | st.Unit, err = getString(p.stringTable, &st.unitX, err)
287 | }
288 |
289 | for _, s := range p.Sample {
290 | labels := make(map[string][]string, len(s.labelX))
291 | numLabels := make(map[string][]int64, len(s.labelX))
292 | for _, l := range s.labelX {
293 | var key, value string
294 | key, err = getString(p.stringTable, &l.keyX, err)
295 | if l.strX != 0 {
296 | value, err = getString(p.stringTable, &l.strX, err)
297 | labels[key] = append(labels[key], value)
298 | } else if l.numX != 0 {
299 | numLabels[key] = append(numLabels[key], l.numX)
300 | }
301 | }
302 | if len(labels) > 0 {
303 | s.Label = labels
304 | }
305 | if len(numLabels) > 0 {
306 | s.NumLabel = numLabels
307 | }
308 | s.Location = make([]*Location, len(s.locationIDX))
309 | for i, lid := range s.locationIDX {
310 | if lid < uint64(len(locationIds)) {
311 | s.Location[i] = locationIds[lid]
312 | } else {
313 | s.Location[i] = locations[lid]
314 | }
315 | }
316 | s.locationIDX = nil
317 | }
318 |
319 | p.DropFrames, err = getString(p.stringTable, &p.dropFramesX, err)
320 | p.KeepFrames, err = getString(p.stringTable, &p.keepFramesX, err)
321 |
322 | if pt := p.PeriodType; pt == nil {
323 | p.PeriodType = &ValueType{}
324 | }
325 |
326 | if pt := p.PeriodType; pt != nil {
327 | pt.Type, err = getString(p.stringTable, &pt.typeX, err)
328 | pt.Unit, err = getString(p.stringTable, &pt.unitX, err)
329 | }
330 |
331 | for _, i := range p.commentX {
332 | var c string
333 | c, err = getString(p.stringTable, &i, err)
334 | p.Comments = append(p.Comments, c)
335 | }
336 |
337 | p.commentX = nil
338 | p.DefaultSampleType, err = getString(p.stringTable, &p.defaultSampleTypeX, err)
339 | p.stringTable = nil
340 | return err
341 | }
342 |
343 | func (p *ValueType) decoder() []decoder {
344 | return valueTypeDecoder
345 | }
346 |
347 | func (p *ValueType) encode(b *buffer) {
348 | encodeInt64Opt(b, 1, p.typeX)
349 | encodeInt64Opt(b, 2, p.unitX)
350 | }
351 |
352 | var valueTypeDecoder = []decoder{
353 | nil, // 0
354 | // optional int64 type = 1
355 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*ValueType).typeX) },
356 | // optional int64 unit = 2
357 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*ValueType).unitX) },
358 | }
359 |
360 | func (p *Sample) decoder() []decoder {
361 | return sampleDecoder
362 | }
363 |
364 | func (p *Sample) encode(b *buffer) {
365 | encodeUint64s(b, 1, p.locationIDX)
366 | encodeInt64s(b, 2, p.Value)
367 | for _, x := range p.labelX {
368 | encodeMessage(b, 3, x)
369 | }
370 | }
371 |
372 | var sampleDecoder = []decoder{
373 | nil, // 0
374 | // repeated uint64 location = 1
375 | func(b *buffer, m message) error { return decodeUint64s(b, &m.(*Sample).locationIDX) },
376 | // repeated int64 value = 2
377 | func(b *buffer, m message) error { return decodeInt64s(b, &m.(*Sample).Value) },
378 | // repeated Label label = 3
379 | func(b *buffer, m message) error {
380 | s := m.(*Sample)
381 | n := len(s.labelX)
382 | s.labelX = append(s.labelX, label{})
383 | return decodeMessage(b, &s.labelX[n])
384 | },
385 | }
386 |
387 | func (p label) decoder() []decoder {
388 | return labelDecoder
389 | }
390 |
391 | func (p label) encode(b *buffer) {
392 | encodeInt64Opt(b, 1, p.keyX)
393 | encodeInt64Opt(b, 2, p.strX)
394 | encodeInt64Opt(b, 3, p.numX)
395 | }
396 |
397 | var labelDecoder = []decoder{
398 | nil, // 0
399 | // optional int64 key = 1
400 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*label).keyX) },
401 | // optional int64 str = 2
402 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*label).strX) },
403 | // optional int64 num = 3
404 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*label).numX) },
405 | }
406 |
407 | func (p *Mapping) decoder() []decoder {
408 | return mappingDecoder
409 | }
410 |
411 | func (p *Mapping) encode(b *buffer) {
412 | encodeUint64Opt(b, 1, p.ID)
413 | encodeUint64Opt(b, 2, p.Start)
414 | encodeUint64Opt(b, 3, p.Limit)
415 | encodeUint64Opt(b, 4, p.Offset)
416 | encodeInt64Opt(b, 5, p.fileX)
417 | encodeInt64Opt(b, 6, p.buildIDX)
418 | encodeBoolOpt(b, 7, p.HasFunctions)
419 | encodeBoolOpt(b, 8, p.HasFilenames)
420 | encodeBoolOpt(b, 9, p.HasLineNumbers)
421 | encodeBoolOpt(b, 10, p.HasInlineFrames)
422 | }
423 |
424 | var mappingDecoder = []decoder{
425 | nil, // 0
426 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Mapping).ID) }, // optional uint64 id = 1
427 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Mapping).Start) }, // optional uint64 memory_offset = 2
428 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Mapping).Limit) }, // optional uint64 memory_limit = 3
429 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Mapping).Offset) }, // optional uint64 file_offset = 4
430 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Mapping).fileX) }, // optional int64 filename = 5
431 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Mapping).buildIDX) }, // optional int64 build_id = 6
432 | func(b *buffer, m message) error { return decodeBool(b, &m.(*Mapping).HasFunctions) }, // optional bool has_functions = 7
433 | func(b *buffer, m message) error { return decodeBool(b, &m.(*Mapping).HasFilenames) }, // optional bool has_filenames = 8
434 | func(b *buffer, m message) error { return decodeBool(b, &m.(*Mapping).HasLineNumbers) }, // optional bool has_line_numbers = 9
435 | func(b *buffer, m message) error { return decodeBool(b, &m.(*Mapping).HasInlineFrames) }, // optional bool has_inline_frames = 10
436 | }
437 |
438 | func (p *Location) decoder() []decoder {
439 | return locationDecoder
440 | }
441 |
442 | func (p *Location) encode(b *buffer) {
443 | encodeUint64Opt(b, 1, p.ID)
444 | encodeUint64Opt(b, 2, p.mappingIDX)
445 | encodeUint64Opt(b, 3, p.Address)
446 | for i := range p.Line {
447 | encodeMessage(b, 4, &p.Line[i])
448 | }
449 | }
450 |
451 | var locationDecoder = []decoder{
452 | nil, // 0
453 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Location).ID) }, // optional uint64 id = 1;
454 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Location).mappingIDX) }, // optional uint64 mapping_id = 2;
455 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Location).Address) }, // optional uint64 address = 3;
456 | func(b *buffer, m message) error { // repeated Line line = 4
457 | pp := m.(*Location)
458 | n := len(pp.Line)
459 | pp.Line = append(pp.Line, Line{})
460 | return decodeMessage(b, &pp.Line[n])
461 | },
462 | }
463 |
464 | func (p *Line) decoder() []decoder {
465 | return lineDecoder
466 | }
467 |
468 | func (p *Line) encode(b *buffer) {
469 | encodeUint64Opt(b, 1, p.functionIDX)
470 | encodeInt64Opt(b, 2, p.Line)
471 | }
472 |
473 | var lineDecoder = []decoder{
474 | nil, // 0
475 | // optional uint64 function_id = 1
476 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Line).functionIDX) },
477 | // optional int64 line = 2
478 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Line).Line) },
479 | }
480 |
481 | func (p *Function) decoder() []decoder {
482 | return functionDecoder
483 | }
484 |
485 | func (p *Function) encode(b *buffer) {
486 | encodeUint64Opt(b, 1, p.ID)
487 | encodeInt64Opt(b, 2, p.nameX)
488 | encodeInt64Opt(b, 3, p.systemNameX)
489 | encodeInt64Opt(b, 4, p.filenameX)
490 | encodeInt64Opt(b, 5, p.StartLine)
491 | }
492 |
493 | var functionDecoder = []decoder{
494 | nil, // 0
495 | // optional uint64 id = 1
496 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Function).ID) },
497 | // optional int64 function_name = 2
498 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Function).nameX) },
499 | // optional int64 function_system_name = 3
500 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Function).systemNameX) },
501 | // repeated int64 filename = 4
502 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Function).filenameX) },
503 | // optional int64 start_line = 5
504 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Function).StartLine) },
505 | }
506 |
507 | func addString(strings map[string]int, s string) int64 {
508 | i, ok := strings[s]
509 | if !ok {
510 | i = len(strings)
511 | strings[s] = i
512 | }
513 | return int64(i)
514 | }
515 |
516 | func getString(strings []string, strng *int64, err error) (string, error) {
517 | if err != nil {
518 | return "", err
519 | }
520 | s := int(*strng)
521 | if s < 0 || s >= len(strings) {
522 | return "", errMalformed
523 | }
524 | *strng = 0
525 | return strings[s], nil
526 | }
527 |
--------------------------------------------------------------------------------
/internal/profile/profile.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Package profile provides a representation of profile.proto and
16 | // methods to encode/decode profiles in this format.
17 | package profile
18 |
19 | import (
20 | "bytes"
21 | "compress/gzip"
22 | "fmt"
23 | "io"
24 | "io/ioutil"
25 | "path/filepath"
26 | "regexp"
27 | "sort"
28 | "strings"
29 | "time"
30 | )
31 |
32 | // Profile is an in-memory representation of profile.proto.
33 | type Profile struct {
34 | SampleType []*ValueType
35 | DefaultSampleType string
36 | Sample []*Sample
37 | Mapping []*Mapping
38 | Location []*Location
39 | Function []*Function
40 | Comments []string
41 |
42 | DropFrames string
43 | KeepFrames string
44 |
45 | TimeNanos int64
46 | DurationNanos int64
47 | PeriodType *ValueType
48 | Period int64
49 |
50 | commentX []int64
51 | dropFramesX int64
52 | keepFramesX int64
53 | stringTable []string
54 | defaultSampleTypeX int64
55 | }
56 |
57 | // ValueType corresponds to Profile.ValueType
58 | type ValueType struct {
59 | Type string // cpu, wall, inuse_space, etc
60 | Unit string // seconds, nanoseconds, bytes, etc
61 |
62 | typeX int64
63 | unitX int64
64 | }
65 |
66 | // Sample corresponds to Profile.Sample
67 | type Sample struct {
68 | Location []*Location
69 | Value []int64
70 | Label map[string][]string
71 | NumLabel map[string][]int64
72 |
73 | locationIDX []uint64
74 | labelX []label
75 | }
76 |
77 | // label corresponds to Profile.Label
78 | type label struct {
79 | keyX int64
80 | // Exactly one of the two following values must be set
81 | strX int64
82 | numX int64 // Integer value for this label
83 | }
84 |
85 | // Mapping corresponds to Profile.Mapping
86 | type Mapping struct {
87 | ID uint64
88 | Start uint64
89 | Limit uint64
90 | Offset uint64
91 | File string
92 | BuildID string
93 | HasFunctions bool
94 | HasFilenames bool
95 | HasLineNumbers bool
96 | HasInlineFrames bool
97 |
98 | fileX int64
99 | buildIDX int64
100 | }
101 |
102 | // Location corresponds to Profile.Location
103 | type Location struct {
104 | ID uint64
105 | Mapping *Mapping
106 | Address uint64
107 | Line []Line
108 |
109 | mappingIDX uint64
110 | }
111 |
112 | // Line corresponds to Profile.Line
113 | type Line struct {
114 | Function *Function
115 | Line int64
116 |
117 | functionIDX uint64
118 | }
119 |
120 | // Function corresponds to Profile.Function
121 | type Function struct {
122 | ID uint64
123 | Name string
124 | SystemName string
125 | Filename string
126 | StartLine int64
127 |
128 | nameX int64
129 | systemNameX int64
130 | filenameX int64
131 | }
132 |
133 | // Parse parses a profile and checks for its validity. The input
134 | // may be a gzip-compressed encoded protobuf or one of many legacy
135 | // profile formats which may be unsupported in the future.
136 | func Parse(r io.Reader) (*Profile, error) {
137 | data, err := ioutil.ReadAll(r)
138 | if err != nil {
139 | return nil, err
140 | }
141 | return ParseData(data)
142 | }
143 |
144 | // ParseData parses a profile from a buffer and checks for its
145 | // validity.
146 | func ParseData(data []byte) (*Profile, error) {
147 | var p *Profile
148 | var err error
149 | if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
150 | gz, err := gzip.NewReader(bytes.NewBuffer(data))
151 | if err == nil {
152 | data, err = ioutil.ReadAll(gz)
153 | }
154 | if err != nil {
155 | return nil, fmt.Errorf("decompressing profile: %v", err)
156 | }
157 | }
158 | if p, err = ParseUncompressed(data); err != nil && err != errNoData {
159 | p, err = parseLegacy(data)
160 | }
161 |
162 | if err != nil {
163 | return nil, fmt.Errorf("parsing profile: %v", err)
164 | }
165 |
166 | if err := p.CheckValid(); err != nil {
167 | return nil, fmt.Errorf("malformed profile: %v", err)
168 | }
169 | return p, nil
170 | }
171 |
172 | var errUnrecognized = fmt.Errorf("unrecognized profile format")
173 | var errMalformed = fmt.Errorf("malformed profile format")
174 | var errNoData = fmt.Errorf("empty input file")
175 |
176 | func parseLegacy(data []byte) (*Profile, error) {
177 | parsers := []func([]byte) (*Profile, error){
178 | parseCPU,
179 | parseHeap,
180 | parseGoCount, // goroutine, threadcreate
181 | parseThread,
182 | parseContention,
183 | }
184 |
185 | for _, parser := range parsers {
186 | p, err := parser(data)
187 | if err == nil {
188 | p.addLegacyFrameInfo()
189 | return p, nil
190 | }
191 | if err != errUnrecognized {
192 | return nil, err
193 | }
194 | }
195 | return nil, errUnrecognized
196 | }
197 |
198 | // ParseUncompressed parses an uncompressed protobuf into a profile.
199 | func ParseUncompressed(data []byte) (*Profile, error) {
200 | if len(data) == 0 {
201 | return nil, errNoData
202 | }
203 | p := &Profile{}
204 | if err := unmarshal(data, p); err != nil {
205 | return nil, err
206 | }
207 |
208 | if err := p.postDecode(); err != nil {
209 | return nil, err
210 | }
211 |
212 | return p, nil
213 | }
214 |
215 | var libRx = regexp.MustCompile(`([.]so$|[.]so[._][0-9]+)`)
216 |
217 | // massageMappings applies heuristic-based changes to the profile
218 | // mappings to account for quirks of some environments.
219 | func (p *Profile) massageMappings() {
220 | // Merge adjacent regions with matching names, checking that the offsets match
221 | if len(p.Mapping) > 1 {
222 | mappings := []*Mapping{p.Mapping[0]}
223 | for _, m := range p.Mapping[1:] {
224 | lm := mappings[len(mappings)-1]
225 | if adjacent(lm, m) {
226 | lm.Limit = m.Limit
227 | if m.File != "" {
228 | lm.File = m.File
229 | }
230 | if m.BuildID != "" {
231 | lm.BuildID = m.BuildID
232 | }
233 | p.updateLocationMapping(m, lm)
234 | continue
235 | }
236 | mappings = append(mappings, m)
237 | }
238 | p.Mapping = mappings
239 | }
240 |
241 | // Use heuristics to identify main binary and move it to the top of the list of mappings
242 | for i, m := range p.Mapping {
243 | file := strings.TrimSpace(strings.Replace(m.File, "(deleted)", "", -1))
244 | if len(file) == 0 {
245 | continue
246 | }
247 | if len(libRx.FindStringSubmatch(file)) > 0 {
248 | continue
249 | }
250 | if file[0] == '[' {
251 | continue
252 | }
253 | // Swap what we guess is main to position 0.
254 | p.Mapping[0], p.Mapping[i] = p.Mapping[i], p.Mapping[0]
255 | break
256 | }
257 |
258 | // Keep the mapping IDs neatly sorted
259 | for i, m := range p.Mapping {
260 | m.ID = uint64(i + 1)
261 | }
262 | }
263 |
264 | // adjacent returns whether two mapping entries represent the same
265 | // mapping that has been split into two. Check that their addresses are adjacent,
266 | // and if the offsets match, if they are available.
267 | func adjacent(m1, m2 *Mapping) bool {
268 | if m1.File != "" && m2.File != "" {
269 | if m1.File != m2.File {
270 | return false
271 | }
272 | }
273 | if m1.BuildID != "" && m2.BuildID != "" {
274 | if m1.BuildID != m2.BuildID {
275 | return false
276 | }
277 | }
278 | if m1.Limit != m2.Start {
279 | return false
280 | }
281 | if m1.Offset != 0 && m2.Offset != 0 {
282 | offset := m1.Offset + (m1.Limit - m1.Start)
283 | if offset != m2.Offset {
284 | return false
285 | }
286 | }
287 | return true
288 | }
289 |
290 | func (p *Profile) updateLocationMapping(from, to *Mapping) {
291 | for _, l := range p.Location {
292 | if l.Mapping == from {
293 | l.Mapping = to
294 | }
295 | }
296 | }
297 |
298 | // Write writes the profile as a gzip-compressed marshaled protobuf.
299 | func (p *Profile) Write(w io.Writer) error {
300 | p.preEncode()
301 | b := marshal(p)
302 | zw := gzip.NewWriter(w)
303 | defer zw.Close()
304 | _, err := zw.Write(b)
305 | return err
306 | }
307 |
308 | // WriteUncompressed writes the profile as a marshaled protobuf.
309 | func (p *Profile) WriteUncompressed(w io.Writer) error {
310 | p.preEncode()
311 | b := marshal(p)
312 | _, err := w.Write(b)
313 | return err
314 | }
315 |
316 | // CheckValid tests whether the profile is valid. Checks include, but are
317 | // not limited to:
318 | // - len(Profile.Sample[n].value) == len(Profile.value_unit)
319 | // - Sample.id has a corresponding Profile.Location
320 | func (p *Profile) CheckValid() error {
321 | // Check that sample values are consistent
322 | sampleLen := len(p.SampleType)
323 | if sampleLen == 0 && len(p.Sample) != 0 {
324 | return fmt.Errorf("missing sample type information")
325 | }
326 | for _, s := range p.Sample {
327 | if len(s.Value) != sampleLen {
328 | return fmt.Errorf("mismatch: sample has: %d values vs. %d types", len(s.Value), len(p.SampleType))
329 | }
330 | for _, l := range s.Location {
331 | if l == nil {
332 | return fmt.Errorf("sample has nil location")
333 | }
334 | }
335 | }
336 |
337 | // Check that all mappings/locations/functions are in the tables
338 | // Check that there are no duplicate ids
339 | mappings := make(map[uint64]*Mapping, len(p.Mapping))
340 | for _, m := range p.Mapping {
341 | if m.ID == 0 {
342 | return fmt.Errorf("found mapping with reserved ID=0")
343 | }
344 | if mappings[m.ID] != nil {
345 | return fmt.Errorf("multiple mappings with same id: %d", m.ID)
346 | }
347 | mappings[m.ID] = m
348 | }
349 | functions := make(map[uint64]*Function, len(p.Function))
350 | for _, f := range p.Function {
351 | if f.ID == 0 {
352 | return fmt.Errorf("found function with reserved ID=0")
353 | }
354 | if functions[f.ID] != nil {
355 | return fmt.Errorf("multiple functions with same id: %d", f.ID)
356 | }
357 | functions[f.ID] = f
358 | }
359 | locations := make(map[uint64]*Location, len(p.Location))
360 | for _, l := range p.Location {
361 | if l.ID == 0 {
362 | return fmt.Errorf("found location with reserved id=0")
363 | }
364 | if locations[l.ID] != nil {
365 | return fmt.Errorf("multiple locations with same id: %d", l.ID)
366 | }
367 | locations[l.ID] = l
368 | if m := l.Mapping; m != nil {
369 | if m.ID == 0 || mappings[m.ID] != m {
370 | return fmt.Errorf("inconsistent mapping %p: %d", m, m.ID)
371 | }
372 | }
373 | for _, ln := range l.Line {
374 | if f := ln.Function; f != nil {
375 | if f.ID == 0 || functions[f.ID] != f {
376 | return fmt.Errorf("inconsistent function %p: %d", f, f.ID)
377 | }
378 | }
379 | }
380 | }
381 | return nil
382 | }
383 |
384 | // Aggregate merges the locations in the profile into equivalence
385 | // classes preserving the request attributes. It also updates the
386 | // samples to point to the merged locations.
387 | func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, address bool) error {
388 | for _, m := range p.Mapping {
389 | m.HasInlineFrames = m.HasInlineFrames && inlineFrame
390 | m.HasFunctions = m.HasFunctions && function
391 | m.HasFilenames = m.HasFilenames && filename
392 | m.HasLineNumbers = m.HasLineNumbers && linenumber
393 | }
394 |
395 | // Aggregate functions
396 | if !function || !filename {
397 | for _, f := range p.Function {
398 | if !function {
399 | f.Name = ""
400 | f.SystemName = ""
401 | }
402 | if !filename {
403 | f.Filename = ""
404 | }
405 | }
406 | }
407 |
408 | // Aggregate locations
409 | if !inlineFrame || !address || !linenumber {
410 | for _, l := range p.Location {
411 | if !inlineFrame && len(l.Line) > 1 {
412 | l.Line = l.Line[len(l.Line)-1:]
413 | }
414 | if !linenumber {
415 | for i := range l.Line {
416 | l.Line[i].Line = 0
417 | }
418 | }
419 | if !address {
420 | l.Address = 0
421 | }
422 | }
423 | }
424 |
425 | return p.CheckValid()
426 | }
427 |
428 | // String dumps a text representation of a profile. Intended mainly
429 | // for debugging purposes.
430 | func (p *Profile) String() string {
431 | ss := make([]string, 0, len(p.Comments)+len(p.Sample)+len(p.Mapping)+len(p.Location))
432 | for _, c := range p.Comments {
433 | ss = append(ss, "Comment: "+c)
434 | }
435 | if pt := p.PeriodType; pt != nil {
436 | ss = append(ss, fmt.Sprintf("PeriodType: %s %s", pt.Type, pt.Unit))
437 | }
438 | ss = append(ss, fmt.Sprintf("Period: %d", p.Period))
439 | if p.TimeNanos != 0 {
440 | ss = append(ss, fmt.Sprintf("Time: %v", time.Unix(0, p.TimeNanos)))
441 | }
442 | if p.DurationNanos != 0 {
443 | ss = append(ss, fmt.Sprintf("Duration: %.4v", time.Duration(p.DurationNanos)))
444 | }
445 |
446 | ss = append(ss, "Samples:")
447 | var sh1 string
448 | for _, s := range p.SampleType {
449 | dflt := ""
450 | if s.Type == p.DefaultSampleType {
451 | dflt = "[dflt]"
452 | }
453 | sh1 = sh1 + fmt.Sprintf("%s/%s%s ", s.Type, s.Unit, dflt)
454 | }
455 | ss = append(ss, strings.TrimSpace(sh1))
456 | for _, s := range p.Sample {
457 | var sv string
458 | for _, v := range s.Value {
459 | sv = fmt.Sprintf("%s %10d", sv, v)
460 | }
461 | sv = sv + ": "
462 | for _, l := range s.Location {
463 | sv = sv + fmt.Sprintf("%d ", l.ID)
464 | }
465 | ss = append(ss, sv)
466 | const labelHeader = " "
467 | if len(s.Label) > 0 {
468 | ls := []string{}
469 | for k, v := range s.Label {
470 | ls = append(ls, fmt.Sprintf("%s:%v", k, v))
471 | }
472 | sort.Strings(ls)
473 | ss = append(ss, labelHeader+strings.Join(ls, " "))
474 | }
475 | if len(s.NumLabel) > 0 {
476 | ls := []string{}
477 | for k, v := range s.NumLabel {
478 | ls = append(ls, fmt.Sprintf("%s:%v", k, v))
479 | }
480 | sort.Strings(ls)
481 | ss = append(ss, labelHeader+strings.Join(ls, " "))
482 | }
483 | }
484 |
485 | ss = append(ss, "Locations")
486 | for _, l := range p.Location {
487 | locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address)
488 | if m := l.Mapping; m != nil {
489 | locStr = locStr + fmt.Sprintf("M=%d ", m.ID)
490 | }
491 | if len(l.Line) == 0 {
492 | ss = append(ss, locStr)
493 | }
494 | for li := range l.Line {
495 | lnStr := "??"
496 | if fn := l.Line[li].Function; fn != nil {
497 | lnStr = fmt.Sprintf("%s %s:%d s=%d",
498 | fn.Name,
499 | fn.Filename,
500 | l.Line[li].Line,
501 | fn.StartLine)
502 | if fn.Name != fn.SystemName {
503 | lnStr = lnStr + "(" + fn.SystemName + ")"
504 | }
505 | }
506 | ss = append(ss, locStr+lnStr)
507 | // Do not print location details past the first line
508 | locStr = " "
509 | }
510 | }
511 |
512 | ss = append(ss, "Mappings")
513 | for _, m := range p.Mapping {
514 | bits := ""
515 | if m.HasFunctions {
516 | bits = bits + "[FN]"
517 | }
518 | if m.HasFilenames {
519 | bits = bits + "[FL]"
520 | }
521 | if m.HasLineNumbers {
522 | bits = bits + "[LN]"
523 | }
524 | if m.HasInlineFrames {
525 | bits = bits + "[IN]"
526 | }
527 | ss = append(ss, fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s",
528 | m.ID,
529 | m.Start, m.Limit, m.Offset,
530 | m.File,
531 | m.BuildID,
532 | bits))
533 | }
534 |
535 | return strings.Join(ss, "\n") + "\n"
536 | }
537 |
538 | // Scale multiplies all sample values in a profile by a constant.
539 | func (p *Profile) Scale(ratio float64) {
540 | if ratio == 1 {
541 | return
542 | }
543 | ratios := make([]float64, len(p.SampleType))
544 | for i := range p.SampleType {
545 | ratios[i] = ratio
546 | }
547 | p.ScaleN(ratios)
548 | }
549 |
550 | // ScaleN multiplies each sample values in a sample by a different amount.
551 | func (p *Profile) ScaleN(ratios []float64) error {
552 | if len(p.SampleType) != len(ratios) {
553 | return fmt.Errorf("mismatched scale ratios, got %d, want %d", len(ratios), len(p.SampleType))
554 | }
555 | allOnes := true
556 | for _, r := range ratios {
557 | if r != 1 {
558 | allOnes = false
559 | break
560 | }
561 | }
562 | if allOnes {
563 | return nil
564 | }
565 | for _, s := range p.Sample {
566 | for i, v := range s.Value {
567 | if ratios[i] != 1 {
568 | s.Value[i] = int64(float64(v) * ratios[i])
569 | }
570 | }
571 | }
572 | return nil
573 | }
574 |
575 | // HasFunctions determines if all locations in this profile have
576 | // symbolized function information.
577 | func (p *Profile) HasFunctions() bool {
578 | for _, l := range p.Location {
579 | if l.Mapping != nil && !l.Mapping.HasFunctions {
580 | return false
581 | }
582 | }
583 | return true
584 | }
585 |
586 | // HasFileLines determines if all locations in this profile have
587 | // symbolized file and line number information.
588 | func (p *Profile) HasFileLines() bool {
589 | for _, l := range p.Location {
590 | if l.Mapping != nil && (!l.Mapping.HasFilenames || !l.Mapping.HasLineNumbers) {
591 | return false
592 | }
593 | }
594 | return true
595 | }
596 |
597 | // Unsymbolizable returns true if a mapping points to a binary for which
598 | // locations can't be symbolized in principle, at least now. Examples are
599 | // "[vdso]", [vsyscall]" and some others, see the code.
600 | func (m *Mapping) Unsymbolizable() bool {
601 | name := filepath.Base(m.File)
602 | return strings.HasPrefix(name, "[") || strings.HasPrefix(name, "linux-vdso") || strings.HasPrefix(m.File, "/dev/dri/")
603 | }
604 |
605 | // Copy makes a fully independent copy of a profile.
606 | func (p *Profile) Copy() *Profile {
607 | p.preEncode()
608 | b := marshal(p)
609 |
610 | pp := &Profile{}
611 | if err := unmarshal(b, pp); err != nil {
612 | panic(err)
613 | }
614 | if err := pp.postDecode(); err != nil {
615 | panic(err)
616 | }
617 |
618 | return pp
619 | }
620 |
--------------------------------------------------------------------------------
/internal/profile/legacy_profile.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // This file implements parsers to convert legacy profiles into the
6 | // profile.proto format.
7 |
8 | // Forked from Go distributions cmd/pprof/internal/profile.
9 |
10 | package profile
11 |
12 | import (
13 | "bufio"
14 | "bytes"
15 | "fmt"
16 | "io"
17 | "math"
18 | "regexp"
19 | "strconv"
20 | "strings"
21 | )
22 |
23 | var (
24 | countStartRE = regexp.MustCompile(`\A(\w+) profile: total \d+\n\z`)
25 | countRE = regexp.MustCompile(`\A(\d+) @(( 0x[0-9a-f]+)+)\n\z`)
26 |
27 | heapHeaderRE = regexp.MustCompile(`heap profile: *(\d+): *(\d+) *\[ *(\d+): *(\d+) *\] *@ *(heap[_a-z0-9]*)/?(\d*)`)
28 | heapSampleRE = regexp.MustCompile(`(-?\d+): *(-?\d+) *\[ *(\d+): *(\d+) *] @([ x0-9a-f]*)`)
29 |
30 | contentionSampleRE = regexp.MustCompile(`(\d+) *(\d+) @([ x0-9a-f]*)`)
31 |
32 | hexNumberRE = regexp.MustCompile(`0x[0-9a-f]+`)
33 |
34 | growthHeaderRE = regexp.MustCompile(`heap profile: *(\d+): *(\d+) *\[ *(\d+): *(\d+) *\] @ growthz`)
35 |
36 | fragmentationHeaderRE = regexp.MustCompile(`heap profile: *(\d+): *(\d+) *\[ *(\d+): *(\d+) *\] @ fragmentationz`)
37 |
38 | threadzStartRE = regexp.MustCompile(`--- threadz \d+ ---`)
39 | threadStartRE = regexp.MustCompile(`--- Thread ([[:xdigit:]]+) \(name: (.*)/(\d+)\) stack: ---`)
40 |
41 | procMapsRE = regexp.MustCompile(`([[:xdigit:]]+)-([[:xdigit:]]+)\s+([-rwxp]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+):([[:xdigit:]]+)\s+([[:digit:]]+)\s*(\S+)?`)
42 |
43 | briefMapsRE = regexp.MustCompile(`\s*([[:xdigit:]]+)-([[:xdigit:]]+):\s*(\S+)(\s.*@)?([[:xdigit:]]+)?`)
44 |
45 | // LegacyHeapAllocated instructs the heapz parsers to use the
46 | // allocated memory stats instead of the default in-use memory. Note
47 | // that tcmalloc doesn't provide all allocated memory, only in-use
48 | // stats.
49 | LegacyHeapAllocated bool
50 | )
51 |
52 | func isSpaceOrComment(line string) bool {
53 | trimmed := strings.TrimSpace(line)
54 | return len(trimmed) == 0 || trimmed[0] == '#'
55 | }
56 |
57 | // parseGoCount parses a Go count profile (e.g., threadcreate or
58 | // goroutine) and returns a new Profile.
59 | func parseGoCount(b []byte) (*Profile, error) {
60 | r := bytes.NewBuffer(b)
61 |
62 | var line string
63 | var err error
64 | for {
65 | // Skip past comments and empty lines seeking a real header.
66 | line, err = r.ReadString('\n')
67 | if err != nil {
68 | return nil, err
69 | }
70 | if !isSpaceOrComment(line) {
71 | break
72 | }
73 | }
74 |
75 | m := countStartRE.FindStringSubmatch(line)
76 | if m == nil {
77 | return nil, errUnrecognized
78 | }
79 | profileType := string(m[1])
80 | p := &Profile{
81 | PeriodType: &ValueType{Type: profileType, Unit: "count"},
82 | Period: 1,
83 | SampleType: []*ValueType{{Type: profileType, Unit: "count"}},
84 | }
85 | locations := make(map[uint64]*Location)
86 | for {
87 | line, err = r.ReadString('\n')
88 | if err != nil {
89 | if err == io.EOF {
90 | break
91 | }
92 | return nil, err
93 | }
94 | if isSpaceOrComment(line) {
95 | continue
96 | }
97 | if strings.HasPrefix(line, "---") {
98 | break
99 | }
100 | m := countRE.FindStringSubmatch(line)
101 | if m == nil {
102 | return nil, errMalformed
103 | }
104 | n, err := strconv.ParseInt(string(m[1]), 0, 64)
105 | if err != nil {
106 | return nil, errMalformed
107 | }
108 | fields := strings.Fields(string(m[2]))
109 | locs := make([]*Location, 0, len(fields))
110 | for _, stk := range fields {
111 | addr, err := strconv.ParseUint(stk, 0, 64)
112 | if err != nil {
113 | return nil, errMalformed
114 | }
115 | // Adjust all frames by -1 (except the leaf) to land on top of
116 | // the call instruction.
117 | if len(locs) > 0 {
118 | addr--
119 | }
120 | loc := locations[addr]
121 | if loc == nil {
122 | loc = &Location{
123 | Address: addr,
124 | }
125 | locations[addr] = loc
126 | p.Location = append(p.Location, loc)
127 | }
128 | locs = append(locs, loc)
129 | }
130 | p.Sample = append(p.Sample, &Sample{
131 | Location: locs,
132 | Value: []int64{n},
133 | })
134 | }
135 |
136 | if err = parseAdditionalSections(strings.TrimSpace(line), r, p); err != nil {
137 | return nil, err
138 | }
139 | return p, nil
140 | }
141 |
142 | // remapLocationIDs ensures there is a location for each address
143 | // referenced by a sample, and remaps the samples to point to the new
144 | // location ids.
145 | func (p *Profile) remapLocationIDs() {
146 | seen := make(map[*Location]bool, len(p.Location))
147 | var locs []*Location
148 |
149 | for _, s := range p.Sample {
150 | for _, l := range s.Location {
151 | if seen[l] {
152 | continue
153 | }
154 | l.ID = uint64(len(locs) + 1)
155 | locs = append(locs, l)
156 | seen[l] = true
157 | }
158 | }
159 | p.Location = locs
160 | }
161 |
162 | func (p *Profile) remapFunctionIDs() {
163 | seen := make(map[*Function]bool, len(p.Function))
164 | var fns []*Function
165 |
166 | for _, l := range p.Location {
167 | for _, ln := range l.Line {
168 | fn := ln.Function
169 | if fn == nil || seen[fn] {
170 | continue
171 | }
172 | fn.ID = uint64(len(fns) + 1)
173 | fns = append(fns, fn)
174 | seen[fn] = true
175 | }
176 | }
177 | p.Function = fns
178 | }
179 |
180 | // remapMappingIDs matches location addresses with existing mappings
181 | // and updates them appropriately. This is O(N*M), if this ever shows
182 | // up as a bottleneck, evaluate sorting the mappings and doing a
183 | // binary search, which would make it O(N*log(M)).
184 | func (p *Profile) remapMappingIDs() {
185 | if len(p.Mapping) == 0 {
186 | return
187 | }
188 |
189 | // Some profile handlers will incorrectly set regions for the main
190 | // executable if its section is remapped. Fix them through heuristics.
191 |
192 | // Remove the initial mapping if named '/anon_hugepage' and has a
193 | // consecutive adjacent mapping.
194 | if m := p.Mapping[0]; strings.HasPrefix(m.File, "/anon_hugepage") {
195 | if len(p.Mapping) > 1 && m.Limit == p.Mapping[1].Start {
196 | p.Mapping = p.Mapping[1:]
197 | }
198 | }
199 |
200 | // Subtract the offset from the start of the main mapping if it
201 | // ends up at a recognizable start address.
202 | const expectedStart = 0x400000
203 | if m := p.Mapping[0]; m.Start-m.Offset == expectedStart {
204 | m.Start = expectedStart
205 | m.Offset = 0
206 | }
207 |
208 | for _, l := range p.Location {
209 | if a := l.Address; a != 0 {
210 | for _, m := range p.Mapping {
211 | if m.Start <= a && a < m.Limit {
212 | l.Mapping = m
213 | break
214 | }
215 | }
216 | }
217 | }
218 |
219 | // Reset all mapping IDs.
220 | for i, m := range p.Mapping {
221 | m.ID = uint64(i + 1)
222 | }
223 | }
224 |
225 | var cpuInts = []func([]byte) (uint64, []byte){
226 | get32l,
227 | get32b,
228 | get64l,
229 | get64b,
230 | }
231 |
232 | func get32l(b []byte) (uint64, []byte) {
233 | if len(b) < 4 {
234 | return 0, nil
235 | }
236 | return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24, b[4:]
237 | }
238 |
239 | func get32b(b []byte) (uint64, []byte) {
240 | if len(b) < 4 {
241 | return 0, nil
242 | }
243 | return uint64(b[3]) | uint64(b[2])<<8 | uint64(b[1])<<16 | uint64(b[0])<<24, b[4:]
244 | }
245 |
246 | func get64l(b []byte) (uint64, []byte) {
247 | if len(b) < 8 {
248 | return 0, nil
249 | }
250 | return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56, b[8:]
251 | }
252 |
253 | func get64b(b []byte) (uint64, []byte) {
254 | if len(b) < 8 {
255 | return 0, nil
256 | }
257 | return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56, b[8:]
258 | }
259 |
260 | // ParseTracebacks parses a set of tracebacks and returns a newly
261 | // populated profile. It will accept any text file and generate a
262 | // Profile out of it with any hex addresses it can identify, including
263 | // a process map if it can recognize one. Each sample will include a
264 | // tag "source" with the addresses recognized in string format.
265 | func ParseTracebacks(b []byte) (*Profile, error) {
266 | r := bytes.NewBuffer(b)
267 |
268 | p := &Profile{
269 | PeriodType: &ValueType{Type: "trace", Unit: "count"},
270 | Period: 1,
271 | SampleType: []*ValueType{
272 | {Type: "trace", Unit: "count"},
273 | },
274 | }
275 |
276 | var sources []string
277 | var sloc []*Location
278 |
279 | locs := make(map[uint64]*Location)
280 | for {
281 | l, err := r.ReadString('\n')
282 | if err != nil {
283 | if err != io.EOF {
284 | return nil, err
285 | }
286 | if l == "" {
287 | break
288 | }
289 | }
290 | if sectionTrigger(l) == memoryMapSection {
291 | break
292 | }
293 | if s, addrs := extractHexAddresses(l); len(s) > 0 {
294 | for _, addr := range addrs {
295 | // Addresses from stack traces point to the next instruction after
296 | // each call. Adjust by -1 to land somewhere on the actual call
297 | // (except for the leaf, which is not a call).
298 | if len(sloc) > 0 {
299 | addr--
300 | }
301 | loc := locs[addr]
302 | if locs[addr] == nil {
303 | loc = &Location{
304 | Address: addr,
305 | }
306 | p.Location = append(p.Location, loc)
307 | locs[addr] = loc
308 | }
309 | sloc = append(sloc, loc)
310 | }
311 |
312 | sources = append(sources, s...)
313 | } else {
314 | if len(sources) > 0 || len(sloc) > 0 {
315 | addTracebackSample(sloc, sources, p)
316 | sloc, sources = nil, nil
317 | }
318 | }
319 | }
320 |
321 | // Add final sample to save any leftover data.
322 | if len(sources) > 0 || len(sloc) > 0 {
323 | addTracebackSample(sloc, sources, p)
324 | }
325 |
326 | if err := p.ParseMemoryMap(r); err != nil {
327 | return nil, err
328 | }
329 | return p, nil
330 | }
331 |
332 | func addTracebackSample(l []*Location, s []string, p *Profile) {
333 | p.Sample = append(p.Sample,
334 | &Sample{
335 | Value: []int64{1},
336 | Location: l,
337 | Label: map[string][]string{"source": s},
338 | })
339 | }
340 |
341 | // parseCPU parses a profilez legacy profile and returns a newly
342 | // populated Profile.
343 | //
344 | // The general format for profilez samples is a sequence of words in
345 | // binary format. The first words are a header with the following data:
346 | // 1st word -- 0
347 | // 2nd word -- 3
348 | // 3rd word -- 0 if a c++ application, 1 if a java application.
349 | // 4th word -- Sampling period (in microseconds).
350 | // 5th word -- Padding.
351 | func parseCPU(b []byte) (*Profile, error) {
352 | var parse func([]byte) (uint64, []byte)
353 | var n1, n2, n3, n4, n5 uint64
354 | for _, parse = range cpuInts {
355 | var tmp []byte
356 | n1, tmp = parse(b)
357 | n2, tmp = parse(tmp)
358 | n3, tmp = parse(tmp)
359 | n4, tmp = parse(tmp)
360 | n5, tmp = parse(tmp)
361 |
362 | if tmp != nil && n1 == 0 && n2 == 3 && n3 == 0 && n4 > 0 && n5 == 0 {
363 | b = tmp
364 | return cpuProfile(b, int64(n4), parse)
365 | }
366 | }
367 | return nil, errUnrecognized
368 | }
369 |
370 | // cpuProfile returns a new Profile from C++ profilez data.
371 | // b is the profile bytes after the header, period is the profiling
372 | // period, and parse is a function to parse 8-byte chunks from the
373 | // profile in its native endianness.
374 | func cpuProfile(b []byte, period int64, parse func(b []byte) (uint64, []byte)) (*Profile, error) {
375 | p := &Profile{
376 | Period: period * 1000,
377 | PeriodType: &ValueType{Type: "cpu", Unit: "nanoseconds"},
378 | SampleType: []*ValueType{
379 | {Type: "samples", Unit: "count"},
380 | {Type: "cpu", Unit: "nanoseconds"},
381 | },
382 | }
383 | var err error
384 | if b, _, err = parseCPUSamples(b, parse, true, p); err != nil {
385 | return nil, err
386 | }
387 |
388 | // If all samples have the same second-to-the-bottom frame, it
389 | // strongly suggests that it is an uninteresting artifact of
390 | // measurement -- a stack frame pushed by the signal handler. The
391 | // bottom frame is always correct as it is picked up from the signal
392 | // structure, not the stack. Check if this is the case and if so,
393 | // remove.
394 | if len(p.Sample) > 1 && len(p.Sample[0].Location) > 1 {
395 | allSame := true
396 | id1 := p.Sample[0].Location[1].Address
397 | for _, s := range p.Sample {
398 | if len(s.Location) < 2 || id1 != s.Location[1].Address {
399 | allSame = false
400 | break
401 | }
402 | }
403 | if allSame {
404 | for _, s := range p.Sample {
405 | s.Location = append(s.Location[:1], s.Location[2:]...)
406 | }
407 | }
408 | }
409 |
410 | if err := p.ParseMemoryMap(bytes.NewBuffer(b)); err != nil {
411 | return nil, err
412 | }
413 | return p, nil
414 | }
415 |
416 | // parseCPUSamples parses a collection of profilez samples from a
417 | // profile.
418 | //
419 | // profilez samples are a repeated sequence of stack frames of the
420 | // form:
421 | // 1st word -- The number of times this stack was encountered.
422 | // 2nd word -- The size of the stack (StackSize).
423 | // 3rd word -- The first address on the stack.
424 | // ...
425 | // StackSize + 2 -- The last address on the stack
426 | // The last stack trace is of the form:
427 | // 1st word -- 0
428 | // 2nd word -- 1
429 | // 3rd word -- 0
430 | //
431 | // Addresses from stack traces may point to the next instruction after
432 | // each call. Optionally adjust by -1 to land somewhere on the actual
433 | // call (except for the leaf, which is not a call).
434 | func parseCPUSamples(b []byte, parse func(b []byte) (uint64, []byte), adjust bool, p *Profile) ([]byte, map[uint64]*Location, error) {
435 | locs := make(map[uint64]*Location)
436 | for len(b) > 0 {
437 | var count, nstk uint64
438 | count, b = parse(b)
439 | nstk, b = parse(b)
440 | if b == nil || nstk > uint64(len(b)/4) {
441 | return nil, nil, errUnrecognized
442 | }
443 | var sloc []*Location
444 | addrs := make([]uint64, nstk)
445 | for i := 0; i < int(nstk); i++ {
446 | addrs[i], b = parse(b)
447 | }
448 |
449 | if count == 0 && nstk == 1 && addrs[0] == 0 {
450 | // End of data marker
451 | break
452 | }
453 | for i, addr := range addrs {
454 | if adjust && i > 0 {
455 | addr--
456 | }
457 | loc := locs[addr]
458 | if loc == nil {
459 | loc = &Location{
460 | Address: addr,
461 | }
462 | locs[addr] = loc
463 | p.Location = append(p.Location, loc)
464 | }
465 | sloc = append(sloc, loc)
466 | }
467 | p.Sample = append(p.Sample,
468 | &Sample{
469 | Value: []int64{int64(count), int64(count) * int64(p.Period)},
470 | Location: sloc,
471 | })
472 | }
473 | // Reached the end without finding the EOD marker.
474 | return b, locs, nil
475 | }
476 |
477 | // parseHeap parses a heapz legacy or a growthz profile and
478 | // returns a newly populated Profile.
479 | func parseHeap(b []byte) (p *Profile, err error) {
480 | r := bytes.NewBuffer(b)
481 | l, err := r.ReadString('\n')
482 | if err != nil {
483 | return nil, errUnrecognized
484 | }
485 |
486 | sampling := ""
487 |
488 | if header := heapHeaderRE.FindStringSubmatch(l); header != nil {
489 | p = &Profile{
490 | SampleType: []*ValueType{
491 | {Type: "objects", Unit: "count"},
492 | {Type: "space", Unit: "bytes"},
493 | },
494 | PeriodType: &ValueType{Type: "objects", Unit: "bytes"},
495 | }
496 |
497 | var period int64
498 | if len(header[6]) > 0 {
499 | if period, err = strconv.ParseInt(string(header[6]), 10, 64); err != nil {
500 | return nil, errUnrecognized
501 | }
502 | }
503 |
504 | switch header[5] {
505 | case "heapz_v2", "heap_v2":
506 | sampling, p.Period = "v2", period
507 | case "heapprofile":
508 | sampling, p.Period = "", 1
509 | case "heap":
510 | sampling, p.Period = "v2", period/2
511 | default:
512 | return nil, errUnrecognized
513 | }
514 | } else if header = growthHeaderRE.FindStringSubmatch(l); header != nil {
515 | p = &Profile{
516 | SampleType: []*ValueType{
517 | {Type: "objects", Unit: "count"},
518 | {Type: "space", Unit: "bytes"},
519 | },
520 | PeriodType: &ValueType{Type: "heapgrowth", Unit: "count"},
521 | Period: 1,
522 | }
523 | } else if header = fragmentationHeaderRE.FindStringSubmatch(l); header != nil {
524 | p = &Profile{
525 | SampleType: []*ValueType{
526 | {Type: "objects", Unit: "count"},
527 | {Type: "space", Unit: "bytes"},
528 | },
529 | PeriodType: &ValueType{Type: "allocations", Unit: "count"},
530 | Period: 1,
531 | }
532 | } else {
533 | return nil, errUnrecognized
534 | }
535 |
536 | if LegacyHeapAllocated {
537 | for _, st := range p.SampleType {
538 | st.Type = "alloc_" + st.Type
539 | }
540 | } else {
541 | for _, st := range p.SampleType {
542 | st.Type = "inuse_" + st.Type
543 | }
544 | }
545 |
546 | locs := make(map[uint64]*Location)
547 | for {
548 | l, err = r.ReadString('\n')
549 | if err != nil {
550 | if err != io.EOF {
551 | return nil, err
552 | }
553 |
554 | if l == "" {
555 | break
556 | }
557 | }
558 |
559 | if isSpaceOrComment(l) {
560 | continue
561 | }
562 | l = strings.TrimSpace(l)
563 |
564 | if sectionTrigger(l) != unrecognizedSection {
565 | break
566 | }
567 |
568 | value, blocksize, addrs, err := parseHeapSample(l, p.Period, sampling)
569 | if err != nil {
570 | return nil, err
571 | }
572 | var sloc []*Location
573 | for i, addr := range addrs {
574 | // Addresses from stack traces point to the next instruction after
575 | // each call. Adjust by -1 to land somewhere on the actual call
576 | // (except for the leaf, which is not a call).
577 | if i > 0 {
578 | addr--
579 | }
580 | loc := locs[addr]
581 | if locs[addr] == nil {
582 | loc = &Location{
583 | Address: addr,
584 | }
585 | p.Location = append(p.Location, loc)
586 | locs[addr] = loc
587 | }
588 | sloc = append(sloc, loc)
589 | }
590 |
591 | p.Sample = append(p.Sample, &Sample{
592 | Value: value,
593 | Location: sloc,
594 | NumLabel: map[string][]int64{"bytes": {blocksize}},
595 | })
596 | }
597 |
598 | if err = parseAdditionalSections(l, r, p); err != nil {
599 | return nil, err
600 | }
601 | return p, nil
602 | }
603 |
604 | // parseHeapSample parses a single row from a heap profile into a new Sample.
605 | func parseHeapSample(line string, rate int64, sampling string) (value []int64, blocksize int64, addrs []uint64, err error) {
606 | sampleData := heapSampleRE.FindStringSubmatch(line)
607 | if len(sampleData) != 6 {
608 | return value, blocksize, addrs, fmt.Errorf("unexpected number of sample values: got %d, want 6", len(sampleData))
609 | }
610 |
611 | // Use first two values by default; tcmalloc sampling generates the
612 | // same value for both, only the older heap-profile collect separate
613 | // stats for in-use and allocated objects.
614 | valueIndex := 1
615 | if LegacyHeapAllocated {
616 | valueIndex = 3
617 | }
618 |
619 | var v1, v2 int64
620 | if v1, err = strconv.ParseInt(sampleData[valueIndex], 10, 64); err != nil {
621 | return value, blocksize, addrs, fmt.Errorf("malformed sample: %s: %v", line, err)
622 | }
623 | if v2, err = strconv.ParseInt(sampleData[valueIndex+1], 10, 64); err != nil {
624 | return value, blocksize, addrs, fmt.Errorf("malformed sample: %s: %v", line, err)
625 | }
626 |
627 | if v1 == 0 {
628 | if v2 != 0 {
629 | return value, blocksize, addrs, fmt.Errorf("allocation count was 0 but allocation bytes was %d", v2)
630 | }
631 | } else {
632 | blocksize = v2 / v1
633 | if sampling == "v2" {
634 | v1, v2 = scaleHeapSample(v1, v2, rate)
635 | }
636 | }
637 |
638 | value = []int64{v1, v2}
639 | addrs = parseHexAddresses(sampleData[5])
640 |
641 | return value, blocksize, addrs, nil
642 | }
643 |
644 | // extractHexAddresses extracts hex numbers from a string and returns
645 | // them, together with their numeric value, in a slice.
646 | func extractHexAddresses(s string) ([]string, []uint64) {
647 | hexStrings := hexNumberRE.FindAllString(s, -1)
648 | var ids []uint64
649 | for _, s := range hexStrings {
650 | if id, err := strconv.ParseUint(s, 0, 64); err == nil {
651 | ids = append(ids, id)
652 | } else {
653 | // Do not expect any parsing failures due to the regexp matching.
654 | panic("failed to parse hex value:" + s)
655 | }
656 | }
657 | return hexStrings, ids
658 | }
659 |
660 | // parseHexAddresses parses hex numbers from a string and returns them
661 | // in a slice.
662 | func parseHexAddresses(s string) []uint64 {
663 | _, ids := extractHexAddresses(s)
664 | return ids
665 | }
666 |
667 | // scaleHeapSample adjusts the data from a heapz Sample to
668 | // account for its probability of appearing in the collected
669 | // data. heapz profiles are a sampling of the memory allocations
670 | // requests in a program. We estimate the unsampled value by dividing
671 | // each collected sample by its probability of appearing in the
672 | // profile. heapz v2 profiles rely on a poisson process to determine
673 | // which samples to collect, based on the desired average collection
674 | // rate R. The probability of a sample of size S to appear in that
675 | // profile is 1-exp(-S/R).
676 | func scaleHeapSample(count, size, rate int64) (int64, int64) {
677 | if count == 0 || size == 0 {
678 | return 0, 0
679 | }
680 |
681 | if rate <= 1 {
682 | // if rate==1 all samples were collected so no adjustment is needed.
683 | // if rate<1 treat as unknown and skip scaling.
684 | return count, size
685 | }
686 |
687 | avgSize := float64(size) / float64(count)
688 | scale := 1 / (1 - math.Exp(-avgSize/float64(rate)))
689 |
690 | return int64(float64(count) * scale), int64(float64(size) * scale)
691 | }
692 |
693 | // parseContention parses a contentionz profile and returns a newly
694 | // populated Profile.
695 | func parseContention(b []byte) (p *Profile, err error) {
696 | r := bytes.NewBuffer(b)
697 | l, err := r.ReadString('\n')
698 | if err != nil {
699 | return nil, errUnrecognized
700 | }
701 |
702 | if !strings.HasPrefix(l, "--- contention") {
703 | return nil, errUnrecognized
704 | }
705 |
706 | p = &Profile{
707 | PeriodType: &ValueType{Type: "contentions", Unit: "count"},
708 | Period: 1,
709 | SampleType: []*ValueType{
710 | {Type: "contentions", Unit: "count"},
711 | {Type: "delay", Unit: "nanoseconds"},
712 | },
713 | }
714 |
715 | var cpuHz int64
716 | // Parse text of the form "attribute = value" before the samples.
717 | const delimiter = "="
718 | for {
719 | l, err = r.ReadString('\n')
720 | if err != nil {
721 | if err != io.EOF {
722 | return nil, err
723 | }
724 |
725 | if l == "" {
726 | break
727 | }
728 | }
729 |
730 | if l = strings.TrimSpace(l); l == "" {
731 | continue
732 | }
733 |
734 | if strings.HasPrefix(l, "---") {
735 | break
736 | }
737 |
738 | attr := strings.SplitN(l, delimiter, 2)
739 | if len(attr) != 2 {
740 | break
741 | }
742 | key, val := strings.TrimSpace(attr[0]), strings.TrimSpace(attr[1])
743 | var err error
744 | switch key {
745 | case "cycles/second":
746 | if cpuHz, err = strconv.ParseInt(val, 0, 64); err != nil {
747 | return nil, errUnrecognized
748 | }
749 | case "sampling period":
750 | if p.Period, err = strconv.ParseInt(val, 0, 64); err != nil {
751 | return nil, errUnrecognized
752 | }
753 | case "ms since reset":
754 | ms, err := strconv.ParseInt(val, 0, 64)
755 | if err != nil {
756 | return nil, errUnrecognized
757 | }
758 | p.DurationNanos = ms * 1000 * 1000
759 | case "format":
760 | // CPP contentionz profiles don't have format.
761 | return nil, errUnrecognized
762 | case "resolution":
763 | // CPP contentionz profiles don't have resolution.
764 | return nil, errUnrecognized
765 | case "discarded samples":
766 | default:
767 | return nil, errUnrecognized
768 | }
769 | }
770 |
771 | locs := make(map[uint64]*Location)
772 | for {
773 | if l = strings.TrimSpace(l); strings.HasPrefix(l, "---") {
774 | break
775 | }
776 | value, addrs, err := parseContentionSample(l, p.Period, cpuHz)
777 | if err != nil {
778 | return nil, err
779 | }
780 | var sloc []*Location
781 | for i, addr := range addrs {
782 | // Addresses from stack traces point to the next instruction after
783 | // each call. Adjust by -1 to land somewhere on the actual call
784 | // (except for the leaf, which is not a call).
785 | if i > 0 {
786 | addr--
787 | }
788 | loc := locs[addr]
789 | if locs[addr] == nil {
790 | loc = &Location{
791 | Address: addr,
792 | }
793 | p.Location = append(p.Location, loc)
794 | locs[addr] = loc
795 | }
796 | sloc = append(sloc, loc)
797 | }
798 | p.Sample = append(p.Sample, &Sample{
799 | Value: value,
800 | Location: sloc,
801 | })
802 |
803 | if l, err = r.ReadString('\n'); err != nil {
804 | if err != io.EOF {
805 | return nil, err
806 | }
807 | if l == "" {
808 | break
809 | }
810 | }
811 | }
812 |
813 | if err = parseAdditionalSections(l, r, p); err != nil {
814 | return nil, err
815 | }
816 |
817 | return p, nil
818 | }
819 |
820 | // parseContentionSample parses a single row from a contention profile
821 | // into a new Sample.
822 | func parseContentionSample(line string, period, cpuHz int64) (value []int64, addrs []uint64, err error) {
823 | sampleData := contentionSampleRE.FindStringSubmatch(line)
824 | if sampleData == nil {
825 | return value, addrs, errUnrecognized
826 | }
827 |
828 | v1, err := strconv.ParseInt(sampleData[1], 10, 64)
829 | if err != nil {
830 | return value, addrs, fmt.Errorf("malformed sample: %s: %v", line, err)
831 | }
832 | v2, err := strconv.ParseInt(sampleData[2], 10, 64)
833 | if err != nil {
834 | return value, addrs, fmt.Errorf("malformed sample: %s: %v", line, err)
835 | }
836 |
837 | // Unsample values if period and cpuHz are available.
838 | // - Delays are scaled to cycles and then to nanoseconds.
839 | // - Contentions are scaled to cycles.
840 | if period > 0 {
841 | if cpuHz > 0 {
842 | cpuGHz := float64(cpuHz) / 1e9
843 | v1 = int64(float64(v1) * float64(period) / cpuGHz)
844 | }
845 | v2 = v2 * period
846 | }
847 |
848 | value = []int64{v2, v1}
849 | addrs = parseHexAddresses(sampleData[3])
850 |
851 | return value, addrs, nil
852 | }
853 |
854 | // parseThread parses a Threadz profile and returns a new Profile.
855 | func parseThread(b []byte) (*Profile, error) {
856 | r := bytes.NewBuffer(b)
857 |
858 | var line string
859 | var err error
860 | for {
861 | // Skip past comments and empty lines seeking a real header.
862 | line, err = r.ReadString('\n')
863 | if err != nil {
864 | return nil, err
865 | }
866 | if !isSpaceOrComment(line) {
867 | break
868 | }
869 | }
870 |
871 | if m := threadzStartRE.FindStringSubmatch(line); m != nil {
872 | // Advance over initial comments until first stack trace.
873 | for {
874 | line, err = r.ReadString('\n')
875 | if err != nil {
876 | if err != io.EOF {
877 | return nil, err
878 | }
879 |
880 | if line == "" {
881 | break
882 | }
883 | }
884 | if sectionTrigger(line) != unrecognizedSection || line[0] == '-' {
885 | break
886 | }
887 | }
888 | } else if t := threadStartRE.FindStringSubmatch(line); len(t) != 4 {
889 | return nil, errUnrecognized
890 | }
891 |
892 | p := &Profile{
893 | SampleType: []*ValueType{{Type: "thread", Unit: "count"}},
894 | PeriodType: &ValueType{Type: "thread", Unit: "count"},
895 | Period: 1,
896 | }
897 |
898 | locs := make(map[uint64]*Location)
899 | // Recognize each thread and populate profile samples.
900 | for sectionTrigger(line) == unrecognizedSection {
901 | if strings.HasPrefix(line, "---- no stack trace for") {
902 | line = ""
903 | break
904 | }
905 | if t := threadStartRE.FindStringSubmatch(line); len(t) != 4 {
906 | return nil, errUnrecognized
907 | }
908 |
909 | var addrs []uint64
910 | line, addrs, err = parseThreadSample(r)
911 | if err != nil {
912 | return nil, errUnrecognized
913 | }
914 | if len(addrs) == 0 {
915 | // We got a --same as previous threads--. Bump counters.
916 | if len(p.Sample) > 0 {
917 | s := p.Sample[len(p.Sample)-1]
918 | s.Value[0]++
919 | }
920 | continue
921 | }
922 |
923 | var sloc []*Location
924 | for i, addr := range addrs {
925 | // Addresses from stack traces point to the next instruction after
926 | // each call. Adjust by -1 to land somewhere on the actual call
927 | // (except for the leaf, which is not a call).
928 | if i > 0 {
929 | addr--
930 | }
931 | loc := locs[addr]
932 | if locs[addr] == nil {
933 | loc = &Location{
934 | Address: addr,
935 | }
936 | p.Location = append(p.Location, loc)
937 | locs[addr] = loc
938 | }
939 | sloc = append(sloc, loc)
940 | }
941 |
942 | p.Sample = append(p.Sample, &Sample{
943 | Value: []int64{1},
944 | Location: sloc,
945 | })
946 | }
947 |
948 | if err = parseAdditionalSections(line, r, p); err != nil {
949 | return nil, err
950 | }
951 |
952 | return p, nil
953 | }
954 |
955 | // parseThreadSample parses a symbolized or unsymbolized stack trace.
956 | // Returns the first line after the traceback, the sample (or nil if
957 | // it hits a 'same-as-previous' marker) and an error.
958 | func parseThreadSample(b *bytes.Buffer) (nextl string, addrs []uint64, err error) {
959 | var l string
960 | sameAsPrevious := false
961 | for {
962 | if l, err = b.ReadString('\n'); err != nil {
963 | if err != io.EOF {
964 | return "", nil, err
965 | }
966 | if l == "" {
967 | break
968 | }
969 | }
970 | if l = strings.TrimSpace(l); l == "" {
971 | continue
972 | }
973 |
974 | if strings.HasPrefix(l, "---") {
975 | break
976 | }
977 | if strings.Contains(l, "same as previous thread") {
978 | sameAsPrevious = true
979 | continue
980 | }
981 |
982 | addrs = append(addrs, parseHexAddresses(l)...)
983 | }
984 |
985 | if sameAsPrevious {
986 | return l, nil, nil
987 | }
988 | return l, addrs, nil
989 | }
990 |
991 | // parseAdditionalSections parses any additional sections in the
992 | // profile, ignoring any unrecognized sections.
993 | func parseAdditionalSections(l string, b *bytes.Buffer, p *Profile) (err error) {
994 | for {
995 | if sectionTrigger(l) == memoryMapSection {
996 | break
997 | }
998 | // Ignore any unrecognized sections.
999 | if l, err := b.ReadString('\n'); err != nil {
1000 | if err != io.EOF {
1001 | return err
1002 | }
1003 | if l == "" {
1004 | break
1005 | }
1006 | }
1007 | }
1008 | return p.ParseMemoryMap(b)
1009 | }
1010 |
1011 | // ParseMemoryMap parses a memory map in the format of
1012 | // /proc/self/maps, and overrides the mappings in the current profile.
1013 | // It renumbers the samples and locations in the profile correspondingly.
1014 | func (p *Profile) ParseMemoryMap(rd io.Reader) error {
1015 | b := bufio.NewReader(rd)
1016 |
1017 | var attrs []string
1018 | var r *strings.Replacer
1019 | const delimiter = "="
1020 | for {
1021 | l, err := b.ReadString('\n')
1022 | if err != nil {
1023 | if err != io.EOF {
1024 | return err
1025 | }
1026 | if l == "" {
1027 | break
1028 | }
1029 | }
1030 | if l = strings.TrimSpace(l); l == "" {
1031 | continue
1032 | }
1033 |
1034 | if r != nil {
1035 | l = r.Replace(l)
1036 | }
1037 | m, err := parseMappingEntry(l)
1038 | if err != nil {
1039 | if err == errUnrecognized {
1040 | // Recognize assignments of the form: attr=value, and replace
1041 | // $attr with value on subsequent mappings.
1042 | if attr := strings.SplitN(l, delimiter, 2); len(attr) == 2 {
1043 | attrs = append(attrs, "$"+strings.TrimSpace(attr[0]), strings.TrimSpace(attr[1]))
1044 | r = strings.NewReplacer(attrs...)
1045 | }
1046 | // Ignore any unrecognized entries
1047 | continue
1048 | }
1049 | return err
1050 | }
1051 | if m == nil || (m.File == "" && len(p.Mapping) != 0) {
1052 | // In some cases the first entry may include the address range
1053 | // but not the name of the file. It should be followed by
1054 | // another entry with the name.
1055 | continue
1056 | }
1057 | if len(p.Mapping) == 1 && p.Mapping[0].File == "" {
1058 | // Update the name if this is the entry following that empty one.
1059 | p.Mapping[0].File = m.File
1060 | continue
1061 | }
1062 | p.Mapping = append(p.Mapping, m)
1063 | }
1064 | p.remapLocationIDs()
1065 | p.remapFunctionIDs()
1066 | p.remapMappingIDs()
1067 | return nil
1068 | }
1069 |
1070 | func parseMappingEntry(l string) (*Mapping, error) {
1071 | mapping := &Mapping{}
1072 | var err error
1073 | if me := procMapsRE.FindStringSubmatch(l); len(me) == 9 {
1074 | if !strings.Contains(me[3], "x") {
1075 | // Skip non-executable entries.
1076 | return nil, nil
1077 | }
1078 | if mapping.Start, err = strconv.ParseUint(me[1], 16, 64); err != nil {
1079 | return nil, errUnrecognized
1080 | }
1081 | if mapping.Limit, err = strconv.ParseUint(me[2], 16, 64); err != nil {
1082 | return nil, errUnrecognized
1083 | }
1084 | if me[4] != "" {
1085 | if mapping.Offset, err = strconv.ParseUint(me[4], 16, 64); err != nil {
1086 | return nil, errUnrecognized
1087 | }
1088 | }
1089 | mapping.File = me[8]
1090 | return mapping, nil
1091 | }
1092 |
1093 | if me := briefMapsRE.FindStringSubmatch(l); len(me) == 6 {
1094 | if mapping.Start, err = strconv.ParseUint(me[1], 16, 64); err != nil {
1095 | return nil, errUnrecognized
1096 | }
1097 | if mapping.Limit, err = strconv.ParseUint(me[2], 16, 64); err != nil {
1098 | return nil, errUnrecognized
1099 | }
1100 | mapping.File = me[3]
1101 | if me[5] != "" {
1102 | if mapping.Offset, err = strconv.ParseUint(me[5], 16, 64); err != nil {
1103 | return nil, errUnrecognized
1104 | }
1105 | }
1106 | return mapping, nil
1107 | }
1108 |
1109 | return nil, errUnrecognized
1110 | }
1111 |
1112 | type sectionType int
1113 |
1114 | const (
1115 | unrecognizedSection sectionType = iota
1116 | memoryMapSection
1117 | )
1118 |
1119 | var memoryMapTriggers = []string{
1120 | "--- Memory map: ---",
1121 | "MAPPED_LIBRARIES:",
1122 | }
1123 |
1124 | func sectionTrigger(line string) sectionType {
1125 | for _, trigger := range memoryMapTriggers {
1126 | if strings.Contains(line, trigger) {
1127 | return memoryMapSection
1128 | }
1129 | }
1130 | return unrecognizedSection
1131 | }
1132 |
1133 | func (p *Profile) addLegacyFrameInfo() {
1134 | switch {
1135 | case isProfileType(p, heapzSampleTypes) ||
1136 | isProfileType(p, heapzInUseSampleTypes) ||
1137 | isProfileType(p, heapzAllocSampleTypes):
1138 | p.DropFrames, p.KeepFrames = allocRxStr, allocSkipRxStr
1139 | case isProfileType(p, contentionzSampleTypes):
1140 | p.DropFrames, p.KeepFrames = lockRxStr, ""
1141 | default:
1142 | p.DropFrames, p.KeepFrames = cpuProfilerRxStr, ""
1143 | }
1144 | }
1145 |
1146 | var heapzSampleTypes = []string{"allocations", "size"} // early Go pprof profiles
1147 | var heapzInUseSampleTypes = []string{"inuse_objects", "inuse_space"}
1148 | var heapzAllocSampleTypes = []string{"alloc_objects", "alloc_space"}
1149 | var contentionzSampleTypes = []string{"contentions", "delay"}
1150 |
1151 | func isProfileType(p *Profile, t []string) bool {
1152 | st := p.SampleType
1153 | if len(st) != len(t) {
1154 | return false
1155 | }
1156 |
1157 | for i := range st {
1158 | if st[i].Type != t[i] {
1159 | return false
1160 | }
1161 | }
1162 | return true
1163 | }
1164 |
1165 | var allocRxStr = strings.Join([]string{
1166 | // POSIX entry points.
1167 | `calloc`,
1168 | `cfree`,
1169 | `malloc`,
1170 | `free`,
1171 | `memalign`,
1172 | `do_memalign`,
1173 | `(__)?posix_memalign`,
1174 | `pvalloc`,
1175 | `valloc`,
1176 | `realloc`,
1177 |
1178 | // TC malloc.
1179 | `tcmalloc::.*`,
1180 | `tc_calloc`,
1181 | `tc_cfree`,
1182 | `tc_malloc`,
1183 | `tc_free`,
1184 | `tc_memalign`,
1185 | `tc_posix_memalign`,
1186 | `tc_pvalloc`,
1187 | `tc_valloc`,
1188 | `tc_realloc`,
1189 | `tc_new`,
1190 | `tc_delete`,
1191 | `tc_newarray`,
1192 | `tc_deletearray`,
1193 | `tc_new_nothrow`,
1194 | `tc_newarray_nothrow`,
1195 |
1196 | // Memory-allocation routines on OS X.
1197 | `malloc_zone_malloc`,
1198 | `malloc_zone_calloc`,
1199 | `malloc_zone_valloc`,
1200 | `malloc_zone_realloc`,
1201 | `malloc_zone_memalign`,
1202 | `malloc_zone_free`,
1203 |
1204 | // Go runtime
1205 | `runtime\..*`,
1206 |
1207 | // Other misc. memory allocation routines
1208 | `BaseArena::.*`,
1209 | `(::)?do_malloc_no_errno`,
1210 | `(::)?do_malloc_pages`,
1211 | `(::)?do_malloc`,
1212 | `DoSampledAllocation`,
1213 | `MallocedMemBlock::MallocedMemBlock`,
1214 | `_M_allocate`,
1215 | `__builtin_(vec_)?delete`,
1216 | `__builtin_(vec_)?new`,
1217 | `__gnu_cxx::new_allocator::allocate`,
1218 | `__libc_malloc`,
1219 | `__malloc_alloc_template::allocate`,
1220 | `allocate`,
1221 | `cpp_alloc`,
1222 | `operator new(\[\])?`,
1223 | `simple_alloc::allocate`,
1224 | }, `|`)
1225 |
1226 | var allocSkipRxStr = strings.Join([]string{
1227 | // Preserve Go runtime frames that appear in the middle/bottom of
1228 | // the stack.
1229 | `runtime\.panic`,
1230 | }, `|`)
1231 |
1232 | var cpuProfilerRxStr = strings.Join([]string{
1233 | `ProfileData::Add`,
1234 | `ProfileData::prof_handler`,
1235 | `CpuProfiler::prof_handler`,
1236 | `__pthread_sighandler`,
1237 | `__restore`,
1238 | }, `|`)
1239 |
1240 | var lockRxStr = strings.Join([]string{
1241 | `RecordLockProfileData`,
1242 | `(base::)?RecordLockProfileData.*`,
1243 | `(base::)?SubmitMutexProfileData.*`,
1244 | `(base::)?SubmitSpinLockProfileData.*`,
1245 | `(Mutex::)?AwaitCommon.*`,
1246 | `(Mutex::)?Unlock.*`,
1247 | `(Mutex::)?UnlockSlow.*`,
1248 | `(Mutex::)?ReaderUnlock.*`,
1249 | `(MutexLock::)?~MutexLock.*`,
1250 | `(SpinLock::)?Unlock.*`,
1251 | `(SpinLock::)?SlowUnlock.*`,
1252 | `(SpinLockHolder::)?~SpinLockHolder.*`,
1253 | }, `|`)
1254 |
--------------------------------------------------------------------------------