Compare commits

..
8 Commits
Author SHA1 Message Date
Jamie Wong 0fe0c454d3 1.2.0 2018-10-08 09:49:15 -07:00
vmarchaud 4b292b2acf Add import of v8 heap allocation profile (#170)
This adds support for importing heap profiles from Chrome: https://developers.google.com/web/tools/chrome-devtools/memory-problems/#allocation-profile
2018-10-08 09:15:39 -07:00
Jamie Wong d78d20e005 Update README.md 2018-09-26 13:38:36 -07:00
Jamie Wong 7d0a3a2c59 1.1.0 2018-09-26 11:46:41 -07:00
Jamie Wong 3f205ec3e9 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) 🎉
2018-09-26 11:33:34 -07:00
Jamie Wong c70171836c 1.0.4 2018-09-12 18:22:59 -07:00
Jamie Wong ee0c2c5025 Fix import from Chrome < 69 when there are multiple profiles
It seems like #160 accidentally broken import of profiles in some circumstances from Chrome < 69. Before #160, we always took the first profile in the list *but* the profiles were not sorted chronologically. After #160 but before this PR, we were taking the chronologically first.

After this PR, we always take the chronologically last `CpuProfile` event in the trace.
2018-09-12 18:21:04 -07:00
Jamie Wong 3ba60a424d Update CHANGELOG.md 2018-09-10 14:40:18 -07:00
29 changed files with 13299 additions and 2611 deletions
+21 -1
View File
@@ -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
+2
View File
@@ -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)
+2622 -2528
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
simple
server
+15
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=$@
+71
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)
}
}
}
+53
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()
}
}
+249
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"`;
File diff suppressed because it is too large Load Diff
-1
View File
@@ -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') {
+4 -4
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)
})
+53 -6
View File
@@ -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
+3 -47
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
View File
@@ -0,0 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importAsPprofProfile', async () => {
await checkProfileSnapshot('./sample/profiles/pprof/simple.prof')
})
+134
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
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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+83
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))
}
}
+9
View File
@@ -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')
})
+109
View File
@@ -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()
}
+5 -4
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
+19
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
})
})
+128
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
}
+36 -19
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))
},
}