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:
Jamie Wong
2018-09-26 11:33:34 -07:00
committed by GitHub
parent c70171836c
commit 3f205ec3e9
21 changed files with 5748 additions and 81 deletions

99
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

Binary file not shown.

2
sample/programs/go/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
simple
server

View 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=$@

View 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)
}
}
}

View 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()
}
}

View 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"`;

View File

@@ -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)
})

View File

@@ -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')

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

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
View 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))
}
}

View File

@@ -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

View File

@@ -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
})
})

View File

@@ -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
}

View File

@@ -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))
},
}