Compare commits

...
19 Commits
Author SHA1 Message Date
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
Jamie Wong c9ba143eb6 1.0.3 2018-09-10 13:54:32 -07:00
Jamie Wong 802fc2d358 Add test for Chrome 69 import 2018-09-09 18:03:49 -07:00
Jamie Wong b910a2069b Fix import for Chrome 69, support leading idle time before first call (#160)
This PR fixes #159, and also fixes various small things about how profiles were imported for previous versions of Chrome & for Firefox.

The Chrome 69 format splits profiles across several [Trace Event Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview) events. There are two relevant events: "Profile" and "ProfileChunk". At first read through a profile, it seems like profiles are incorrectly terminated, but it seems like the cause of that is that, for whatever reason, events in the event log are not always sorted in chronological order. If sorted chronologically, then the event sequence can be parsed sensibly.

In the process of looking at this information, I also discovered that speedscope's chrome importer was incorrectly interpreting the value of the first element in `timeDeltas` array. It's intended to be the elapsed time since the start of the profile, not the time between the first pair of samples. This changes the weight attributed to the first sample.
2018-09-09 18:00:30 -07:00
William Martin Stewart a1f9755f9c Pretty print JSON (#158) 2018-09-05 09:21:26 -07:00
Jamie Wong 2686a3ccc0 1.0.2 2018-09-04 20:55:25 -07:00
Jamie Wong 789f296c9c Run unit tests as part of release build 2018-09-04 20:53:58 -07:00
Jamie Wong 64e290c9fc Change deploy script to use assets from npm 2018-09-04 20:50:30 -07:00
Jamie Wong a0eba8d434 Update CHANGELOG.md 2018-09-04 20:13:29 -07:00
januszn 281d9f9033 Allow optional CR before LF when probing collapsed stacks files (#154)
This fixes #152, in that it allows "collapsed stacks" files generated with
tools using Windows line endings to be imported into the tool verbatim.
2018-09-04 20:12:27 -07:00
Jamie Wong 44a1f520fe Update CHANGELOG.md 2018-09-04 17:04:35 -07:00
Jonathan Chan b6190362b4 Match more Firefox-internal locations (#156)
Looks like Firefox also generates locations with names like
`bound (self-hosted:951:0)`. We check for `self-hosted`, but not for
`self-hosted` with stuff after it following a colon. We should ignore
these too, otherwise we can end up with stuff on our stack that we don't
expect. This was causing Firefox profiles not to load because we
completed building the profile with a non-empty stack.

Attached is a profile that errors without this patch and successfully renders
with this patch.

[copy.json.zip](https://github.com/jlfwong/speedscope/files/2350583/copy.json.zip)
2018-09-04 17:03:55 -07:00
Jamie Wong a09f27d816 Update CHANGELOG.md 2018-09-04 16:14:41 -07:00
Alex Dukhno 944a6cb126 Change time formatting for minutes from 1.50min to 1:30 (#153) 2018-09-04 13:30:02 -07:00
Jamie Wong 828beb7ccf Update README-ADMINS.md 2018-08-23 10:04:42 -07:00
43 changed files with 10468 additions and 2833 deletions
+28
View File
@@ -1,3 +1,31 @@
## Unreleased
## [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]
## [1.0.1] - 2018-08-23
* Fixed an issue where flamegraph bounds were not always being cleared correctly, leading to visual artifacts [#150]
+11 -6
View File
@@ -44,7 +44,7 @@ If everything looks good, proceed to "Prepare the release".
2. Update CHANGELOG.md to indicate the changes that were made as part of this release
3. Commit the changes with the version name as the commit message, e.g. `git commit -m 0.6.0`
4. `git tag` the release. We use tags like `v0.6.0`, e.g. `git tag v0.6.0`
5. `git push --tags`
5. `git push && git push --tags`
## Publish to npm
@@ -59,13 +59,16 @@ Try `speedscope sample/profiles/stackcollapse/simple.txt`, which should immediat
## Deploying the website
This step must follow the "Publish to npm" step, since it uses assets from
the npm publish.
https://www.speedscope.app/ is hosted on GitHub pages, and is published via pushing
to the `gh-pages` branch. The `gh-pages` branch has totally different contents than
other branches of this repository: https://github.com/jlfwong/speedscope/tree/gh-pages.
It's populated by a deploy script which is invoked by running `npm run deploy` script.
This will do a build of the static resources, and boot a local server for you to test
the compiled assets. Please do not skip the manual testing in this step.
It's populated by a deploy script which is invoked by running `npm run deploy` script. This populate a directory with assets pulled from npm, and
boot a local server for you to test the compiled assets. Please do not skip
the manual testing in this step.
If everything looks good, you should be able to hit Ctrl+C, and you should see this prompt:
@@ -80,8 +83,10 @@ which includes the version, the date, and the commit of the deploy.
## Upload a release to GitHub
To make a zipfile suitable for uploading to GitHub as a release, run prepare-zip-file.sh. Note that this step must follow the "Publish to npm" step,
since it uses assets from the npm publish.
This step must follow the "Publish to npm" step, since it uses assets from
the npm publish.
To make a zipfile suitable for uploading to GitHub as a release, run `scripts/prepare-zip-file.sh`.
Once that's done, you should have a zip file in `dist/release/`
+2622 -2528
View File
File diff suppressed because it is too large Load Diff
+22 -5
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "1.0.1",
"version": "1.1.0",
"description": "",
"repository": "jlfwong/speedscope",
"main": "index.js",
@@ -17,8 +17,15 @@
"test": "tsc --noEmit && npm run lint && npm run coverage",
"serve": "parcel assets/index.html --open --no-autoinstall"
},
"files": ["bin/cli.js", "dist/release/**", "!*.map"],
"browserslist": ["last 2 Chrome versions", "last 2 Firefox versions"],
"files": [
"bin/cli.js",
"dist/release/**",
"!*.map"
],
"browserslist": [
"last 2 Chrome versions",
"last 2 Firefox versions"
],
"author": "",
"license": "MIT",
"devDependencies": {
@@ -38,6 +45,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",
@@ -50,8 +58,17 @@
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "\\.test\\.tsx?$",
"collectCoverageFrom": ["**/*.{ts,tsx}", "!**/*.d.{ts,tsx}"],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"]
"collectCoverageFrom": [
"**/*.{ts,tsx}",
"!**/*.d.{ts,tsx}"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
]
},
"dependencies": {
"opn": "5.3.0"
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +1,81 @@
{"version":"0.0.1","$schema":"https://www.speedscope.app/file-format-schema.json","shared":{"frames":[{"name":"a"},{"name":"b"},{"name":"c"},{"name":"d"}]},"profiles":[{"type":"evented","name":"simple.txt","unit":"none","startValue":0,"endValue":14,"events":[{"type":"O","frame":0,"at":0},{"type":"O","frame":1,"at":0},{"type":"O","frame":2,"at":0},{"type":"C","frame":2,"at":2},{"type":"O","frame":3,"at":2},{"type":"C","frame":3,"at":6},{"type":"O","frame":2,"at":6},{"type":"C","frame":2,"at":9},{"type":"C","frame":1,"at":14},{"type":"C","frame":0,"at":14}]}]}
{
"$schema": "https://www.speedscope.app/file-format-schema.json",
"profiles": [
{
"endValue": 14,
"events": [
{
"at": 0,
"frame": 0,
"type": "O"
},
{
"at": 0,
"frame": 1,
"type": "O"
},
{
"at": 0,
"frame": 2,
"type": "O"
},
{
"at": 2,
"frame": 2,
"type": "C"
},
{
"at": 2,
"frame": 3,
"type": "O"
},
{
"at": 6,
"frame": 3,
"type": "C"
},
{
"at": 6,
"frame": 2,
"type": "O"
},
{
"at": 9,
"frame": 2,
"type": "C"
},
{
"at": 14,
"frame": 1,
"type": "C"
},
{
"at": 14,
"frame": 0,
"type": "C"
}
],
"name": "simple.txt",
"startValue": 0,
"type": "evented",
"unit": "none"
}
],
"shared": {
"frames": [
{
"name": "a"
},
{
"name": "b"
},
{
"name": "c"
},
{
"name": "d"
}
]
},
"version": "0.0.1"
}
@@ -0,0 +1,5 @@
a;b;c 1
a;b;c 1
a;b;d 4
a;b;c 3
a;b 5
+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()
}
}
-4
View File
@@ -25,8 +25,4 @@ function gamma() {
return prod
}
console.profile('simple')
alpha()
setTimeout(() => {
console.profileEnd('simple')
}, 0)
+3
View File
@@ -7,6 +7,9 @@ OUTDIR=`pwd`/dist/release
# Typecheck
node_modules/.bin/tsc --noEmit
# Run unit tests
npm run jest
# Clean out the release directory
rm -rf "$OUTDIR"
mkdir -p "$OUTDIR"
+12 -13
View File
@@ -1,26 +1,28 @@
#!/bin/bash
#
# type check, do a release build, then do a shallow clone of the
# repository into a temporary directory and copy the release build
# artifacts into there to commit & push to the gh-pages branch
# Do a shallow clone of the repository into a temporary directory and copy the
# artifacts pulled from npm into the shallow clone to commit & push to the
# gh-pages branch.
set -euxo pipefail
OUTDIR=`pwd`/dist/release
echo $OUTDIR
SRCDIR=`pwd`
OUTDIR=`mktemp -d -t speedscope-unpacked`
./scripts/build-release.sh
# Untar the package
pushd "$OUTDIR"
PACKEDNAME=`npm pack speedscope | tail -n1`
tar -xvvf "$PACKEDNAME"
# Create a shallow clone of the repository
TMPDIR=`mktemp -d -t speedscope-release`
echo "Entering $TMPDIR"
TMPDIR=`mktemp -d -t speedscope-deploy`
pushd "$TMPDIR"
git clone --depth 1 git@github.com:jlfwong/speedscope.git -b gh-pages
# Copy the build artifacts into the shallow clone
pushd speedscope
rm -rf *
cp -R "$OUTDIR"/* .
cp -R "$OUTDIR"/package/dist/release/** .
# Set the CNAME record
echo www.speedscope.app > CNAME
@@ -35,17 +37,14 @@ function ctrl_c() {
if [[ $REPLY =~ ^yes$ ]]
then
git add --all
git commit -m 'Release'
git commit -m "Deploy $PACKEDNAME"
git push origin HEAD:gh-pages
popd
rm -rf "$TMPDIR"
exit 0
else
set +x
echo "Aborting release."
set -x
popd
rm -rf "$TMPDIR"
exit 1
fi
+2 -2
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Prepare a mock installation of speedscope to test it before the actual npm
# publish
# Create a zip file containing a standalone copy of speedscope
# based on the contents of the package published to npm
set -euxo pipefail
@@ -50,6 +50,60 @@ Object {
}
`;
exports[`importFromBGFlameGraph with CRLF 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "a",
"line": undefined,
"name": "a",
"selfWeight": 0,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "b",
"line": undefined,
"name": "b",
"selfWeight": 5,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "c",
"line": undefined,
"name": "c",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "d",
"line": undefined,
"name": "d",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "simple-crlf.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
"a;b;c 3",
"a;b 5",
],
}
`;
exports[`importFromBGFlameGraph with CRLF: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with CRLF: profileGroup.name 1`] = `"simple-crlf.txt"`;
exports[`importFromBGFlameGraph: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph: profileGroup.name 1`] = `"simple.txt"`;
+407 -154
View File
@@ -9,8 +9,8 @@ Object {
"key": "(anonymous)::0:0",
"line": 0,
"name": "(anonymous)",
"selfWeight": 11575,
"totalWeight": 45920,
"selfWeight": 1060,
"totalWeight": 34345,
},
Frame {
"col": 10,
@@ -19,7 +19,7 @@ Object {
"line": 0,
"name": "a",
"selfWeight": 0,
"totalWeight": 34345,
"totalWeight": 33285,
},
Frame {
"col": 10,
@@ -28,7 +28,7 @@ Object {
"line": 5,
"name": "b",
"selfWeight": 0,
"totalWeight": 16788,
"totalWeight": 17002,
},
Frame {
"col": 10,
@@ -36,8 +36,8 @@ Object {
"key": "d::13:10",
"line": 13,
"name": "d",
"selfWeight": 34345,
"totalWeight": 34345,
"selfWeight": 33285,
"totalWeight": 33285,
},
Frame {
"col": 10,
@@ -46,14 +46,15 @@ Object {
"line": 9,
"name": "c",
"selfWeight": 0,
"totalWeight": 17557,
"totalWeight": 16283,
},
],
"name": "simple.cpuprofile",
"stacks": Array [
"(anonymous) 11.57ms",
"(anonymous);a;b;d 16.79ms",
"(anonymous);a;c;d 17.56ms",
" 11.57ms",
"(anonymous) 1.06ms",
"(anonymous);a;b;d 17.00ms",
"(anonymous);a;c;d 16.28ms",
],
}
`;
@@ -71,8 +72,8 @@ Object {
"key": "(program)::-1:-1",
"line": -1,
"name": "(program)",
"selfWeight": 12244,
"totalWeight": 12244,
"selfWeight": 2299,
"totalWeight": 2299,
},
Frame {
"col": 10,
@@ -81,7 +82,7 @@ Object {
"line": 0,
"name": "a",
"selfWeight": 0,
"totalWeight": 33184,
"totalWeight": 33186,
},
Frame {
"col": 10,
@@ -89,8 +90,8 @@ Object {
"key": "c:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:13:10",
"line": 13,
"name": "c",
"selfWeight": 16197,
"totalWeight": 16197,
"selfWeight": 16275,
"totalWeight": 16275,
},
Frame {
"col": 10,
@@ -98,170 +99,422 @@ Object {
"key": "b:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:7:10",
"line": 7,
"name": "b",
"selfWeight": 16987,
"totalWeight": 16987,
"selfWeight": 16911,
"totalWeight": 16911,
},
],
"name": "simple-timeline.json",
"stacks": Array [
"(program) 10.16ms",
" 1.99ms",
"(program) 926.00µs",
" 85.95ms",
"(program) 133.00µs",
" 10.06ms",
"(program) 202.00µs",
" 1.97ms",
"(program) 1.04ms",
" 85.89ms",
"(program) 132.00µs",
" 8.40ms",
"(program) 134.00µs",
"a;c 257.00µs",
"a;b 534.00µs",
"a;c 513.00µs",
"a;c 273.00µs",
"a;b 513.00µs",
"a;c 257.00µs",
"a;b 518.00µs",
"a;c 514.00µs",
"a;b 261.00µs",
"a;c 512.00µs",
"a;b 513.00µs",
"a;c 262.00µs",
"a;b 514.00µs",
"a;c 517.00µs",
"a;b 258.00µs",
"a;b 261.00µs",
"a;c 515.00µs",
"a;b 273.00µs",
"a;c 134.00µs",
"a;b 146.00µs",
"a;c 291.00µs",
"a;b 256.00µs",
"a;c 145.00µs",
"a;b 134.00µs",
"a;c 292.00µs",
"a;b 272.00µs",
"a;c 129.00µs",
"a;b 145.00µs",
"a;c 265.00µs",
"a;b 142.00µs",
"a;c 141.00µs",
"a;b 139.00µs",
"a;c 397.00µs",
"a;b 126.00µs",
"a;c 131.00µs",
"a;b 126.00µs",
"a;c 521.00µs",
"a;b 128.00µs",
"a;c 261.00µs",
"a;b 127.00µs",
"a;c 130.00µs",
"a;b 128.00µs",
"a;c 136.00µs",
"a;b 272.00µs",
"a;c 146.00µs",
"a;b 661.00µs",
"a;c 113.00µs",
"a;b 135.00µs",
"a;c 585.00µs",
"a;b 294.00µs",
"a;c 274.00µs",
"a;b 129.00µs",
"a;c 127.00µs",
"a;b 128.00µs",
"a;c 247.00µs",
"a;b 526.00µs",
"a;c 125.00µs",
"a;b 129.00µs",
"a;c 131.00µs",
"a;b 253.00µs",
"a;c 135.00µs",
"a;b 123.00µs",
"a;c 128.00µs",
"a;b 257.00µs",
"a;c 137.00µs",
"a;b 256.00µs",
"a;c 411.00µs",
"a;b 892.00µs",
"a;c 146.00µs",
"a;b 148.00µs",
"a;c 309.00µs",
"a;b 140.00µs",
"a;c 151.00µs",
"a;b 458.00µs",
"a;c 156.00µs",
"a;b 170.00µs",
"a;c 273.00µs",
"a;b 141.00µs",
"a;c 139.00µs",
"a;b 290.00µs",
"a;c 115.00µs",
"a;b 281.00µs",
"a;c 270.00µs",
"a;b 283.00µs",
"a;c 144.00µs",
"a;b 146.00µs",
"a;c 410.00µs",
"a;b 146.00µs",
"a;c 147.00µs",
"a;b 312.00µs",
"a;c 146.00µs",
"a;b 907.00µs",
"a;c 231.00µs",
"a;b 138.00µs",
"a;c 280.00µs",
"a;b 255.00µs",
"a;c 257.00µs",
"a;b 130.00µs",
"a;c 255.00µs",
"a;b 128.00µs",
"a;c 526.00µs",
"a;b 128.00µs",
"a;c 384.00µs",
"a;b 398.00µs",
"a;c 260.00µs",
"a;b 664.00µs",
"a;c 393.00µs",
"a;b 131.00µs",
"a;c 126.00µs",
"a;b 129.00µs",
"a;c 520.00µs",
"a;b 129.00µs",
"a;c 259.00µs",
"a;b 130.00µs",
"a;c 128.00µs",
"a;b 136.00µs",
"a;c 137.00µs",
"a;b 281.00µs",
"a;c 150.00µs",
"a;b 624.00µs",
"a;c 135.00µs",
"a;b 129.00µs",
"a;c 604.00µs",
"a;b 292.00µs",
"a;c 257.00µs",
"a;b 127.00µs",
"a;c 128.00µs",
"a;b 118.00µs",
"a;c 256.00µs",
"a;b 524.00µs",
"a;c 129.00µs",
"a;b 131.00µs",
"a;c 125.00µs",
"a;b 263.00µs",
"a;c 123.00µs",
"a;b 128.00µs",
"a;c 129.00µs",
"a;b 265.00µs",
"a;c 128.00µs",
"a;b 280.00µs",
"a;c 366.00µs",
"a;b 931.00µs",
"a;c 148.00µs",
"a;b 147.00µs",
"a;c 156.00µs",
"a;b 137.00µs",
"a;c 302.00µs",
"a;b 151.00µs",
"a;c 158.00µs",
"a;b 456.00µs",
"a;c 170.00µs",
"a;b 139.00µs",
"a;c 145.00µs",
"a;b 260.00µs",
"a;c 141.00µs",
"a;b 280.00µs",
"a;c 268.00µs",
"a;b 289.00µs",
"a;c 146.00µs",
"a;b 120.00µs",
"a;c 436.00µs",
"a;b 147.00µs",
"a;c 164.00µs",
"a;b 294.00µs",
"a;c 147.00µs",
"a;b 856.00µs",
"a;c 273.00µs",
"a;b 139.00µs",
"a;c 269.00µs",
"a;b 256.00µs",
"a;c 258.00µs",
"a;b 128.00µs",
"a;c 255.00µs",
"a;b 140.00µs",
"a;c 141.00µs",
"a;c 514.00µs",
"a;b 129.00µs",
"a;c 395.00µs",
"a;b 389.00µs",
"a;c 257.00µs",
"a;b 686.00µs",
"a;c 147.00µs",
"a;b 156.00µs",
"a;c 137.00µs",
"a;b 141.00µs",
"a;c 140.00µs",
"a;b 140.00µs",
"a;c 139.00µs",
"a;b 129.00µs",
"a;c 269.00µs",
"a;b 293.00µs",
"a;c 148.00µs",
"a;b 293.00µs",
"a;c 292.00µs",
"a;b 296.00µs",
"a;c 147.00µs",
"a;b 140.00µs",
"a;c 139.00µs",
"a;b 139.00µs",
"a;c 511.00µs",
"a;b 515.00µs",
"a;c 386.00µs",
"a;b 514.00µs",
"a;c 272.00µs",
"a;b 242.00µs",
"a;c 128.00µs",
"a;b 134.00µs",
"a;c 112.00µs",
"a;b 288.00µs",
"a;c 451.00µs",
"a;b 138.00µs",
"a;c 292.00µs",
"a;b 145.00µs",
"a;c 259.00µs",
"a;b 148.00µs",
"a;c 293.00µs",
"a;b 141.00µs",
"a;c 141.00µs",
"a;b 140.00µs",
"a;c 140.00µs",
" 61.61ms",
"(program) 118.00µs",
" 134.00µs",
"(program) 128.00µs",
" 16.40ms",
"(program) 125.00µs",
" 16.64ms",
"(program) 129.00µs",
" 16.72ms",
"(program) 130.00µs",
" 294.15ms",
"(program) 129.00µs",
" 7.68ms",
"a;b 139.00µs",
"a;c 129.00µs",
"a;b 149.00µs",
"a;c 267.00µs",
"a;b 294.00µs",
"a;c 145.00µs",
"a;b 294.00µs",
"a;c 294.00µs",
"a;b 295.00µs",
"a;c 140.00µs",
"a;b 139.00µs",
"a;c 139.00µs",
"a;b 127.00µs",
"a;c 513.00µs",
"a;b 516.00µs",
"a;c 385.00µs",
"a;b 520.00µs",
"a;c 252.00µs",
"a;b 255.00µs",
"a;c 134.00µs",
"a;b 112.00µs",
"a;c 140.00µs",
"a;b 296.00µs",
"a;c 441.00µs",
"a;b 145.00µs",
"a;c 292.00µs",
"a;b 130.00µs",
"a;c 277.00µs",
"a;b 147.00µs",
"a;c 286.00µs",
"a;b 140.00µs",
"a;c 136.00µs",
" 61.59ms",
"(program) 134.00µs",
" 128.00µs",
"(program) 141.00µs",
" 16.39ms",
"(program) 145.00µs",
" 16.63ms",
"(program) 131.00µs",
" 16.71ms",
"(program) 131.00µs",
" 294.15ms",
"(program) 109.00µs",
],
}
`;
exports[`importFromChromeTimeline Chrome 69 1`] = `
Object {
"frames": Array [
Frame {
"col": 0,
"file": "extensions::SafeBuiltins",
"key": "(anonymous):extensions::SafeBuiltins:0:0",
"line": 0,
"name": "(anonymous)",
"selfWeight": 0,
"totalWeight": 3188,
},
Frame {
"col": 9,
"file": "extensions::SafeBuiltins",
"key": "(anonymous):extensions::SafeBuiltins:0:9",
"line": 0,
"name": "(anonymous)",
"selfWeight": 0,
"totalWeight": 3188,
},
Frame {
"col": 20,
"file": "extensions::SafeBuiltins",
"key": "saveBuiltin:extensions::SafeBuiltins:24:20",
"line": 24,
"name": "saveBuiltin",
"selfWeight": 302,
"totalWeight": 3188,
},
Frame {
"col": undefined,
"file": undefined,
"key": "(program):undefined:undefined:undefined",
"line": undefined,
"name": "(program)",
"selfWeight": 6862,
"totalWeight": 6862,
},
Frame {
"col": 0,
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "(anonymous):file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:0:0",
"line": 0,
"name": "(anonymous)",
"selfWeight": 136,
"totalWeight": 37217,
},
Frame {
"col": 14,
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "alpha:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:0:14",
"line": 0,
"name": "alpha",
"selfWeight": 873,
"totalWeight": 37081,
},
Frame {
"col": 13,
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "beta:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:7:13",
"line": 7,
"name": "beta",
"selfWeight": 7552,
"totalWeight": 17562,
},
Frame {
"col": 14,
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "gamma:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:19:14",
"line": 19,
"name": "gamma",
"selfWeight": 20018,
"totalWeight": 20018,
},
Frame {
"col": 14,
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "delta:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:13:14",
"line": 13,
"name": "delta",
"selfWeight": 7179,
"totalWeight": 18646,
},
Frame {
"col": undefined,
"file": undefined,
"key": "(garbage collector):undefined:undefined:undefined",
"line": undefined,
"name": "(garbage collector)",
"selfWeight": 646,
"totalWeight": 778,
},
],
"name": "simple.json",
"stacks": Array [
" 6.85ms",
"(anonymous);(anonymous);saveBuiltin 302.00µs",
"(anonymous);(anonymous);saveBuiltin;(program) 2.89ms",
" 230.00µs",
"(program) 828.00µs",
" 110.00µs",
"(program) 118.00µs",
" 323.00µs",
"(program) 123.00µs",
" 139.00µs",
"(program) 400.00µs",
"(anonymous) 136.00µs",
"(anonymous);alpha;beta 108.00µs",
"(anonymous);alpha;beta;gamma 268.00µs",
"(anonymous);alpha 137.00µs",
"(anonymous);alpha;delta 139.00µs",
"(anonymous);alpha;delta;gamma 261.00µs",
"(anonymous);alpha;beta;gamma 964.00µs",
"(anonymous);alpha;delta;gamma 268.00µs",
"(anonymous);alpha;beta;gamma 265.00µs",
"(anonymous);alpha;delta;gamma 582.00µs",
"(anonymous);alpha;beta;gamma 135.00µs",
"(anonymous);alpha;delta;gamma 147.00µs",
"(anonymous);alpha;beta;gamma 163.00µs",
"(anonymous);alpha;delta;gamma 133.00µs",
"(anonymous);alpha;beta;gamma 144.00µs",
"(anonymous);alpha;delta;gamma 276.00µs",
"(anonymous);alpha;delta 136.00µs",
"(anonymous);alpha;beta;gamma 144.00µs",
"(anonymous);alpha;delta;gamma 431.00µs",
"(anonymous);alpha;beta;gamma 419.00µs",
"(anonymous);alpha;delta;gamma 134.00µs",
"(anonymous);alpha;beta;gamma 136.00µs",
"(anonymous);alpha;delta;gamma 144.00µs",
"(anonymous);alpha;beta;gamma 269.00µs",
"(anonymous);alpha;delta;gamma 160.00µs",
"(anonymous);alpha;beta;gamma 141.00µs",
"(anonymous);alpha;delta;gamma 292.00µs",
"(anonymous);alpha;beta;gamma 583.00µs",
"(anonymous);alpha;delta;gamma 149.00µs",
"(anonymous);alpha;beta;gamma 139.00µs",
"(anonymous);alpha;delta;gamma 133.00µs",
"(anonymous);alpha;beta;gamma 386.00µs",
"(anonymous);alpha;delta;gamma 385.00µs",
"(anonymous);alpha;beta;gamma 391.00µs",
"(anonymous);alpha;delta;gamma 261.00µs",
"(anonymous);alpha;beta;gamma 131.00µs",
"(anonymous);alpha;delta;gamma 132.00µs",
"(anonymous);alpha 144.00µs",
"(anonymous);alpha;delta;gamma 134.00µs",
"(anonymous);alpha;beta;gamma 146.00µs",
"(anonymous);alpha;delta;gamma 119.00µs",
"(anonymous);alpha;beta;gamma 120.00µs",
"(anonymous);alpha;delta;gamma 134.00µs",
"(anonymous);alpha;beta;gamma 1.47ms",
"(anonymous);alpha;delta;gamma 148.00µs",
"(anonymous);alpha;beta;gamma 147.00µs",
"(anonymous);alpha;delta;gamma 146.00µs",
"(anonymous);alpha;beta;gamma 269.00µs",
"(anonymous);alpha;delta;gamma 138.00µs",
"(anonymous);alpha;beta;gamma 509.00µs",
"(anonymous);alpha;delta;gamma 1.63ms",
"(anonymous);alpha;beta;gamma 432.00µs",
"(anonymous);alpha;delta;gamma 129.00µs",
"(anonymous);alpha;beta;gamma 139.00µs",
"(anonymous);alpha;delta;gamma 389.00µs",
"(anonymous);alpha;beta 133.00µs",
"(anonymous);alpha;beta;gamma 133.00µs",
"(anonymous);alpha;delta;gamma 803.00µs",
"(anonymous);alpha;beta;gamma 283.00µs",
"(anonymous);alpha;delta;gamma 152.00µs",
"(anonymous);alpha;beta;gamma 135.00µs",
"(anonymous);alpha;delta;gamma 132.00µs",
"(anonymous);alpha;beta;gamma 134.00µs",
"(anonymous);alpha;delta;gamma 154.00µs",
"(anonymous);alpha;beta;gamma 145.00µs",
"(anonymous);alpha;delta;gamma 516.00µs",
"(anonymous);alpha;delta 128.00µs",
"(anonymous);alpha;delta;gamma 138.00µs",
"(anonymous);alpha;beta;gamma 139.00µs",
"(anonymous);alpha;delta;gamma 253.00µs",
"(anonymous);alpha;beta;gamma 145.00µs",
"(anonymous);alpha;delta;gamma 450.00µs",
"(anonymous);alpha;beta;gamma 142.00µs",
"(anonymous);alpha;delta;gamma 125.00µs",
"(anonymous);alpha;beta;gamma 289.00µs",
"(anonymous);alpha;delta;gamma 293.00µs",
"(anonymous);alpha;beta;gamma 147.00µs",
"(anonymous);alpha 443.00µs",
"(anonymous);alpha;delta;gamma 133.00µs",
"(anonymous);alpha;beta;gamma 409.00µs",
"(anonymous);alpha;beta 385.00µs",
"(anonymous);alpha;delta 659.00µs",
"(anonymous);alpha;beta 253.00µs",
"(anonymous);alpha;delta 257.00µs",
"(anonymous);alpha;beta 256.00µs",
"(anonymous);alpha;delta 129.00µs",
"(anonymous);alpha;beta 260.00µs",
"(anonymous);alpha;delta 255.00µs",
"(anonymous);alpha;beta 129.00µs",
"(anonymous);alpha;delta 140.00µs",
"(anonymous);alpha;beta 136.00µs",
"(anonymous);alpha;delta 134.00µs",
"(anonymous);alpha;beta 283.00µs",
"(anonymous);alpha;delta 253.00µs",
"(anonymous);alpha;beta 397.00µs",
"(anonymous);alpha;delta 401.00µs",
"(anonymous);alpha;beta 267.00µs",
"(anonymous);alpha;delta 283.00µs",
"(anonymous);alpha;beta 137.00µs",
"(anonymous);alpha;delta 134.00µs",
"(anonymous);alpha;beta 403.00µs",
"(anonymous);alpha;delta 135.00µs",
"(anonymous);alpha;beta 658.00µs",
"(anonymous);alpha;delta 117.00µs",
"(anonymous);alpha;beta 257.00µs",
"(anonymous);alpha;delta 134.00µs",
"(anonymous);alpha;beta 143.00µs",
"(anonymous);alpha;delta 260.00µs",
"(anonymous);alpha 149.00µs",
"(anonymous);alpha;delta 146.00µs",
"(anonymous);alpha;beta 129.00µs",
"(anonymous);alpha;delta 131.00µs",
"(anonymous);alpha;beta 130.00µs",
"(anonymous);alpha;delta 665.00µs",
"(anonymous);alpha;beta 415.00µs",
"(anonymous);alpha;delta 264.00µs",
"(anonymous);alpha;beta 527.00µs",
"(anonymous);alpha;delta 140.00µs",
"(anonymous);alpha;beta 269.00µs",
"(anonymous);alpha;delta 135.00µs",
"(anonymous);alpha;beta 386.00µs",
"(anonymous);alpha;delta 559.00µs",
"(anonymous);alpha;beta 131.00µs",
"(anonymous);alpha;delta 140.00µs",
"(anonymous);alpha;beta 129.00µs",
"(anonymous);alpha;delta 400.00µs",
"(anonymous);alpha;beta 134.00µs",
"(anonymous);alpha;delta 634.00µs",
"(anonymous);alpha;beta 1.10ms",
"(anonymous);alpha;delta 271.00µs",
"(anonymous);alpha;delta;(program) 1.46ms",
" 430.00µs",
"(program) 781.00µs",
" 273.00µs",
"(garbage collector) 646.00µs",
"(garbage collector);(program) 132.00µs",
" 26.36ms",
"(program) 135.00µs",
],
}
`;
exports[`importFromChromeTimeline Chrome 69: indexToView 1`] = `0`;
exports[`importFromChromeTimeline Chrome 69: profileGroup.name 1`] = `"simple.json"`;
exports[`importFromChromeTimeline: indexToView 1`] = `0`;
exports[`importFromChromeTimeline: profileGroup.name 1`] = `"simple-timeline.json"`;
@@ -42,6 +42,7 @@ Object {
],
"name": "simple-firefox.json",
"stacks": Array [
" 2.04ms",
"a;b;d 989.53µs",
"a;c;d 1.02ms",
"a;b;d 982.04µs",
@@ -59,6 +60,71 @@ Object {
}
`;
exports[`importFromFirefox ignore self-hosted 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:1",
"key": "alpha (http://localhost:8000/simple.js:1:14)",
"line": 14,
"name": "alpha",
"selfWeight": 0,
"totalWeight": 26.983816999942064,
},
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:14",
"key": "delta (http://localhost:8000/simple.js:14:14)",
"line": 14,
"name": "delta",
"selfWeight": 0,
"totalWeight": 11.946324001066387,
},
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:20",
"key": "gamma (http://localhost:8000/simple.js:20:14)",
"line": 14,
"name": "gamma",
"selfWeight": 26.983816999942064,
"totalWeight": 26.983816999942064,
},
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:8",
"key": "beta (http://localhost:8000/simple.js:8:13)",
"line": 13,
"name": "beta",
"selfWeight": 0,
"totalWeight": 15.037492998875678,
},
],
"name": "simple-firefox.json",
"stacks": Array [
" 4.61ms",
"alpha;delta;gamma 999.57µs",
"alpha;beta;gamma 2.01ms",
"alpha;delta;gamma 1.00ms",
"alpha;beta;gamma 995.68µs",
"alpha;delta;gamma 996.27µs",
"alpha;beta;gamma 4.01ms",
"alpha;delta;gamma 2.02ms",
"alpha;beta;gamma 959.44µs",
"alpha;delta;gamma 2.01ms",
"alpha;beta;gamma 4.01ms",
"alpha;delta;gamma 959.28µs",
"alpha;beta;gamma 2.03ms",
"alpha;delta;gamma 3.96ms",
"alpha;beta;gamma 1.02ms",
],
}
`;
exports[`importFromFirefox ignore self-hosted: indexToView 1`] = `0`;
exports[`importFromFirefox ignore self-hosted: profileGroup.name 1`] = `"simple-firefox.json"`;
exports[`importFromFirefox recursion 1`] = `
Object {
"frames": Array [
@@ -110,6 +176,7 @@ Object {
],
"name": "recursion.json",
"stacks": Array [
" 1.71ms",
"main;alpha;beta;alpha;beta;alpha;beta;alpha;delta;gamma 998.89µs",
"main 1.19ms",
"main;alpha;beta;alpha;delta;gamma 1.83ms",
+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"`;
+4
View File
@@ -3,3 +3,7 @@ import {checkProfileSnapshot} from '../lib/test-utils'
test('importFromBGFlameGraph', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple.txt')
})
test('importFromBGFlameGraph with CRLF', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-crlf.txt')
})
+4
View File
@@ -7,3 +7,7 @@ test('importFromChromeCPUProfile', async () => {
test('importFromChromeTimeline', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/65/simple-timeline.json')
})
test('importFromChromeTimeline Chrome 69', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/69/simple.json')
})
+102 -25
View File
@@ -1,7 +1,9 @@
import {Profile, FrameInfo, CallTreeProfileBuilder} from '../lib/profile'
import {getOrInsert, lastOf} from '../lib/utils'
import {getOrInsert, lastOf, sortBy} from '../lib/utils'
import {TimeFormatter} from '../lib/value-formatters'
// See: https://github.com/v8/v8/blob/master/src/inspector/js_protocol.json
interface TimelineEvent {
pid: number
tid: number
@@ -45,16 +47,77 @@ interface CPUProfile {
timeDeltas: number[]
}
export function isChromeTimeline(rawProfile: any): boolean {
if (!Array.isArray(rawProfile)) return false
if (rawProfile.length < 1) return false
const first = rawProfile[0]
if (!('pid' in first && 'tid' in first && 'ph' in first && 'cat' in first)) return false
if (
!rawProfile.find(
e => e.name === 'CpuProfile' || e.name === 'Profile' || e.name === 'ProfileChunk',
)
)
return false
return true
}
export function importFromChromeTimeline(events: TimelineEvent[]): Profile {
// It seems like sometimes Chrome timeline files contain multiple CpuProfiles?
// For now, choose the first one in the list.
let cpuProfile: CPUProfile | null = null
// The events aren't necessarily recorded in chronological order. Sort them so
// that they are.
sortBy(events, e => e.ts)
for (let event of events) {
if (event.name == 'CpuProfile') {
const chromeProfile = event.args.data.cpuProfile as CPUProfile
return importFromChromeCPUProfile(chromeProfile)
cpuProfile = event.args.data.cpuProfile as CPUProfile
}
if (event.name == 'Profile') {
cpuProfile = {
startTime: 0,
endTime: 0,
nodes: [],
samples: [],
timeDeltas: [],
...event.args.data,
}
}
if (event.name == 'ProfileChunk') {
if (cpuProfile) {
const chunk = event.args.data
if (chunk.cpuProfile) {
if (chunk.cpuProfile.nodes) {
cpuProfile.nodes = cpuProfile.nodes.concat(chunk.cpuProfile.nodes)
}
if (chunk.cpuProfile.samples) {
cpuProfile.samples = cpuProfile.samples.concat(chunk.cpuProfile.samples)
}
}
if (chunk.timeDeltas) {
cpuProfile.timeDeltas = cpuProfile.timeDeltas.concat(chunk.timeDeltas)
}
if (chunk.startTime != null) {
cpuProfile.startTime = chunk.startTime
}
if (chunk.endTime != null) {
cpuProfile.endTime = chunk.endTime
}
} else {
console.log('Ignoring ProfileChunk when no Profile is active')
}
}
}
throw new Error('Could not find CPU profile in Timeline')
if (cpuProfile) {
return importFromChromeCPUProfile(cpuProfile as CPUProfile)
} else {
throw new Error('Could not find CPU profile in Timeline')
}
}
const callFrameToFrameInfo = new Map<CPUProfileCallFrame, FrameInfo>()
@@ -74,7 +137,14 @@ function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
})
}
function shouldIgnoreFunction(functionName: string) {
function shouldIgnoreFunction(callFrame: CPUProfileCallFrame) {
const {functionName, url} = callFrame
if (url === 'native dummy.js') {
// I'm not really sure what this is about, but this seems to be used
// as a way of avoiding edge cases in V8's implementation.
// See: https://github.com/v8/v8/blob/b8626ca4/tools/js2c.py#L419-L424
return true
}
return functionName === '(root)' || functionName === '(idle)'
}
@@ -90,6 +160,10 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
nodeById.set(node.id, node)
}
for (let node of chromeProfile.nodes) {
if (typeof node.parent === 'number') {
node.parent = nodeById.get(node.parent)
}
if (!node.children) continue
for (let childId of node.children) {
const child = nodeById.get(childId)
@@ -99,9 +173,12 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
}
const samples: number[] = []
const timeDeltas: number[] = []
const sampleTimes: number[] = []
// The first delta is relative to the profile startTime.
// Ref: https://github.com/v8/v8/blob/44bd8fd7/src/inspector/js_protocol.json#L1485
let elapsed = chromeProfile.timeDeltas[0]
let elapsed = 0
let lastNodeId = NaN
// The chrome CPU profile format doesn't collapse identical samples. We'll do that
@@ -110,29 +187,30 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
const nodeId = chromeProfile.samples[i]
if (nodeId != lastNodeId) {
samples.push(nodeId)
timeDeltas.push(elapsed)
elapsed = 0
sampleTimes.push(elapsed)
}
let timeDelta = chromeProfile.timeDeltas[i]
if (timeDelta < 0) {
console.warn('Substituting zero for unexpected time delta:', timeDelta, 'at index', i)
timeDelta = 0
}
if (i === chromeProfile.samples.length - 1) {
if (!isNaN(lastNodeId)) {
samples.push(lastNodeId)
sampleTimes.push(elapsed)
}
} else {
let timeDelta = chromeProfile.timeDeltas[i + 1]
if (timeDelta < 0) {
console.warn('Substituting zero for unexpected time delta:', timeDelta, 'at index', i)
timeDelta = 0
}
elapsed += timeDelta
lastNodeId = nodeId
}
if (!isNaN(lastNodeId)) {
samples.push(lastNodeId)
timeDeltas.push(elapsed)
elapsed += timeDelta
lastNodeId = nodeId
}
}
let prevStack: CPUProfileNode[] = []
let value = 0
for (let i = 0; i < samples.length; i++) {
const timeDelta = timeDeltas[i + 1] || 0
const value = sampleTimes[i]
const nodeId = samples[i]
let stackTop = nodeById.get(nodeId)
if (!stackTop) continue
@@ -161,7 +239,7 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
const toOpen: CPUProfileNode[] = []
for (
let node: CPUProfileNode | null = stackTop;
node && node != lca && !shouldIgnoreFunction(node.callFrame.functionName);
node && node != lca && !shouldIgnoreFunction(node.callFrame);
// Place Chrome internal functions on top of the previous call stack
node = shouldPlaceOnTopOfPreviousStack(node.callFrame.functionName)
? lastOf(prevStack)
@@ -176,12 +254,11 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
}
prevStack = prevStack.concat(toOpen)
value += timeDelta
}
// Close frames that are open at the end of the trace
for (let i = prevStack.length - 1; i >= 0; i--) {
profile.leaveFrame(frameInfoForCallFrame(prevStack[i].callFrame), value)
profile.leaveFrame(frameInfoForCallFrame(prevStack[i].callFrame), lastOf(sampleTimes)!)
}
profile.setValueFormatter(new TimeFormatter('microseconds'))
+4
View File
@@ -7,3 +7,7 @@ test('importFromFirefox', async () => {
test('importFromFirefox recursion', async () => {
await checkProfileSnapshot('./sample/profiles/Firefox/61/recursion.json')
})
test('importFromFirefox ignore self-hosted', async () => {
await checkProfileSnapshot('./sample/profiles/Firefox/63/simple-firefox.json')
})
+5 -1
View File
@@ -180,7 +180,11 @@ export function importFromFirefox(firefoxProfile: FirefoxProfile): Profile {
if (!match) return null
if (match[2].startsWith('resource:') || match[2] === 'self-hosted') {
if (
match[2].startsWith('resource:') ||
match[2] === 'self-hosted' ||
match[2].startsWith('self-hosted:')
) {
// Ignore Firefox-internals stuff
return null
}
+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)
})
+49 -9
View File
@@ -1,7 +1,7 @@
import {Profile, ProfileGroup} from '../lib/profile'
import {FileSystemDirectoryEntry} from './file-system-entry'
import {importFromChromeCPUProfile, importFromChromeTimeline} from './chrome'
import {importFromChromeCPUProfile, importFromChromeTimeline, isChromeTimeline} from './chrome'
import {importFromStackprof} from './stackprof'
import {importFromInstrumentsDeepCopy, importFromInstrumentsTrace} from './instruments'
import {importFromBGFlameGraph} from './bg-flamegraph'
@@ -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')
@@ -77,7 +117,7 @@ async function _importProfileGroup(
} else if (parsed['systemHost'] && parsed['systemHost']['name'] == 'Firefox') {
console.log('Importing as Firefox profile')
return toGroup(importFromFirefox(parsed))
} else if (Array.isArray(parsed) && parsed[parsed.length - 1].name === 'CpuProfile') {
} else if (isChromeTimeline(parsed)) {
console.log('Importing as Chrome CPU Profile')
return toGroup(importFromChromeTimeline(parsed))
} else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) {
@@ -103,7 +143,7 @@ async function _importProfileGroup(
// If every line ends with a space followed by a number, it's probably
// the collapsed stack format.
const lineCount = contents.split(/\n/).length
if (lineCount >= 1 && lineCount === contents.split(/ \d+\n/).length) {
if (lineCount >= 1 && lineCount === contents.split(/ \d+\r?\n/).length) {
console.log('Importing as collapsed stack format')
return toGroup(importFromBGFlameGraph(contents))
}
+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))
}
}
+8 -8
View File
@@ -521,10 +521,9 @@ export class CallTreeProfileBuilder extends Profile {
private framesInStack = new Map<Frame, number>()
private stack: Frame[] = []
private lastValue: number | null = null
private lastValue: number = 0
private addWeightsToFrames(value: number) {
if (this.lastValue == null) this.lastValue = value
const delta = value - this.lastValue!
const delta = value - this.lastValue
for (let frame of this.framesInStack.keys()) {
frame.addToTotalWeight(delta)
}
@@ -534,7 +533,7 @@ export class CallTreeProfileBuilder extends Profile {
}
}
private addWeightsToNodes(value: number, stack: CallTreeNode[]) {
const delta = value - this.lastValue!
const delta = value - this.lastValue
for (let node of stack) {
node.addToTotalWeight(delta)
}
@@ -552,14 +551,15 @@ export class CallTreeProfileBuilder extends Profile {
if (prevTop) {
if (useAppendOrder) {
const delta = value - this.lastValue!
const delta = value - this.lastValue
if (delta > 0) {
this.samples.push(prevTop)
this.weights.push(value - this.lastValue!)
this.weights.push(value - this.lastValue)
} else if (delta < 0) {
throw new Error(
`Samples must be provided in increasing order of cumulative value. Last sample was ${this
.lastValue!}, this sample was ${value}`,
`Samples must be provided in increasing order of cumulative value. Last sample was ${
this.lastValue
}, this sample was ${value}`,
)
}
}
+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
}
+4 -2
View File
@@ -7,7 +7,8 @@ describe('TimeFormatter', () => {
expect(f.format(0.04)).toEqual('40.00µs')
expect(f.format(3)).toEqual('3.00ms')
expect(f.format(2070)).toEqual('2.07s')
expect(f.format(1203123)).toEqual('20.05min')
expect(f.format(150000)).toEqual('2:30')
expect(f.format(1203123)).toEqual('20:03')
})
test('input units seconds', () => {
@@ -15,7 +16,8 @@ describe('TimeFormatter', () => {
expect(f.format(0.00004)).toEqual('40.00µs')
expect(f.format(0.003)).toEqual('3.00ms')
expect(f.format(2.07)).toEqual('2.07s')
expect(f.format(1203.123)).toEqual('20.05min')
expect(f.format(150)).toEqual('2:30')
expect(f.format(1203.123)).toEqual('20:03')
})
})
+6 -1
View File
@@ -1,4 +1,5 @@
import {FileFormat} from './file-format-spec'
import {zeroPad} from './utils'
export interface ValueFormatter {
unit: FileFormat.ValueUnit
@@ -25,7 +26,11 @@ export class TimeFormatter implements ValueFormatter {
formatUnsigned(v: number) {
const s = v * this.multiplier
if (s / 60 >= 1) return `${(s / 60).toFixed(2)}min`
if (s / 60 >= 1) {
const minutes = Math.floor(s / 60)
const seconds = Math.floor(s - minutes * 60).toString()
return `${minutes}:${zeroPad(seconds, 2)}`
}
if (s / 1 >= 1) return `${s.toFixed(2)}s`
if (s / 1e-3 >= 1) return `${(s / 1e-3).toFixed(2)}ms`
if (s / 1e-6 >= 1) return `${(s / 1e-6).toFixed(2)}µs`
+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))
},
}