Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fe0c454d3 | ||
|
|
4b292b2acf | ||
|
|
d78d20e005 | ||
|
|
7d0a3a2c59 | ||
|
|
3f205ec3e9 | ||
|
|
c70171836c | ||
|
|
ee0c2c5025 | ||
|
|
3ba60a424d |
+21
-1
@@ -1,16 +1,36 @@
|
||||
## Unreleased
|
||||
|
||||
## [1.2.0] - 2018-10-08
|
||||
|
||||
### Added
|
||||
|
||||
* Add import of v8 heap allocation profile [#170] (by @vmarchaud)
|
||||
|
||||
## [1.1.0] - 2018-09-26
|
||||
|
||||
### Added
|
||||
|
||||
* Add go tool pprof import support [#165]
|
||||
|
||||
## [1.0.4] - 2018-09-12
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix import from Chrome < 69 when there are multiple profiles [#161]
|
||||
|
||||
## [1.0.3] - 2018-09-10
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix import for Chrome 69, support leading idle time before first call [#160]
|
||||
|
||||
## [1.0.2] - 2018-09-04
|
||||
|
||||
### Fixed
|
||||
|
||||
* Allow optional CR before LF when probing collapsed stacks files [#154]
|
||||
* Fix import for Firefox 63 [#156]
|
||||
* Change time formatting for minutes from 1.50min to 1:30 [#153]
|
||||
* Change time formatting for minutes from 1.50min to 1:30 [#153] (by @Alex-Diez)
|
||||
|
||||
## [1.0.1] - 2018-08-23
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ speedscope is designed to ingest profiles from a variety of different profilers
|
||||
- Ruby
|
||||
- [Importing from stackprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-stackprof-(ruby))
|
||||
- [Importing from rbspy](https://github.com/jlfwong/speedscope/wiki/Importing-from-rbspy-(ruby))
|
||||
- Go
|
||||
- [Importing from pprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-pprof-(go))
|
||||
- Native code
|
||||
- [Importing from Instruments.app](https://github.com/jlfwong/speedscope/wiki/Importing-from-Instruments.app) (macOS)
|
||||
- [Importing from `perf`](https://github.com/jlfwong/speedscope/wiki/Importing-from-perf-(linux)) (linux)
|
||||
|
||||
Generated
+2622
-2528
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "speedscope",
|
||||
"version": "1.0.3",
|
||||
"version": "1.2.0",
|
||||
"description": "",
|
||||
"repository": "jlfwong/speedscope",
|
||||
"main": "index.js",
|
||||
@@ -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",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
simple
|
||||
server
|
||||
@@ -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=$@
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,7 +74,6 @@ export function importFromChromeTimeline(events: TimelineEvent[]): Profile {
|
||||
for (let event of events) {
|
||||
if (event.name == 'CpuProfile') {
|
||||
cpuProfile = event.args.data.cpuProfile as CPUProfile
|
||||
break
|
||||
}
|
||||
|
||||
if (event.name == 'Profile') {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
+53
-6
@@ -9,12 +9,42 @@ 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'
|
||||
import {importFromChromeHeapProfile} from './v8heapalloc'
|
||||
|
||||
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 +64,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')
|
||||
@@ -63,6 +104,9 @@ async function _importProfileGroup(
|
||||
} else if (fileName.endsWith('.v8log.json')) {
|
||||
console.log('Importing as --prof-process v8 log')
|
||||
return toGroup(importFromV8ProfLog(JSON.parse(contents)))
|
||||
} else if (fileName.endsWith('.heapprofile')) {
|
||||
console.log('Importing as Chrome Heap Profile')
|
||||
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
|
||||
}
|
||||
|
||||
// Second pass: Try to guess what file format it is based on structure
|
||||
@@ -89,6 +133,9 @@ async function _importProfileGroup(
|
||||
} else if ('code' in parsed && 'functions' in parsed && 'ticks' in parsed) {
|
||||
console.log('Importing as --prof-process v8 log')
|
||||
return toGroup(importFromV8ProfLog(parsed))
|
||||
} else if ('head' in parsed && 'selfSize' in parsed['head']) {
|
||||
console.log('Importing as Chrome Heap Profile')
|
||||
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
|
||||
}
|
||||
} else {
|
||||
// Format is not JSON
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
test('importAsPprofProfile', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/pprof/simple.prof')
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Vendored
+1047
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
test('importV8HeapAlloc from Chrome', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Chrome/69/Heap-20181005T144546.heapprofile')
|
||||
})
|
||||
|
||||
test('importV8HeapAlloc from NodeJS', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/node/10.11.0/Heap-20181003T105432.heapprofile')
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
|
||||
import {getOrInsert} from '../lib/utils'
|
||||
import {ByteFormatter} from '../lib/value-formatters'
|
||||
|
||||
/**
|
||||
* The V8 Heap Allocation profile is a way to represent heap allocation for each
|
||||
* javascript function. The format is a simple tree where the weight of each node
|
||||
* represent the memory allocated by the function and all its callee.
|
||||
* You can find more information on how to get a profile there :
|
||||
* https://developers.google.com/web/tools/chrome-devtools/memory-problems/#allocation-profile
|
||||
* You need to scroll down to "Investigate memory allocation by function"
|
||||
*
|
||||
* Note that Node.JS can retrieve this kind of profile via the Inspector protocol.
|
||||
*/
|
||||
|
||||
interface HeapProfileCallFrame {
|
||||
columnNumber: number
|
||||
functionName: string
|
||||
lineNumber: number
|
||||
scriptId: string
|
||||
url: string
|
||||
}
|
||||
|
||||
interface HeapProfileNode {
|
||||
callFrame: HeapProfileCallFrame
|
||||
selfSize: number
|
||||
children: HeapProfileNode[]
|
||||
id: number
|
||||
parent?: number
|
||||
totalSize: number
|
||||
}
|
||||
|
||||
interface HeapProfile {
|
||||
head: HeapProfileNode
|
||||
}
|
||||
|
||||
const callFrameToFrameInfo = new Map<HeapProfileCallFrame, FrameInfo>()
|
||||
function frameInfoForCallFrame(callFrame: HeapProfileCallFrame) {
|
||||
return getOrInsert(callFrameToFrameInfo, callFrame, callFrame => {
|
||||
const name = callFrame.functionName || '(anonymous)'
|
||||
const file = callFrame.url
|
||||
const line = callFrame.lineNumber
|
||||
const col = callFrame.columnNumber
|
||||
return {
|
||||
key: `${name}:${file}:${line}:${col}`,
|
||||
name,
|
||||
file,
|
||||
line,
|
||||
col,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function importFromChromeHeapProfile(chromeProfile: HeapProfile): Profile {
|
||||
const nodeById = new Map<number, HeapProfileNode>()
|
||||
let currentId = 0
|
||||
const computeId = (node: HeapProfileNode, parent?: HeapProfileNode) => {
|
||||
node.id = currentId++
|
||||
nodeById.set(node.id, node)
|
||||
if (parent) {
|
||||
node.parent = parent.id
|
||||
}
|
||||
|
||||
node.children.forEach(children => computeId(children, node))
|
||||
}
|
||||
computeId(chromeProfile.head)
|
||||
|
||||
// Compute the total size
|
||||
const computeTotalSize = (node: HeapProfileNode): number => {
|
||||
if (node.children.length === 0) return node.selfSize || 0
|
||||
const totalChild = node.children.reduce((total: number, children) => {
|
||||
total += computeTotalSize(children)
|
||||
return total
|
||||
}, node.selfSize)
|
||||
node.totalSize = totalChild
|
||||
return totalChild
|
||||
}
|
||||
const total = computeTotalSize(chromeProfile.head)
|
||||
|
||||
// Compute all stacks by taking each last node and going upward
|
||||
const stacks: HeapProfileNode[][] = []
|
||||
for (let currentNode of nodeById.values()) {
|
||||
let stack: HeapProfileNode[] = []
|
||||
stack.push(currentNode)
|
||||
// While we found a parent
|
||||
while (true) {
|
||||
if (currentNode.parent === undefined) break
|
||||
const parent = nodeById.get(currentNode.parent)
|
||||
if (parent === undefined) break
|
||||
// Push the parent at the beginning of the stack
|
||||
stack.unshift(parent)
|
||||
currentNode = parent
|
||||
}
|
||||
stacks.push(stack)
|
||||
}
|
||||
|
||||
const profile = new StackListProfileBuilder(total)
|
||||
|
||||
for (let stack of stacks) {
|
||||
const lastFrame = stack[stack.length - 1]
|
||||
profile.appendSampleWithWeight(
|
||||
stack.map(frame => frameInfoForCallFrame(frame.callFrame)),
|
||||
lastFrame.selfSize,
|
||||
)
|
||||
}
|
||||
|
||||
profile.setValueFormatter(new ByteFormatter())
|
||||
return profile.build()
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+36
-19
@@ -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