Add go tool pprof import support (#165)
This PR adds support for importing from Google's pprof format, which is a gzipped, protobuf encoded file format (that's incredibly well documented!) The [pprof http library](https://golang.org/pkg/net/http/pprof/) also offers an output of the trace file format, which continues to not be supported in speedscope to date (See #77). This will allow importing of profiles generated by the standard library go profiler for analysis of profiles containing heap allocation information, CPU profile information, and a few other things like coroutine creation information. In order to add support for that a number of dependent bits of functionality were added, which should each provide an easier path for future binary input sources - A protobuf decoding library was included ([protobufjs](https://www.npmjs.com/package/protobufjs)) which includes both a protobuf parser generator based on a .proto file & TypeScript definition generation from the resulting generated JavaScript file - More generic binary file import. Before this PR, all supported sources were plaintext, with the exception of Instruments 10 support, which takes a totally different codepath. Now binary file import should work when files are dropped, opened via file browsing, or opened via invocation of the speedscope CLI. - Transparent gzip decoding of imported files (this means that if you were to gzip compress another JSON file, then importing it should still work fine) Fixes #60. -- This is a [donation motivated](https://github.com/jlfwong/speedscope/issues/60#issuecomment-419660710) PR motivated by donations by @davecheney & @jmoiron to [/dev/color](https://www.devcolor.org/welcome) 🎉
This commit is contained in:
99
package-lock.json
generated
99
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "speedscope",
|
||||
"version": "0.7.0",
|
||||
"version": "1.0.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -24,6 +24,70 @@
|
||||
"js-tokens": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=",
|
||||
"dev": true
|
||||
},
|
||||
"@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"dev": true
|
||||
},
|
||||
"@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"dev": true
|
||||
},
|
||||
"@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=",
|
||||
"dev": true
|
||||
},
|
||||
"@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=",
|
||||
"dev": true
|
||||
},
|
||||
"@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=",
|
||||
"dev": true
|
||||
},
|
||||
"@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=",
|
||||
"dev": true
|
||||
},
|
||||
"@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=",
|
||||
"dev": true
|
||||
},
|
||||
"@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=",
|
||||
"dev": true
|
||||
},
|
||||
"@types/jest": {
|
||||
"version": "22.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-22.2.3.tgz",
|
||||
@@ -39,6 +103,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "10.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.1.4.tgz",
|
||||
@@ -7502,6 +7572,12 @@
|
||||
"integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==",
|
||||
"dev": true
|
||||
},
|
||||
"long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
|
||||
"dev": true
|
||||
},
|
||||
"longest": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
|
||||
@@ -10703,6 +10779,27 @@
|
||||
"integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=",
|
||||
"dev": true
|
||||
},
|
||||
"protobufjs": {
|
||||
"version": "6.8.8",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz",
|
||||
"integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/long": "^4.0.0",
|
||||
"@types/node": "^10.1.0",
|
||||
"long": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"preact": "8.2.7",
|
||||
"preact-redux": "jlfwong/preact-redux#a56dcc4",
|
||||
"prettier": "1.12.0",
|
||||
"protobufjs": "6.8.8",
|
||||
"quicktype": "15.0.45",
|
||||
"redux": "^4.0.0",
|
||||
"ts-jest": "22.4.6",
|
||||
|
||||
BIN
sample/profiles/pprof/simple.prof
Normal file
BIN
sample/profiles/pprof/simple.prof
Normal file
Binary file not shown.
2
sample/programs/go/.gitignore
vendored
Normal file
2
sample/programs/go/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
simple
|
||||
server
|
||||
15
sample/programs/go/Makefile
Normal file
15
sample/programs/go/Makefile
Normal file
@@ -0,0 +1,15 @@
|
||||
.PHONY: all clean linux-perf
|
||||
|
||||
all: simple server
|
||||
|
||||
clean:
|
||||
rm -f simple server
|
||||
|
||||
simple: simple.go
|
||||
go build $<
|
||||
|
||||
server: server.go
|
||||
go build $<
|
||||
|
||||
simple.prof: simple
|
||||
./$< -cpuprofile=$@
|
||||
71
sample/programs/go/server.go
Normal file
71
sample/programs/go/server.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
import _ "net/http/pprof"
|
||||
|
||||
// See https://golang.org/pkg/net/http/pprof/ for details
|
||||
|
||||
func alpha() {
|
||||
z := 3
|
||||
for i := 0; i < 100000; i++ {
|
||||
z *= 3
|
||||
}
|
||||
}
|
||||
|
||||
func beta() {
|
||||
z := 3
|
||||
for i := 0; i < 100000; i++ {
|
||||
z *= 3
|
||||
}
|
||||
}
|
||||
|
||||
func delta() {
|
||||
z := 3
|
||||
for i := 0; i < 100000; i++ {
|
||||
z *= 3
|
||||
}
|
||||
alpha()
|
||||
beta()
|
||||
}
|
||||
|
||||
func gamma() {
|
||||
z := 3
|
||||
for i := 0; i < 100000; i++ {
|
||||
z *= 3
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
// we need a webserver to get the pprof webserver
|
||||
go func() {
|
||||
log.Println(http.ListenAndServe("localhost:6060", nil))
|
||||
}()
|
||||
fmt.Println("hello world")
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go leakyFunction(wg)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func leakyFunction(wg sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
s := make([]string, 3)
|
||||
for i:= 0; i < 10000000; i++{
|
||||
alpha()
|
||||
beta()
|
||||
delta()
|
||||
gamma()
|
||||
s = append(s, "magical pandas")
|
||||
if (i % 100000) == 0 {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
53
sample/programs/go/simple.go
Normal file
53
sample/programs/go/simple.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import "flag"
|
||||
import "runtime/pprof"
|
||||
import "os"
|
||||
|
||||
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
|
||||
func alpha() {
|
||||
z := 3
|
||||
for i := 0; i < 100000; i++ {
|
||||
z *= 3
|
||||
}
|
||||
}
|
||||
|
||||
func beta() {
|
||||
z := 3
|
||||
for i := 0; i < 100000; i++ {
|
||||
z *= 3
|
||||
}
|
||||
}
|
||||
|
||||
func delta() {
|
||||
z := 3
|
||||
for i := 0; i < 100000; i++ {
|
||||
z *= 3
|
||||
}
|
||||
alpha()
|
||||
beta()
|
||||
}
|
||||
|
||||
func gamma() {
|
||||
z := 3
|
||||
for i := 0; i < 100000; i++ {
|
||||
z *= 3
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *cpuprofile != "" {
|
||||
f, _ := os.Create(*cpuprofile)
|
||||
pprof.StartCPUProfile(f)
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
for i := 0; i < 10000; i++ {
|
||||
alpha()
|
||||
beta()
|
||||
delta()
|
||||
gamma()
|
||||
}
|
||||
}
|
||||
249
src/import/__snapshots__/pprof.test.ts.snap
Normal file
249
src/import/__snapshots__/pprof.test.ts.snap
Normal file
@@ -0,0 +1,249 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`importAsPprofProfile 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
|
||||
"key": "runtime.main:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.main",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 136,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/Users/jlfwong/code/speedscope/sample/programs/go/simple.go",
|
||||
"key": "main.main:/Users/jlfwong/code/speedscope/sample/programs/go/simple.go:0",
|
||||
"line": 0,
|
||||
"name": "main.main",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 136,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/Users/jlfwong/code/speedscope/sample/programs/go/simple.go",
|
||||
"key": "main.delta:/Users/jlfwong/code/speedscope/sample/programs/go/simple.go:0",
|
||||
"line": 0,
|
||||
"name": "main.delta",
|
||||
"selfWeight": 22,
|
||||
"totalWeight": 58,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/Users/jlfwong/code/speedscope/sample/programs/go/simple.go",
|
||||
"key": "main.beta:/Users/jlfwong/code/speedscope/sample/programs/go/simple.go:0",
|
||||
"line": 0,
|
||||
"name": "main.beta",
|
||||
"selfWeight": 39,
|
||||
"totalWeight": 39,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/Users/jlfwong/code/speedscope/sample/programs/go/simple.go",
|
||||
"key": "main.alpha:/Users/jlfwong/code/speedscope/sample/programs/go/simple.go:0",
|
||||
"line": 0,
|
||||
"name": "main.alpha",
|
||||
"selfWeight": 48,
|
||||
"totalWeight": 48,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/Users/jlfwong/code/speedscope/sample/programs/go/simple.go",
|
||||
"key": "main.gamma:/Users/jlfwong/code/speedscope/sample/programs/go/simple.go:0",
|
||||
"line": 0,
|
||||
"name": "main.gamma",
|
||||
"selfWeight": 27,
|
||||
"totalWeight": 27,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/asm_amd64.s",
|
||||
"key": "runtime.morestack:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/asm_amd64.s:0",
|
||||
"line": 0,
|
||||
"name": "runtime.morestack",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 11,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/stack.go",
|
||||
"key": "runtime.newstack:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/stack.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.newstack",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 11,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/duff_amd64.s",
|
||||
"key": "runtime.duffcopy:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/duff_amd64.s:0",
|
||||
"line": 0,
|
||||
"name": "runtime.duffcopy",
|
||||
"selfWeight": 11,
|
||||
"totalWeight": 11,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/time.go",
|
||||
"key": "runtime.timerproc:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/time.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.timerproc",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/time.go",
|
||||
"key": "runtime.goroutineReady:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/time.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.goroutineReady",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
|
||||
"key": "runtime.goready:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.goready",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/asm_amd64.s",
|
||||
"key": "runtime.systemstack:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/asm_amd64.s:0",
|
||||
"line": 0,
|
||||
"name": "runtime.systemstack",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
|
||||
"key": "runtime.goready.func1:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.goready.func1",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
|
||||
"key": "runtime.ready:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.ready",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
|
||||
"key": "runtime.wakep:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.wakep",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
|
||||
"key": "runtime.startm:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.startm",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/lock_sema.go",
|
||||
"key": "runtime.notewakeup:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/lock_sema.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.notewakeup",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/os_darwin.go",
|
||||
"key": "runtime.semawakeup:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/os_darwin.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.semawakeup",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/os_darwin.go",
|
||||
"key": "runtime.mach_semrelease:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/os_darwin.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.mach_semrelease",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/sys_darwin_amd64.s",
|
||||
"key": "runtime.mach_semaphore_signal:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/sys_darwin_amd64.s:0",
|
||||
"line": 0,
|
||||
"name": "runtime.mach_semaphore_signal",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
|
||||
"key": "runtime.mstart:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.mstart",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
|
||||
"key": "runtime.mstart1:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.mstart1",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
|
||||
"key": "runtime.sysmon:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
|
||||
"line": 0,
|
||||
"name": "runtime.sysmon",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/sys_darwin_amd64.s",
|
||||
"key": "runtime.usleep:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/sys_darwin_amd64.s:0",
|
||||
"line": 0,
|
||||
"name": "runtime.usleep",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
],
|
||||
"name": "simple.prof",
|
||||
"stacks": Array [
|
||||
"runtime.main;main.main;main.delta;main.beta 14",
|
||||
"runtime.main;main.main;main.alpha 26",
|
||||
"runtime.main;main.main;main.gamma 27",
|
||||
"runtime.morestack;runtime.newstack;runtime.duffcopy 11",
|
||||
"runtime.main;main.main;main.beta 25",
|
||||
"runtime.main;main.main;main.delta 22",
|
||||
"runtime.main;main.main;main.delta;main.alpha 22",
|
||||
"runtime.timerproc;runtime.goroutineReady;runtime.goready;runtime.systemstack;runtime.goready.func1;runtime.ready;runtime.wakep;runtime.startm;runtime.notewakeup;runtime.semawakeup;runtime.mach_semrelease;runtime.mach_semaphore_signal 1",
|
||||
"runtime.mstart;runtime.mstart1;runtime.sysmon;runtime.usleep 2",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importAsPprofProfile: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importAsPprofProfile: profileGroup.name 1`] = `"simple.prof"`;
|
||||
@@ -1,8 +1,8 @@
|
||||
import {importProfileGroup} from '.'
|
||||
import {importProfileGroupFromText} from '.'
|
||||
|
||||
test('importProfileGroup', async () => {
|
||||
// Importing garbage should return null
|
||||
expect(await importProfileGroup('unknown', '')).toBe(null)
|
||||
expect(await importProfileGroup('unknown', 'Hello world')).toBe(null)
|
||||
expect(await importProfileGroup('unknown', 'Hello\n\nWorld')).toBe(null)
|
||||
expect(await importProfileGroupFromText('unknown', '')).toBe(null)
|
||||
expect(await importProfileGroupFromText('unknown', 'Hello world')).toBe(null)
|
||||
expect(await importProfileGroupFromText('unknown', 'Hello\n\nWorld')).toBe(null)
|
||||
})
|
||||
|
||||
@@ -9,12 +9,41 @@ import {importFromFirefox} from './firefox'
|
||||
import {importSpeedscopeProfiles} from '../lib/file-format'
|
||||
import {importFromV8ProfLog} from './v8proflog'
|
||||
import {importFromLinuxPerf} from './linux-tools-perf'
|
||||
import {ProfileDataSource, TextProfileDataSource, MaybeCompressedDataReader} from './utils'
|
||||
import {importAsPprofProfile} from './pprof'
|
||||
import {decodeBase64} from '../lib/utils'
|
||||
|
||||
export async function importProfileGroup(
|
||||
export async function importProfileGroupFromText(
|
||||
fileName: string,
|
||||
contents: string,
|
||||
): Promise<ProfileGroup | null> {
|
||||
const profileGroup = await _importProfileGroup(fileName, contents)
|
||||
return await importProfileGroup(new TextProfileDataSource(fileName, contents))
|
||||
}
|
||||
|
||||
export async function importProfileGroupFromBase64(
|
||||
fileName: string,
|
||||
b64contents: string,
|
||||
): Promise<ProfileGroup | null> {
|
||||
return await importProfileGroup(
|
||||
MaybeCompressedDataReader.fromArrayBuffer(fileName, decodeBase64(b64contents).buffer),
|
||||
)
|
||||
}
|
||||
|
||||
export async function importProfilesFromFile(file: File): Promise<ProfileGroup | null> {
|
||||
return importProfileGroup(MaybeCompressedDataReader.fromFile(file))
|
||||
}
|
||||
|
||||
export async function importProfilesFromArrayBuffer(
|
||||
fileName: string,
|
||||
buffer: ArrayBuffer,
|
||||
): Promise<ProfileGroup | null> {
|
||||
return importProfileGroup(MaybeCompressedDataReader.fromArrayBuffer(fileName, buffer))
|
||||
}
|
||||
|
||||
async function importProfileGroup(dataSource: ProfileDataSource): Promise<ProfileGroup | null> {
|
||||
const fileName = await dataSource.name()
|
||||
|
||||
const profileGroup = await _importProfileGroup(dataSource)
|
||||
if (profileGroup) {
|
||||
if (!profileGroup.name) {
|
||||
profileGroup.name = fileName
|
||||
@@ -34,10 +63,21 @@ function toGroup(profile: Profile | null): ProfileGroup | null {
|
||||
return {name: profile.getName(), indexToView: 0, profiles: [profile]}
|
||||
}
|
||||
|
||||
async function _importProfileGroup(
|
||||
fileName: string,
|
||||
contents: string,
|
||||
): Promise<ProfileGroup | null> {
|
||||
async function _importProfileGroup(dataSource: ProfileDataSource): Promise<ProfileGroup | null> {
|
||||
const fileName = await dataSource.name()
|
||||
|
||||
const buffer = await dataSource.readAsArrayBuffer()
|
||||
|
||||
{
|
||||
const profile = importAsPprofProfile(buffer)
|
||||
if (profile) {
|
||||
console.log('Importing as protobuf encoded pprof file')
|
||||
return toGroup(profile)
|
||||
}
|
||||
}
|
||||
|
||||
const contents = await dataSource.readAsText()
|
||||
|
||||
// First pass: Check known file format names to infer the file type
|
||||
if (fileName.endsWith('.speedscope.json')) {
|
||||
console.log('Importing as speedscope json file')
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
ProfileGroup,
|
||||
} from '../lib/profile'
|
||||
import {sortBy, getOrThrow, getOrInsert, lastOf, getOrElse, zeroPad} from '../lib/utils'
|
||||
import * as pako from 'pako'
|
||||
import {ByteFormatter, TimeFormatter} from '../lib/value-formatters'
|
||||
import {FileSystemDirectoryEntry, FileSystemEntry, FileSystemFileEntry} from './file-system-entry'
|
||||
import {MaybeCompressedDataReader} from './utils'
|
||||
|
||||
function parseTSV<T>(contents: string): T[] {
|
||||
const lines = contents.split('\n').map(l => l.split('\t'))
|
||||
@@ -183,56 +183,12 @@ async function extractDirectoryTree(entry: FileSystemDirectoryEntry): Promise<Tr
|
||||
return node
|
||||
}
|
||||
|
||||
class MaybeCompressedFileReader {
|
||||
private fileData: Promise<ArrayBuffer>
|
||||
|
||||
constructor(file: File) {
|
||||
this.fileData = new Promise(resolve => {
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('loadend', () => {
|
||||
if (!(reader.result instanceof ArrayBuffer)) {
|
||||
throw new Error('Expected reader.result to be an instance of ArrayBuffer')
|
||||
}
|
||||
resolve(reader.result)
|
||||
})
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
|
||||
private async getUncompressed(): Promise<ArrayBuffer> {
|
||||
const fileData = await this.fileData
|
||||
try {
|
||||
const result = pako.inflate(new Uint8Array(fileData)).buffer
|
||||
return result
|
||||
} catch (e) {
|
||||
return fileData
|
||||
}
|
||||
}
|
||||
|
||||
async readAsArrayBuffer(): Promise<ArrayBuffer> {
|
||||
return await this.getUncompressed()
|
||||
}
|
||||
|
||||
async readAsText(): Promise<string> {
|
||||
const buffer = await this.getUncompressed()
|
||||
let ret: string = ''
|
||||
|
||||
// JavaScript strings are UTF-16 encoded, but this data is coming
|
||||
// from a UTF-8 encoded file.
|
||||
const array = new Uint8Array(buffer)
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
ret += String.fromCharCode(array[i])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
function readAsArrayBuffer(file: File): Promise<ArrayBuffer> {
|
||||
return new MaybeCompressedFileReader(file).readAsArrayBuffer()
|
||||
return MaybeCompressedDataReader.fromFile(file).readAsArrayBuffer()
|
||||
}
|
||||
|
||||
function readAsText(file: File): Promise<string> {
|
||||
return new MaybeCompressedFileReader(file).readAsText()
|
||||
return MaybeCompressedDataReader.fromFile(file).readAsText()
|
||||
}
|
||||
|
||||
function getCoreDirForRun(tree: TraceDirectoryTree, selectedRun: number): TraceDirectoryTree {
|
||||
|
||||
5
src/import/pprof.test.ts
Normal file
5
src/import/pprof.test.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
test('importAsPprofProfile', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/pprof/simple.prof')
|
||||
})
|
||||
134
src/import/pprof.ts
Normal file
134
src/import/pprof.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import {perftools} from './profile.proto.js'
|
||||
import {FrameInfo, StackListProfileBuilder, Profile} from '../lib/profile'
|
||||
import {lastOf} from '../lib/utils'
|
||||
import {TimeFormatter, ByteFormatter} from '../lib/value-formatters'
|
||||
|
||||
interface SampleType {
|
||||
type: string
|
||||
unit: string
|
||||
}
|
||||
|
||||
export function importAsPprofProfile(rawProfile: ArrayBuffer): Profile | null {
|
||||
if (rawProfile.byteLength === 0) return null
|
||||
|
||||
let protoProfile: perftools.profiles.Profile
|
||||
try {
|
||||
protoProfile = perftools.profiles.Profile.decode(new Uint8Array(rawProfile))
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
|
||||
function i32(n: number | Long): number {
|
||||
return typeof n === 'number' ? n : (n as Long).low
|
||||
}
|
||||
|
||||
function stringVal(key: number | Long): string | null {
|
||||
return protoProfile.stringTable[i32(key)] || null
|
||||
}
|
||||
|
||||
const frameInfoByFunctionID = new Map<number, FrameInfo>()
|
||||
|
||||
function frameInfoForFunction(f: perftools.profiles.IFunction): FrameInfo | null {
|
||||
const {name, filename, startLine} = f
|
||||
|
||||
const nameString = (name != null && stringVal(name)) || '(unknown)'
|
||||
const fileNameString = filename != null ? stringVal(filename) : null
|
||||
const line = startLine != null ? +startLine : null
|
||||
|
||||
const key = `${nameString}:${fileNameString}:${line}`
|
||||
|
||||
const frameInfo: FrameInfo = {
|
||||
key,
|
||||
name: nameString,
|
||||
}
|
||||
|
||||
if (fileNameString != null) {
|
||||
frameInfo.file = fileNameString
|
||||
}
|
||||
|
||||
if (line != null) {
|
||||
frameInfo.line = line
|
||||
}
|
||||
|
||||
return frameInfo
|
||||
}
|
||||
|
||||
for (let f of protoProfile.function) {
|
||||
if (f.id) {
|
||||
const frameInfo = frameInfoForFunction(f)
|
||||
if (frameInfo != null) {
|
||||
frameInfoByFunctionID.set(i32(f.id), frameInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function frameInfoForLocation(location: perftools.profiles.ILocation): FrameInfo | null {
|
||||
const {line} = location
|
||||
if (line == null) return null
|
||||
|
||||
// From a comment on profile.proto:
|
||||
//
|
||||
// Multiple line indicates this location has inlined functions,
|
||||
// where the last entry represents the caller into which the
|
||||
// preceding entries were inlined.
|
||||
//
|
||||
// E.g., if memcpy() is inlined into printf:
|
||||
// line[0].function_name == "memcpy"
|
||||
// line[1].function_name == "printf"
|
||||
//
|
||||
// Let's just take the last line then
|
||||
const lastLine = lastOf(line)
|
||||
if (lastLine == null) return null
|
||||
|
||||
if (lastLine.functionId) {
|
||||
return frameInfoByFunctionID.get(i32(lastLine.functionId)) || null
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const frameByLocationID = new Map<number, FrameInfo>()
|
||||
|
||||
for (let l of protoProfile.location) {
|
||||
if (l.id != null) {
|
||||
const frameInfo = frameInfoForLocation(l)
|
||||
if (frameInfo) {
|
||||
frameByLocationID.set(i32(l.id), frameInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sampleTypes: SampleType[] = protoProfile.sampleType.map(type => ({
|
||||
type: (type.type && stringVal(type.type)) || 'samples',
|
||||
unit: (type.unit && stringVal(type.unit)) || 'count',
|
||||
}))
|
||||
|
||||
const sampleTypeIndex = protoProfile.defaultSampleType
|
||||
? +protoProfile.defaultSampleType
|
||||
: sampleTypes.length - 1
|
||||
const sampleType = sampleTypes[sampleTypeIndex]
|
||||
|
||||
const profileBuilder = new StackListProfileBuilder()
|
||||
|
||||
switch (sampleType.unit) {
|
||||
case 'nanoseconds':
|
||||
case 'microseconds':
|
||||
case 'milliseconds':
|
||||
case 'seconds':
|
||||
profileBuilder.setValueFormatter(new TimeFormatter(sampleType.unit))
|
||||
break
|
||||
|
||||
case 'bytes':
|
||||
profileBuilder.setValueFormatter(new ByteFormatter())
|
||||
break
|
||||
}
|
||||
|
||||
for (let s of protoProfile.sample) {
|
||||
const stack = s.locationId ? s.locationId.map(l => frameByLocationID.get(i32(l))) : []
|
||||
stack.reverse()
|
||||
const value = s.value![sampleTypeIndex]
|
||||
profileBuilder.appendSampleWithWeight(stack.filter(f => f != null) as FrameInfo[], +value)
|
||||
}
|
||||
|
||||
return profileBuilder.build()
|
||||
}
|
||||
206
src/import/profile.proto
Normal file
206
src/import/profile.proto
Normal file
@@ -0,0 +1,206 @@
|
||||
// THIS FILE WAS IMPORTED FROM AN EXTERNAL SOURCE. DO NOT MODIFY THIS FILE
|
||||
// MANUALLY.
|
||||
//
|
||||
// Original from: https://github.com/google/pprof/blob/e027b50/proto/profile.proto
|
||||
//
|
||||
// This file is licensed under the Apache License 2.0
|
||||
// (https://github.com/google/pprof/blob/e027b5/LICENSE)
|
||||
|
||||
// Profile is a common stacktrace profile format.
|
||||
//
|
||||
// Measurements represented with this format should follow the
|
||||
// following conventions:
|
||||
//
|
||||
// - Consumers should treat unset optional fields as if they had been
|
||||
// set with their default value.
|
||||
//
|
||||
// - When possible, measurements should be stored in "unsampled" form
|
||||
// that is most useful to humans. There should be enough
|
||||
// information present to determine the original sampled values.
|
||||
//
|
||||
// - On-disk, the serialized proto must be gzip-compressed.
|
||||
//
|
||||
// - The profile is represented as a set of samples, where each sample
|
||||
// references a sequence of locations, and where each location belongs
|
||||
// to a mapping.
|
||||
// - There is a N->1 relationship from sample.location_id entries to
|
||||
// locations. For every sample.location_id entry there must be a
|
||||
// unique Location with that id.
|
||||
// - There is an optional N->1 relationship from locations to
|
||||
// mappings. For every nonzero Location.mapping_id there must be a
|
||||
// unique Mapping with that id.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package perftools.profiles;
|
||||
|
||||
option java_package = "com.google.perftools.profiles";
|
||||
option java_outer_classname = "ProfileProto";
|
||||
|
||||
message Profile {
|
||||
// A description of the samples associated with each Sample.value.
|
||||
// For a cpu profile this might be:
|
||||
// [["cpu","nanoseconds"]] or [["wall","seconds"]] or [["syscall","count"]]
|
||||
// For a heap profile, this might be:
|
||||
// [["allocations","count"], ["space","bytes"]],
|
||||
// If one of the values represents the number of events represented
|
||||
// by the sample, by convention it should be at index 0 and use
|
||||
// sample_type.unit == "count".
|
||||
repeated ValueType sample_type = 1;
|
||||
// The set of samples recorded in this profile.
|
||||
repeated Sample sample = 2;
|
||||
// Mapping from address ranges to the image/binary/library mapped
|
||||
// into that address range. mapping[0] will be the main binary.
|
||||
repeated Mapping mapping = 3;
|
||||
// Useful program location
|
||||
repeated Location location = 4;
|
||||
// Functions referenced by locations
|
||||
repeated Function function = 5;
|
||||
// A common table for strings referenced by various messages.
|
||||
// string_table[0] must always be "".
|
||||
repeated string string_table = 6;
|
||||
// frames with Function.function_name fully matching the following
|
||||
// regexp will be dropped from the samples, along with their successors.
|
||||
int64 drop_frames = 7; // Index into string table.
|
||||
// frames with Function.function_name fully matching the following
|
||||
// regexp will be kept, even if it matches drop_functions.
|
||||
int64 keep_frames = 8; // Index into string table.
|
||||
|
||||
// The following fields are informational, do not affect
|
||||
// interpretation of results.
|
||||
|
||||
// Time of collection (UTC) represented as nanoseconds past the epoch.
|
||||
int64 time_nanos = 9;
|
||||
// Duration of the profile, if a duration makes sense.
|
||||
int64 duration_nanos = 10;
|
||||
// The kind of events between sampled ocurrences.
|
||||
// e.g [ "cpu","cycles" ] or [ "heap","bytes" ]
|
||||
ValueType period_type = 11;
|
||||
// The number of events between sampled occurrences.
|
||||
int64 period = 12;
|
||||
// Freeform text associated to the profile.
|
||||
repeated int64 comment = 13; // Indices into string table.
|
||||
// Index into the string table of the type of the preferred sample
|
||||
// value. If unset, clients should default to the last sample value.
|
||||
int64 default_sample_type = 14;
|
||||
}
|
||||
|
||||
// ValueType describes the semantics and measurement units of a value.
|
||||
message ValueType {
|
||||
int64 type = 1; // Index into string table.
|
||||
int64 unit = 2; // Index into string table.
|
||||
}
|
||||
|
||||
// Each Sample records values encountered in some program
|
||||
// context. The program context is typically a stack trace, perhaps
|
||||
// augmented with auxiliary information like the thread-id, some
|
||||
// indicator of a higher level request being handled etc.
|
||||
message Sample {
|
||||
// The ids recorded here correspond to a Profile.location.id.
|
||||
// The leaf is at location_id[0].
|
||||
repeated uint64 location_id = 1;
|
||||
// The type and unit of each value is defined by the corresponding
|
||||
// entry in Profile.sample_type. All samples must have the same
|
||||
// number of values, the same as the length of Profile.sample_type.
|
||||
// When aggregating multiple samples into a single sample, the
|
||||
// result has a list of values that is the elemntwise sum of the
|
||||
// lists of the originals.
|
||||
repeated int64 value = 2;
|
||||
// label includes additional context for this sample. It can include
|
||||
// things like a thread id, allocation size, etc
|
||||
repeated Label label = 3;
|
||||
}
|
||||
|
||||
message Label {
|
||||
int64 key = 1; // Index into string table
|
||||
|
||||
// At most one of the following must be present
|
||||
int64 str = 2; // Index into string table
|
||||
int64 num = 3;
|
||||
|
||||
// Should only be present when num is present.
|
||||
// Specifies the units of num.
|
||||
// Use arbitrary string (for example, "requests") as a custom count unit.
|
||||
// If no unit is specified, consumer may apply heuristic to deduce the unit.
|
||||
// Consumers may also interpret units like "bytes" and "kilobytes" as memory
|
||||
// units and units like "seconds" and "nanoseconds" as time units,
|
||||
// and apply appropriate unit conversions to these.
|
||||
int64 num_unit = 4; // Index into string table
|
||||
}
|
||||
|
||||
message Mapping {
|
||||
// Unique nonzero id for the mapping.
|
||||
uint64 id = 1;
|
||||
// Address at which the binary (or DLL) is loaded into memory.
|
||||
uint64 memory_start = 2;
|
||||
// The limit of the address range occupied by this mapping.
|
||||
uint64 memory_limit = 3;
|
||||
// Offset in the binary that corresponds to the first mapped address.
|
||||
uint64 file_offset = 4;
|
||||
// The object this entry is loaded from. This can be a filename on
|
||||
// disk for the main binary and shared libraries, or virtual
|
||||
// abstractions like "[vdso]".
|
||||
int64 filename = 5; // Index into string table
|
||||
// A string that uniquely identifies a particular program version
|
||||
// with high probability. E.g., for binaries generated by GNU tools,
|
||||
// it could be the contents of the .note.gnu.build-id field.
|
||||
int64 build_id = 6; // Index into string table
|
||||
|
||||
// The following fields indicate the resolution of symbolic info.
|
||||
bool has_functions = 7;
|
||||
bool has_filenames = 8;
|
||||
bool has_line_numbers = 9;
|
||||
bool has_inline_frames = 10;
|
||||
}
|
||||
|
||||
// Describes function and line table debug information.
|
||||
message Location {
|
||||
// Unique nonzero id for the location. A profile could use
|
||||
// instruction addresses or any integer sequence as ids.
|
||||
uint64 id = 1;
|
||||
// The id of the corresponding profile.Mapping for this location.
|
||||
// It can be unset if the mapping is unknown or not applicable for
|
||||
// this profile type.
|
||||
uint64 mapping_id = 2;
|
||||
// The instruction address for this location, if available. It
|
||||
// should be within [Mapping.memory_start...Mapping.memory_limit]
|
||||
// for the corresponding mapping. A non-leaf address may be in the
|
||||
// middle of a call instruction. It is up to display tools to find
|
||||
// the beginning of the instruction if necessary.
|
||||
uint64 address = 3;
|
||||
// Multiple line indicates this location has inlined functions,
|
||||
// where the last entry represents the caller into which the
|
||||
// preceding entries were inlined.
|
||||
//
|
||||
// E.g., if memcpy() is inlined into printf:
|
||||
// line[0].function_name == "memcpy"
|
||||
// line[1].function_name == "printf"
|
||||
repeated Line line = 4;
|
||||
// Provides an indication that multiple symbols map to this location's
|
||||
// address, for example due to identical code folding by the linker. In that
|
||||
// case the line information above represents one of the multiple
|
||||
// symbols. This field must be recomputed when the symbolization state of the
|
||||
// profile changes.
|
||||
bool is_folded = 5;
|
||||
}
|
||||
|
||||
message Line {
|
||||
// The id of the corresponding profile.Function for this line.
|
||||
uint64 function_id = 1;
|
||||
// Line number in source code.
|
||||
int64 line = 2;
|
||||
}
|
||||
|
||||
message Function {
|
||||
// Unique nonzero id for the function.
|
||||
uint64 id = 1;
|
||||
// Name of the function, in human-readable form if available.
|
||||
int64 name = 2; // Index into string table
|
||||
// Name of the function, as identified by the system.
|
||||
// For instance, it can be a C++ mangled name.
|
||||
int64 system_name = 3; // Index into string table
|
||||
// Source file containing the function.
|
||||
int64 filename = 4; // Index into string table
|
||||
// Line number in source file.
|
||||
int64 start_line = 5;
|
||||
}
|
||||
1047
src/import/profile.proto.d.ts
vendored
Normal file
1047
src/import/profile.proto.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3543
src/import/profile.proto.js
Normal file
3543
src/import/profile.proto.js
Normal file
File diff suppressed because it is too large
Load Diff
83
src/import/utils.ts
Normal file
83
src/import/utils.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import * as pako from 'pako'
|
||||
|
||||
export interface ProfileDataSource {
|
||||
name(): Promise<string>
|
||||
readAsArrayBuffer(): Promise<ArrayBuffer>
|
||||
readAsText(): Promise<string>
|
||||
}
|
||||
|
||||
export class TextProfileDataSource implements ProfileDataSource {
|
||||
constructor(private fileName: string, private contents: string) {}
|
||||
async name() {
|
||||
return this.fileName
|
||||
}
|
||||
async readAsArrayBuffer() {
|
||||
// JavaScript strings are UTF-16 encoded, but if this string is
|
||||
// constructed based on
|
||||
|
||||
// TODO(jlfwong): Might want to make this construct an array
|
||||
// buffer based on the text
|
||||
return new ArrayBuffer(0)
|
||||
}
|
||||
async readAsText() {
|
||||
return this.contents
|
||||
}
|
||||
}
|
||||
|
||||
export class MaybeCompressedDataReader implements ProfileDataSource {
|
||||
private uncompressedData: Promise<ArrayBuffer>
|
||||
|
||||
constructor(
|
||||
private namePromise: Promise<string>,
|
||||
maybeCompressedDataPromise: Promise<ArrayBuffer>,
|
||||
) {
|
||||
this.uncompressedData = maybeCompressedDataPromise.then(async (fileData: ArrayBuffer) => {
|
||||
try {
|
||||
const result = pako.inflate(new Uint8Array(fileData)).buffer
|
||||
return result
|
||||
} catch (e) {
|
||||
return fileData
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async name(): Promise<string> {
|
||||
return await this.namePromise
|
||||
}
|
||||
|
||||
async readAsArrayBuffer(): Promise<ArrayBuffer> {
|
||||
return await this.uncompressedData
|
||||
}
|
||||
|
||||
async readAsText(): Promise<string> {
|
||||
const buffer = await this.readAsArrayBuffer()
|
||||
let ret: string = ''
|
||||
|
||||
// JavaScript strings are UTF-16 encoded, but we're reading data
|
||||
// from disk that we're going to asusme is UTF-8 encoded.
|
||||
const array = new Uint8Array(buffer)
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
ret += String.fromCharCode(array[i])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
static fromFile(file: File): MaybeCompressedDataReader {
|
||||
const maybeCompressedDataPromise: Promise<ArrayBuffer> = new Promise(resolve => {
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('loadend', () => {
|
||||
if (!(reader.result instanceof ArrayBuffer)) {
|
||||
throw new Error('Expected reader.result to be an instance of ArrayBuffer')
|
||||
}
|
||||
resolve(reader.result)
|
||||
})
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
|
||||
return new MaybeCompressedDataReader(Promise.resolve(file.name), maybeCompressedDataPromise)
|
||||
}
|
||||
|
||||
static fromArrayBuffer(name: string, buffer: ArrayBuffer): MaybeCompressedDataReader {
|
||||
return new MaybeCompressedDataReader(Promise.resolve(name), Promise.resolve(buffer))
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import {Profile, CallTreeNode, Frame} from './profile'
|
||||
import {importProfileGroup} from '../import'
|
||||
import {exportProfileGroup, importSpeedscopeProfiles} from './file-format'
|
||||
import {importProfilesFromArrayBuffer} from '../import'
|
||||
|
||||
interface DumpedProfile {
|
||||
name: string
|
||||
@@ -47,9 +47,10 @@ export function dumpProfile(profile: Profile): any {
|
||||
}
|
||||
|
||||
export async function checkProfileSnapshot(filepath: string) {
|
||||
const input = fs.readFileSync(filepath, 'utf8')
|
||||
const buffer = fs.readFileSync(filepath)
|
||||
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
|
||||
|
||||
const profileGroup = await importProfileGroup(path.basename(filepath), input)
|
||||
const profileGroup = await importProfilesFromArrayBuffer(path.basename(filepath), arrayBuffer)
|
||||
if (profileGroup) {
|
||||
expect(profileGroup.name).toMatchSnapshot('profileGroup.name')
|
||||
expect(profileGroup.indexToView).toMatchSnapshot('indexToView')
|
||||
@@ -62,7 +63,7 @@ export async function checkProfileSnapshot(filepath: string) {
|
||||
return
|
||||
}
|
||||
|
||||
const profilesWithoutFilename = await importProfileGroup('unknown', input)
|
||||
const profilesWithoutFilename = await importProfilesFromArrayBuffer('unknown', arrayBuffer)
|
||||
if (profilesWithoutFilename) {
|
||||
expect(profilesWithoutFilename.profiles.length).toEqual(profileGroup.profiles.length)
|
||||
profilesWithoutFilename.name = profileGroup.name
|
||||
|
||||
@@ -13,8 +13,11 @@ import {
|
||||
memoizeByReference,
|
||||
memoizeByShallowEquality,
|
||||
objectsHaveShallowEquality,
|
||||
decodeBase64,
|
||||
} from './utils'
|
||||
|
||||
import * as jsc from 'jsverify'
|
||||
|
||||
test('sortBy', () => {
|
||||
const ls = ['a3', 'b2', 'c1', 'd4']
|
||||
sortBy(ls, k => k.charCodeAt(1))
|
||||
@@ -173,3 +176,19 @@ test('objectsHaveShallowEquality', () => {
|
||||
expect(objectsHaveShallowEquality([1], [1, 2])).toBe(false)
|
||||
expect(objectsHaveShallowEquality([1, 2], [1])).toBe(false)
|
||||
})
|
||||
|
||||
test('decodeBase64', () => {
|
||||
jsc.assertForall(jsc.array(jsc.uint8), byteArray => {
|
||||
let binaryString = ''
|
||||
for (let byte of byteArray) {
|
||||
binaryString += String.fromCharCode(byte)
|
||||
}
|
||||
const b64string = btoa(binaryString)
|
||||
const decoded = decodeBase64(b64string)
|
||||
|
||||
expect(Uint8Array.from(byteArray)).toEqual(decoded)
|
||||
|
||||
// If the above expect(...) assertion fails, we won't reach here.
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
128
src/lib/utils.ts
128
src/lib/utils.ts
@@ -167,3 +167,131 @@ export function memoizeByReference<T, U>(cb: (t: T) => U): (t: T) => U {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function lazyStatic<T>(cb: () => T): () => T {
|
||||
let last: {result: T} | null = null
|
||||
return () => {
|
||||
if (last == null) {
|
||||
last = {result: cb()}
|
||||
}
|
||||
return last.result
|
||||
}
|
||||
}
|
||||
|
||||
const base64lookupTable = lazyStatic((): Map<string, number> => {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
const ret = new Map<string, number>()
|
||||
for (let i = 0; i < alphabet.length; i++) {
|
||||
ret.set(alphabet.charAt(i), i)
|
||||
}
|
||||
ret.set('=', -1)
|
||||
return ret
|
||||
})
|
||||
|
||||
// NOTE: There are probably simpler solutions to this problem, but I have this written already, so
|
||||
// until we run into problems with this, let's just use this.
|
||||
//
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem#The_Unicode_Problem
|
||||
export function decodeBase64(encoded: string): Uint8Array {
|
||||
// Reference: https://www.rfc-editor.org/rfc/rfc4648.txt
|
||||
|
||||
const lookupTable = base64lookupTable()
|
||||
|
||||
// 3 byte groups are represented as sequneces of 4 characters.
|
||||
//
|
||||
// "The encoding process represents 24-bit groups of input bits as output
|
||||
// strings of 4 encoded characters."
|
||||
//
|
||||
// "Special processing is performed if fewer than 24 bits are available
|
||||
// at the end of the data being encoded. A full encoding quantum is
|
||||
// always completed at the end of a quantity. When fewer than 24 input
|
||||
// bits are available in an input group bits with value zero are added
|
||||
// (on the right) to form an integral number of 6-bit groups."
|
||||
|
||||
if (encoded.length % 4 !== 0) {
|
||||
throw new Error(
|
||||
`Invalid length for base64 encoded string. Expected length % 4 = 0, got length = ${
|
||||
encoded.length
|
||||
}`,
|
||||
)
|
||||
}
|
||||
|
||||
const quartetCount = encoded.length / 4
|
||||
let byteCount: number
|
||||
|
||||
// Special processing is performed if fewer than 24 bits are available
|
||||
// at the end of the data being encoded. A full encoding quantum is
|
||||
// always completed at the end of a quantity. When fewer than 24 input
|
||||
// bits are available in an input group, bits with value zero are added
|
||||
// (on the right) to form an integral number of 6-bit groups. Padding
|
||||
// at the end of the data is performed using the '=' character. Since
|
||||
// all base 64 input is an integral number of octets, only the following
|
||||
// cases can arise:
|
||||
//
|
||||
// (1) The final quantum of encoding input is an integral multiple of 24
|
||||
// bits; here, the final unit of encoded output will be an integral
|
||||
// multiple of 4 characters with no "=" padding.
|
||||
//
|
||||
// (2) The final quantum of encoding input is exactly 8 bits; here, the
|
||||
// final unit of encoded output will be two characters followed by
|
||||
// two "=" padding characters.
|
||||
//
|
||||
// (3) The final quantum of encoding input is exactly 16 bits; here, the
|
||||
// final unit of encoded output will be three characters followed by
|
||||
// one "=" padding character.
|
||||
if (encoded.length >= 4) {
|
||||
if (encoded.charAt(encoded.length - 1) === '=') {
|
||||
if (encoded.charAt(encoded.length - 2) === '=') {
|
||||
// Case (2)
|
||||
byteCount = quartetCount * 3 - 2
|
||||
} else {
|
||||
// Case (3)
|
||||
byteCount = quartetCount * 3 - 1
|
||||
}
|
||||
} else {
|
||||
// Case (1)
|
||||
byteCount = quartetCount * 3
|
||||
}
|
||||
} else {
|
||||
// Case (1)
|
||||
byteCount = quartetCount * 3
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(byteCount)
|
||||
let offset = 0
|
||||
|
||||
for (let i = 0; i < quartetCount; i++) {
|
||||
const enc1 = encoded.charAt(i * 4 + 0)
|
||||
const enc2 = encoded.charAt(i * 4 + 1)
|
||||
const enc3 = encoded.charAt(i * 4 + 2)
|
||||
const enc4 = encoded.charAt(i * 4 + 3)
|
||||
|
||||
const sextet1 = lookupTable.get(enc1)
|
||||
const sextet2 = lookupTable.get(enc2)
|
||||
const sextet3 = lookupTable.get(enc3)
|
||||
const sextet4 = lookupTable.get(enc4)
|
||||
|
||||
if (sextet1 == null || sextet2 == null || sextet3 == null || sextet4 == null) {
|
||||
throw new Error(
|
||||
`Invalid quartet at indices ${i * 4} .. ${i * 4 + 3}: ${encoded.substring(
|
||||
i * 4,
|
||||
i * 4 + 3,
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
|
||||
bytes[offset++] = (sextet1 << 2) | (sextet2 >> 4)
|
||||
if (enc3 !== '=') {
|
||||
bytes[offset++] = ((sextet2 & 15) << 4) | (sextet3 >> 2)
|
||||
}
|
||||
if (enc4 !== '=') {
|
||||
bytes[offset++] = ((sextet3 & 7) << 6) | sextet4
|
||||
}
|
||||
}
|
||||
|
||||
if (offset !== byteCount) {
|
||||
throw new Error(`Expected to decode ${byteCount} bytes, but only decoded ${offset})`)
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
@@ -18,8 +18,23 @@ import {Graphics} from '../gl/graphics'
|
||||
const importModule = import('../import')
|
||||
// Force eager loading of the module
|
||||
importModule.then(() => {})
|
||||
async function importProfiles(fileName: string, contents: string): Promise<ProfileGroup | null> {
|
||||
return (await importModule).importProfileGroup(fileName, contents)
|
||||
|
||||
async function importProfilesFromText(
|
||||
fileName: string,
|
||||
contents: string,
|
||||
): Promise<ProfileGroup | null> {
|
||||
return (await importModule).importProfileGroupFromText(fileName, contents)
|
||||
}
|
||||
|
||||
async function importProfilesFromBase64(
|
||||
fileName: string,
|
||||
contents: string,
|
||||
): Promise<ProfileGroup | null> {
|
||||
return (await importModule).importProfileGroupFromBase64(fileName, contents)
|
||||
}
|
||||
|
||||
async function importProfilesFromFile(file: File): Promise<ProfileGroup | null> {
|
||||
return (await importModule).importProfilesFromFile(file)
|
||||
}
|
||||
async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntry) {
|
||||
return (await importModule).importFromFileSystemDirectoryEntry(entry)
|
||||
@@ -320,16 +335,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
|
||||
loadFromFile(file: File) {
|
||||
this.loadProfile(async () => {
|
||||
const reader = new FileReader()
|
||||
const loadPromise = new Promise(resolve => reader.addEventListener('loadend', resolve))
|
||||
reader.readAsText(file)
|
||||
await loadPromise
|
||||
|
||||
if (typeof reader.result !== 'string') {
|
||||
throw new Error('Expected ArrayBuffer')
|
||||
}
|
||||
|
||||
const profiles = await importProfiles(file.name, reader.result)
|
||||
const profiles = await importProfilesFromFile(file)
|
||||
if (profiles) {
|
||||
for (let profile of profiles.profiles) {
|
||||
if (!profile.getName()) {
|
||||
@@ -344,7 +350,19 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
// a symbol map. If that's the case, we want to parse it, and apply the symbol
|
||||
// mapping to the already loaded profile. This can be use to take an opaque
|
||||
// profile and make it readable.
|
||||
const map = importEmscriptenSymbolMap(reader.result)
|
||||
const reader = new FileReader()
|
||||
const fileContentsPromise = new Promise<string>(resolve => {
|
||||
reader.addEventListener('loadend', () => {
|
||||
if (typeof reader.result !== 'string') {
|
||||
throw new Error('Expected reader.result to be a string')
|
||||
}
|
||||
resolve(reader.result)
|
||||
})
|
||||
})
|
||||
reader.readAsText(file)
|
||||
const fileContents = await fileContentsPromise
|
||||
|
||||
const map = importEmscriptenSymbolMap(fileContents)
|
||||
if (map) {
|
||||
const {profile, index} = this.props.activeProfileState
|
||||
console.log('Importing as emscripten symbol map')
|
||||
@@ -365,7 +383,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
this.loadProfile(async () => {
|
||||
const filename = 'perf-vertx-stacks-01-collapsed-all.txt'
|
||||
const data = await fetch(exampleProfileURL).then(resp => resp.text())
|
||||
return await importProfiles(filename, data)
|
||||
return await importProfilesFromText(filename, data)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -463,7 +481,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
|
||||
const pasted = (ev as ClipboardEvent).clipboardData.getData('text')
|
||||
this.loadProfile(async () => {
|
||||
return await importProfiles('From Clipboard', pasted)
|
||||
return await importProfilesFromText('From Clipboard', pasted)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -489,12 +507,12 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
return
|
||||
}
|
||||
this.loadProfile(async () => {
|
||||
const response = await fetch(this.props.hashParams.profileURL!)
|
||||
const response: Response = await fetch(this.props.hashParams.profileURL!)
|
||||
let filename = new URL(this.props.hashParams.profileURL!).pathname
|
||||
if (filename.includes('/')) {
|
||||
filename = filename.slice(filename.lastIndexOf('/') + 1)
|
||||
}
|
||||
return await importProfiles(filename, await response.text())
|
||||
return await importProfilesFromText(filename, await response.text())
|
||||
})
|
||||
} else if (this.props.hashParams.localProfilePath) {
|
||||
// There isn't good cross-browser support for XHR of local files, even from
|
||||
@@ -502,8 +520,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
// as a JavaScript file which will invoke a global function.
|
||||
;(window as any)['speedscope'] = {
|
||||
loadFileFromBase64: (filename: string, base64source: string) => {
|
||||
const source = atob(base64source)
|
||||
this.loadProfile(() => importProfiles(filename, source))
|
||||
this.loadProfile(() => importProfilesFromBase64(filename, base64source))
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user