Compare commits

...
23 Commits
Author SHA1 Message Date
Jamie Wong 30ca6291ca 1.5.0 2019-02-17 18:38:17 -08:00
Jamie Wong 66a9e5d1cf Support importing unterminated JSON in simple cases (#208)
This PR introduces support for importing JSON based profiles that are missing a terminating `]` (and possibly have an extraneous `,`).

This is similar to #202, but takes a much more targeted and simple approach.

I'm confident that this approach is sufficient because this is exactly what `chrome://tracing` does: https://github.com/catapult-project/catapult/blob/27e047e0494df162022be6aa8a8862742a270232/tracing/tracing/extras/importer/trace_event_importer.html#L197-L208

Fixes #204
2019-02-17 18:32:26 -08:00
Jamie Wong a2022a07a2 Fix crash when importing from stackprof without raw_timestamp_deltas (#207)
Fixes #200
2019-02-17 18:30:02 -08:00
Jamie Wong b7806a1c5f Alert instead of crash when successfully importing a file containing no profiles (#205)
Fixes #169
2019-02-17 17:46:24 -08:00
Jamie Wong 7f19a13012 Support importing multithreaded profiles from Chrome 66 (#206)
In #194, I added code to support import of multithreaded profiles from Chrome 70. I'm now doing some profiling work on an older version of Android chrome, and it seems like the profile objects don't yet have `id` properties. Instead, we should try using the `pid/tid` pair to identify profiles when the `id` field is absent.

This was tested against a profile import from Android Chrome 66.
2019-02-17 17:46:09 -08:00
Archerlly abd74be9fa add default instruments selected run number (#203)
this's will lack `com.apple.xray.owner.template` in instruments archive data where run instruments with command line.
like:
1. run`instruments -t Template.tracetemplate -D demo.trace -l 10000 -w  test.app`
2. drag `demo.trace` into `https://www.speedscope.app`
3. alert `Unrecognized format! See documentation about supported formats`
2019-02-17 17:45:51 -08:00
Jamie Wong c706bdfe04 Revert "Support importing partial JSON files (#202)"
This reverts commit cfc8fe8f6e.
2019-02-08 18:33:30 -08:00
Marcin Kolny cfc8fe8f6e Support importing partial JSON files (#202)
Partial files are allowed in many specs, e.g. Trace Event Format,
so the viewer should be able to load partial files as well.
2019-02-08 18:08:51 -08:00
Jamie Wong ddc61302e8 1.4.1 2019-01-22 21:53:09 -08:00
Jamie Wong 864c065053 Fix importing of Trace Event Format files with no ts field on M events (#198)
The spec for the Trace Event Format technically requires that all entries have "ts" values, and they do in the profiles recorded using chrome://tracing. We don't actually use those values in the case of "M" (metadata) events, however, and they're semantically meaningless as far as I can tell, so let's stop requiring them.

This allows the files that @aras-p provided in #77 to import successfully.

Fixes #77
2019-01-22 21:51:23 -08:00
Jamie Wong 7cca1a76bc 1.4.0 2019-01-22 12:03:31 -08:00
Jamie Wong 8c574d1c92 Support basic import of profiles in the "Trace Event Format" (#197)
This PR implements basic import of profiles from the "Trace Event Format", which is used by `chrome://tracing`, but also which many other tools target as a convenient event tracing format. The spec can be found here: https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.xqopa5m0e28f.

The standard supports a broad set of events, some of which don't yet have any practical way to visualize them in speedscope. This PR implements support for the `B`, `E`, and `X` events, as well as gathering process and thread names via some of the `M` events.

This work was motivated by a generous donation to /dev/color by @aras-p: https://github.com/jlfwong/speedscope/issues/77#issuecomment-455077014

Fixes #77
2019-01-21 20:49:25 -08:00
Jamie Wong fbc8946caa Update CHANGELOG.md 2018-12-04 12:05:59 -08:00
vmarchaud 8cddf3fe81 Import v8 cpu profile (old format) (#177)
As said on #170, i added the support for the old format used by https://github.com/hyj1991/v8-profiler-node8 (which is currently used for pm2.io).
2018-12-04 12:05:19 -08:00
Jamie Wong c15ca263d3 Update CHANGELOG.md 2018-12-03 19:29:25 -08:00
Jamie Wong 519847b489 1.3.2 2018-12-03 19:25:54 -08:00
Jamie Wong 6d4f3499da Fix import of multithreaded Chrome profiles (#194)
In #160, I wrote code which incorrectly assumed that at most one profile would be active at a time. It turns out this assumption is incorrect because of webworkers! This PR introduces a fix which correctly separates samples taken on the main thread from samples taken on worker threads, and allows viewing both in speedscope.

Fixes #171
2018-12-03 19:21:59 -08:00
Jamie Wong ad49dacb29 1.3.1 2018-11-08 10:05:52 -08:00
Jamie Wong 6562fec7a7 Use TextDecoder if available for converting from an ArrayBuffer for speed (#188)
#165 introduced a performance regression by using a really inefficient method for converting from array buffers into string. This should ix it by using `TextDecoder` instead.
2018-11-08 10:01:05 -08:00
Jamie Wong 23d4042e04 1.3.0 2018-10-29 09:56:06 -07:00
Vincent Rischmann 9961ed8295 Make the wasd keymappings work on azerty keyboards (#184)
Instead of using `key`, use `code` which according to [this](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code) should work consistently for different layouts.

I tested the modification on a french AZERTY keyboard and it works fine.
2018-10-29 09:43:10 -07:00
Tristan Hume e35335fe3c Haskell GHC JSON format support (fixes #182) (#183)
Fixes #182 by adding support for importing the JSON profiling format created by GHC's built in profiling support when the executable is passed the `-pj` option. Produces a profile group containing both a time and allocation profile.

Unfortunately, GHC doesn't provide the raw sample information to get the time view to be useful, so only left heavy and sandwich are useful.

Includes a test profile, and I've also tested it on a more real large 2MB profile file in the UI and it works great.

I also modified the Readme to link to a wiki page I'm unable to create, but that should have something like this content copy-pasted into it:

# Importing from Haskell

GHC provides built in profiling support that can export a JSON file.
In order to do this you need to compile your executable with profiling
support and then pass the `-pj` RTS flag to the executable.

This will produce a `my-binary.prof` file in the current directory which
you can import into speedscope.

## Using GHC

See the [GHC manual page on profiling](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/profiling.html)
for more extensive information on the command line flags available.

```
$ ghc -prof -fprof-auto -rtsopts Main.hs
$ ./Main +RTS -pj -RTS
```

## Using Stack

### With executables

```
$ stack build --profile
$ stack exec -- my-executable +RTS -pj -RTS
```

### With tests

```
stack test --profile --test-arguments "+RTS -pj -RTS"
```
2018-10-29 09:37:11 -07:00
Florian Hermouet-Joscht 86f4ba636d Use arrayBuffer instead of text for profileURL (#179)
When using #profileURL, some binary characters cannot be read if we use `fetch text`. So I changed that to use `arrayBuffer`.

Now we can read pprof protobuf files and normal JSON files instead of only text files.
2018-10-12 17:22:45 -07:00
35 changed files with 15766 additions and 39 deletions
+49
View File
@@ -1,5 +1,54 @@
## Unreleased
## [1.5.0] - 2019-02-17
### Added
* Support importing unterminated JSON in simple cases (#208) (by @jlfwong)
### Fixed
* Fix crash when importing from stackpro without raw_timestamp_deltas (#207) (by @jlfwong)
* Alert instead of crash when importing a file containing no profiles (#205) (by @jlfwong)
* Fixed import of multithreaded profiles from Chrome 66 (#206) (by @jlfwong)
* Fixed import of instruments trace files with missing run number (#203) (by @Archerlly)
## [1.4.1] - 2019-01-22
### Fixed
* Fix importing of Trace Event Format files with no ts field on M events [#198] (by @jlfwong)
## [1.4.0] - 2019-01-22
### Added
* Import v8 cpu profile (old format) [#177] (by @vmarchaud)
* Import basic "Trace Event Format" profiles [#197] (by @jlfwong)
## [1.3.2] - 2018-12-03
### Fixed
* Fixed import of multithreaded Chrome profiles [#19] (by @jlfwong)
## [1.3.1] - 2018-11-08
### Fixed
* Fixed a file import performance regression by using TextDecoder [#188] (by @jlfwong)
## [1.3.0] - 2018-10-29
### Added
* Support import from Haskell GHC JSON format support [#183] (by @trishume)
### Fixed
* Make the wasd keymappings work on azerty keyboards [#184] (by @vrischmann)
* Fix import of binary formats via profileURL [#179] (by @f-hj)
## [1.2.0] - 2018-10-08
### Added
+1
View File
@@ -45,6 +45,7 @@ speedscope is designed to ingest profiles from a variety of different profilers
- 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)
- [Importing from GHC (Haskell)](https://github.com/jlfwong/speedscope/wiki/Importing-from-Haskell)
- [Importing from custom sources](https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources)
Contributions to add support for additional formats are welcome! See issues with the ["import source" tag](https://github.com/jlfwong/speedscope/issues?q=is%3Aissue+is%3Aopen+label%3A%22import+source%22).
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "1.2.0",
"version": "1.5.0",
"description": "",
"repository": "jlfwong/speedscope",
"main": "index.js",
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"head":{"functionName":"(root)","url":"","lineNumber":-1,"bailoutReason":"","id":1,"scriptId":0,"hitCount":0,"children":[{"functionName":"","url":"","lineNumber":0,"callUID":1,"bailoutReason":"","id":2,"scriptId":0,"hitCount":0,"children":[{"functionName":"a","url":"","lineNumber":0,"callUID":2,"bailoutReason":"","id":3,"scriptId":0,"hitCount":0,"children":[{"functionName":"b","url":"","lineNumber":5,"callUID":3,"bailoutReason":"","id":4,"scriptId":0,"hitCount":0,"children":[{"functionName":"d","url":"","lineNumber":13,"callUID":4,"bailoutReason":"","id":5,"scriptId":0,"hitCount":14,"children":[]}]},{"functionName":"c","url":"","lineNumber":9,"callUID":3,"bailoutReason":"","id":6,"scriptId":0,"hitCount":0,"children":[{"functionName":"d","url":"","lineNumber":13,"callUID":6,"bailoutReason":"","id":7,"scriptId":0,"hitCount":14,"children":[]}]}]}]}]},"startTime":163140,"endTime":163140,"samples":[2,5,5,5,5,5,5,5,5,5,5,5,5,5,5,7,7,7,7,7,7,7,7,7,7,7,7,7,7],"timestamps":[163140599286,163140610861,163140611921,163140612966,163140614236,163140615507,163140616694,163140617968,163140619238,163140620258,163140621507,163140622765,163140624037,163140625303,163140626378,163140627649,163140628923,163140630191,163140631457,163140632746,163140634032,163140635304,163140636440,163140637716,163140638990,163140640255,163140641520,163140642791,163140644063,163140645206]}
@@ -0,0 +1,22 @@
[
{"pid": 0, "tid": 0, "ph": "M", "name": "process_name", "args": {"name": "p0"}},
{"pid": 0, "tid": 0, "ph": "M", "name": "thread_name", "args": {"name": "p0t0"}},
{"pid": 0, "tid": 0, "ph": "X", "name": "alpha", "ts": 0, "dur": 1},
{"pid": 0, "tid": 1, "ph": "M", "name": "thread_name", "args": {"name": "p0t1"}},
{"pid": 0, "tid": 1, "ph": "X", "name": "beta", "ts": 0, "dur": 1},
{"pid": 1, "tid": 0, "ph": "M", "name": "process_name", "args": {"name": "p1"}},
{"pid": 1, "tid": 0, "ph": "M", "name": "thread_name", "args": {"name": "p1t0"}},
{"pid": 1, "tid": 0, "ph": "X", "name": "gamma", "ts": 0, "dur": 1},
{"pid": 1, "tid": 1, "ph": "M", "name": "thread_name", "args": {"name": "p1t1"}},
{"pid": 1, "tid": 1, "ph": "X", "name": "delta", "ts": 0, "dur": 1},
{"pid": 2, "tid": 0, "ph": "M", "name": "thread_name", "args": {"name": "p2t0"}},
{"pid": 2, "tid": 0, "ph": "X", "name": "epsilon", "ts": 0, "dur": 1},
{"pid": 2, "tid": 1, "ph": "M", "name": "thread_name", "args": {"name": "p2t1"}},
{"pid": 2, "tid": 1, "ph": "X", "name": "phi", "ts": 0, "dur": 1},
{"pid": 3, "tid": 0, "ph": "M", "name": "process_name", "args": {"name": "p3"}},
{"pid": 3, "tid": 0, "ph": "X", "name": "zeta", "ts": 0, "dur": 1},
{"pid": 3, "tid": 1, "ph": "X", "name": "eta", "ts": 0, "dur": 1}
]
@@ -0,0 +1,9 @@
{
"traceEvents": [
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
{"pid": 0, "tid": 0, "ph": "X", "ts": 7, "tdur": 4},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14}
]
}
@@ -0,0 +1,7 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
{"pid": 0, "tid": 0, "ph": "X", "name": "gamma", "ts": 2, "dur": 5, "args": {"detail": "foobar"}},
{"pid": 0, "tid": 0, "ph": "X", "name": "epsilon", "ts": 7, "tdur": 4},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14},
@@ -0,0 +1,13 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
{"pid": 0, "tid": 0, "ph": "X", "name": "gamma", "ts": 2, "dur": 5, "args": {"detail": "foobar"}},
{"pid": 0, "tid": 0, "ph": "X", "name": "epsilon", "ts": 7, "tdur": 4},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14},
@@ -0,0 +1,7 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
{"pid": 0, "tid": 0, "ph": "X", "name": "gamma", "ts": 2, "dur": 5, "args": {"detail": "foobar"}},
{"pid": 0, "tid": 0, "ph": "X", "name": "epsilon", "ts": 7, "tdur": 4},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14}
+8
View File
@@ -0,0 +1,8 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
{"pid": 0, "tid": 0, "ph": "X", "name": "gamma", "ts": 2, "dur": 5, "args": {"detail": "foobar"}},
{"pid": 0, "tid": 0, "ph": "X", "name": "epsilon", "ts": 7, "tdur": 4},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14}
]
@@ -0,0 +1,60 @@
/*
* This script is used to reconstruct an cpu profile from chrome with an old format
* still used in nodejs with v8-profiler
*/
const fs = require('fs')
const data = JSON.parse(fs.readFileSync('../../profiles/Chrome/65/simple.cpuprofile').toString())
const _convertTimeDeltas = (profile) => {
if (!profile.timeDeltas) return null
let lastTimeUsec = profile.startTime
const timestamps = new Array(profile.timeDeltas.length + 1)
for (let i = 0; i < profile.timeDeltas.length; ++i) {
timestamps[i] = lastTimeUsec
lastTimeUsec += profile.timeDeltas[i]
}
timestamps[profile.timeDeltas.length] = lastTimeUsec
return timestamps
}
const reformatNode = node => {
if (!node.children) node.children = []
node.children = node.children.map(childID => {
if (typeof childID !== 'number') return childID
const childNode = data.nodes.find(node => node.id === childID)
if (typeof childNode !== 'object') return null
childNode.callUID = node.id
return childNode
})
return {
functionName: node.callFrame.functionName,
url: node.callFrame.url,
lineNumber: node.callFrame.lineNumber,
callUID: node.callUID,
bailoutReason: '',
id: node.id,
scriptId: 0,
hitCount: node.hitCount,
children: node.children.map(reformatNode)
}
}
// reformat then only keep the root as top level node
const nodes = data.nodes
.map(reformatNode)
.filter(node => node.functionName === '(root)')[0]
// since it can be undefined, create an array so execution still works
if (!data.timeDeltas) {
data.timeDeltas = []
}
fs.writeFileSync('./new.cpuprofile', JSON.stringify({
head: nodes,
startTime: Math.floor(data.startTime / 1000000),
endTime: Math.floor(data.endTime / 1000000),
samples: data.samples,
timestamps: _convertTimeDeltas(data)
}))
+28
View File
@@ -0,0 +1,28 @@
<script>
function banana() {
let prod = 1
for (let i = 1; i < 1000; i++) {
prod *= i
}
return prod
}
function apple() {
for (let i = 0; i < 1000; i++) {
banana()
}
}
let bounces = 0
const worker = new Worker('./worker.js')
worker.onmessage = function() {
apple()
if (bounces++ < 10) {
worker.postMessage('ping')
}
}
worker.postMessage('ping')
</script>
+18
View File
@@ -0,0 +1,18 @@
function gamma() {
let prod = 1
for (let i = 1; i < 1000; i++) {
prod *= i
}
return prod
}
function alpha() {
for (let i = 0; i < 1000; i++) {
gamma()
}
}
onmessage = function(e) {
alpha()
postMessage('pong')
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,864 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importFromHaskell 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": 144,
"line": undefined,
"name": "MAIN.MAIN",
"selfWeight": 0,
"totalWeight": 798,
},
Frame {
"col": undefined,
"file": undefined,
"key": 56,
"line": undefined,
"name": "GHC.Conc.Signal.CAF",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 73,
"line": undefined,
"name": "GHC.IO.Encoding.CAF",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 75,
"line": undefined,
"name": "GHC.IO.Encoding.Iconv.CAF",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 84,
"line": undefined,
"name": "GHC.IO.Handle.FD.CAF",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 119,
"line": undefined,
"name": "Text.Printf.CAF",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 26,
"line": undefined,
"name": "Main.CAF:eta1_r6nI",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "src/Main.hs:(21,1)-(38,42)",
"key": 19,
"line": undefined,
"name": "Main.main",
"selfWeight": 0,
"totalWeight": 733,
},
Frame {
"col": undefined,
"file": "src/Main.hs:(56,1)-(57,42)",
"key": 4,
"line": undefined,
"name": "Main.check",
"selfWeight": 320,
"totalWeight": 320,
},
Frame {
"col": undefined,
"file": "src/Main.hs:31:9-29",
"key": 16,
"line": undefined,
"name": "Main.main.long",
"selfWeight": 0,
"totalWeight": 3,
},
Frame {
"col": undefined,
"file": undefined,
"key": 28,
"line": undefined,
"name": "Main.CAF:eta_r6nG",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 35,
"line": undefined,
"name": "Main.CAF:io1",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 27,
"line": undefined,
"name": "Main.CAF:lvl2_r6nH",
"selfWeight": 0,
"totalWeight": 3,
},
Frame {
"col": undefined,
"file": "src/Main.hs:(61,1)-(63,26)",
"key": 3,
"line": undefined,
"name": "Main.make",
"selfWeight": 406,
"totalWeight": 409,
},
Frame {
"col": undefined,
"file": "src/Main.hs:63:19-26",
"key": 1,
"line": undefined,
"name": "Main.make.d2",
"selfWeight": 2,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": "src/Main.hs:63:9-16",
"key": 2,
"line": undefined,
"name": "Main.make.i2",
"selfWeight": 1,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": undefined,
"key": 24,
"line": undefined,
"name": "Main.CAF:lvl4_r6nL",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 22,
"line": undefined,
"name": "Main.CAF:lvl8_r6nP",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 21,
"line": undefined,
"name": "Main.CAF:main1",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 29,
"line": undefined,
"name": "Main.CAF:main11",
"selfWeight": 0,
"totalWeight": 4,
},
Frame {
"col": undefined,
"file": "src/Main.hs:27:9-35",
"key": 15,
"line": undefined,
"name": "Main.main.c",
"selfWeight": 0,
"totalWeight": 8,
},
Frame {
"col": undefined,
"file": undefined,
"key": 30,
"line": undefined,
"name": "Main.CAF:main12",
"selfWeight": 0,
"totalWeight": 4,
},
Frame {
"col": undefined,
"file": undefined,
"key": 23,
"line": undefined,
"name": "Main.CAF:main5",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 31,
"line": undefined,
"name": "Main.CAF:main7",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 25,
"line": undefined,
"name": "Main.CAF:main9",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:23:9-12",
"key": 33,
"line": undefined,
"name": "Main.CAF:main_maxN",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:23:9-35",
"key": 13,
"line": undefined,
"name": "Main.main.maxN",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:22:9",
"key": 34,
"line": undefined,
"name": "Main.CAF:main_n",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:22:9-14",
"key": 12,
"line": undefined,
"name": "Main.main.n",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:24:9-16",
"key": 32,
"line": undefined,
"name": "Main.CAF:main_stretchN",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:24:9-27",
"key": 14,
"line": undefined,
"name": "Main.main.stretchN",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:17:1-4",
"key": 36,
"line": undefined,
"name": "Main.CAF:minN",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:17:1-8",
"key": 9,
"line": undefined,
"name": "Main.minN",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 146,
"line": undefined,
"name": "GC.GC",
"selfWeight": 46,
"totalWeight": 46,
},
Frame {
"col": undefined,
"file": undefined,
"key": 147,
"line": undefined,
"name": "PROFILING.OVERHEAD_of",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 145,
"line": undefined,
"name": "SYSTEM.SYSTEM",
"selfWeight": 19,
"totalWeight": 19,
},
Frame {
"col": undefined,
"file": "src/Main.hs:35:26-54",
"key": 18,
"line": undefined,
"name": "Main.main.\\\\",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:19:1-56",
"key": 8,
"line": undefined,
"name": "Main.io",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:34:9-28",
"key": 17,
"line": undefined,
"name": "Main.main.vs",
"selfWeight": 0,
"totalWeight": 721,
},
Frame {
"col": undefined,
"file": "src/Main.hs:(42,1)-(45,38)",
"key": 11,
"line": undefined,
"name": "Main.depth",
"selfWeight": 0,
"totalWeight": 721,
},
Frame {
"col": undefined,
"file": "src/Main.hs:(49,1)-(52,31)",
"key": 7,
"line": undefined,
"name": "Main.sumT",
"selfWeight": 0,
"totalWeight": 721,
},
Frame {
"col": undefined,
"file": "src/Main.hs:51:9-31",
"key": 6,
"line": undefined,
"name": "Main.sumT.a",
"selfWeight": 3,
"totalWeight": 369,
},
Frame {
"col": undefined,
"file": "src/Main.hs:52:9-31",
"key": 5,
"line": undefined,
"name": "Main.sumT.b",
"selfWeight": 1,
"totalWeight": 352,
},
Frame {
"col": undefined,
"file": "src/Main.hs:45:9-38",
"key": 10,
"line": undefined,
"name": "Main.depth.n",
"selfWeight": 0,
"totalWeight": 0,
},
],
"name": "binary-trees time",
"stacks": Array [
"MAIN.MAIN;Main.CAF:eta1_r6nI;Main.main;Main.check 1.00ms",
"MAIN.MAIN;Main.CAF:lvl2_r6nH;Main.main;Main.main.long;Main.make 3.00ms",
"MAIN.MAIN;Main.CAF:main11;Main.main;Main.main.c;Main.check 4.00ms",
"MAIN.MAIN;Main.CAF:main12;Main.main;Main.main.c;Main.make 4.00ms",
"MAIN.MAIN;GC.GC 46.00ms",
"MAIN.MAIN;SYSTEM.SYSTEM 19.00ms",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.a;Main.check 153.00ms",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.a;Main.make;Main.make.d2 2.00ms",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.a;Main.make;Main.make.i2 1.00ms",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.a;Main.make 210.00ms",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.a 3.00ms",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.b;Main.check 162.00ms",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.b;Main.make 189.00ms",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.b 1.00ms",
],
}
`;
exports[`importFromHaskell 2`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": 144,
"line": undefined,
"name": "MAIN.MAIN",
"selfWeight": 648,
"totalWeight": 1921672664,
},
Frame {
"col": undefined,
"file": undefined,
"key": 56,
"line": undefined,
"name": "GHC.Conc.Signal.CAF",
"selfWeight": 640,
"totalWeight": 640,
},
Frame {
"col": undefined,
"file": undefined,
"key": 73,
"line": undefined,
"name": "GHC.IO.Encoding.CAF",
"selfWeight": 2768,
"totalWeight": 2768,
},
Frame {
"col": undefined,
"file": undefined,
"key": 75,
"line": undefined,
"name": "GHC.IO.Encoding.Iconv.CAF",
"selfWeight": 200,
"totalWeight": 200,
},
Frame {
"col": undefined,
"file": undefined,
"key": 84,
"line": undefined,
"name": "GHC.IO.Handle.FD.CAF",
"selfWeight": 34704,
"totalWeight": 34704,
},
Frame {
"col": undefined,
"file": undefined,
"key": 119,
"line": undefined,
"name": "Text.Printf.CAF",
"selfWeight": 528,
"totalWeight": 528,
},
Frame {
"col": undefined,
"file": undefined,
"key": 26,
"line": undefined,
"name": "Main.CAF:eta1_r6nI",
"selfWeight": 0,
"totalWeight": 2097152,
},
Frame {
"col": undefined,
"file": "src/Main.hs:(21,1)-(38,42)",
"key": 19,
"line": undefined,
"name": "Main.main",
"selfWeight": 32,
"totalWeight": 1921591544,
},
Frame {
"col": undefined,
"file": "src/Main.hs:(56,1)-(57,42)",
"key": 4,
"line": undefined,
"name": "Main.check",
"selfWeight": 406149056,
"totalWeight": 406149056,
},
Frame {
"col": undefined,
"file": "src/Main.hs:31:9-29",
"key": 16,
"line": undefined,
"name": "Main.main.long",
"selfWeight": 32,
"totalWeight": 7864112,
},
Frame {
"col": undefined,
"file": undefined,
"key": 28,
"line": undefined,
"name": "Main.CAF:eta_r6nG",
"selfWeight": 1096,
"totalWeight": 1096,
},
Frame {
"col": undefined,
"file": undefined,
"key": 35,
"line": undefined,
"name": "Main.CAF:io1",
"selfWeight": 1888,
"totalWeight": 1888,
},
Frame {
"col": undefined,
"file": undefined,
"key": 27,
"line": undefined,
"name": "Main.CAF:lvl2_r6nH",
"selfWeight": 0,
"totalWeight": 7864112,
},
Frame {
"col": undefined,
"file": "src/Main.hs:(61,1)-(63,26)",
"key": 3,
"line": undefined,
"name": "Main.make",
"selfWeight": 1512575520,
"totalWeight": 1512575520,
},
Frame {
"col": undefined,
"file": "src/Main.hs:63:19-26",
"key": 1,
"line": undefined,
"name": "Main.make.d2",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:63:9-16",
"key": 2,
"line": undefined,
"name": "Main.make.i2",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 24,
"line": undefined,
"name": "Main.CAF:lvl4_r6nL",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 22,
"line": undefined,
"name": "Main.CAF:lvl8_r6nP",
"selfWeight": 520,
"totalWeight": 520,
},
Frame {
"col": undefined,
"file": undefined,
"key": 21,
"line": undefined,
"name": "Main.CAF:main1",
"selfWeight": 16,
"totalWeight": 16,
},
Frame {
"col": undefined,
"file": undefined,
"key": 29,
"line": undefined,
"name": "Main.CAF:main11",
"selfWeight": 0,
"totalWeight": 4194304,
},
Frame {
"col": undefined,
"file": "src/Main.hs:27:9-35",
"key": 15,
"line": undefined,
"name": "Main.main.c",
"selfWeight": 64,
"totalWeight": 19922736,
},
Frame {
"col": undefined,
"file": undefined,
"key": 30,
"line": undefined,
"name": "Main.CAF:main12",
"selfWeight": 0,
"totalWeight": 15728432,
},
Frame {
"col": undefined,
"file": undefined,
"key": 23,
"line": undefined,
"name": "Main.CAF:main5",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 31,
"line": undefined,
"name": "Main.CAF:main7",
"selfWeight": 880,
"totalWeight": 880,
},
Frame {
"col": undefined,
"file": undefined,
"key": 25,
"line": undefined,
"name": "Main.CAF:main9",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:23:9-12",
"key": 33,
"line": undefined,
"name": "Main.CAF:main_maxN",
"selfWeight": 0,
"totalWeight": 32,
},
Frame {
"col": undefined,
"file": "src/Main.hs:23:9-35",
"key": 13,
"line": undefined,
"name": "Main.main.maxN",
"selfWeight": 32,
"totalWeight": 32,
},
Frame {
"col": undefined,
"file": "src/Main.hs:22:9",
"key": 34,
"line": undefined,
"name": "Main.CAF:main_n",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:22:9-14",
"key": 12,
"line": undefined,
"name": "Main.main.n",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:24:9-16",
"key": 32,
"line": undefined,
"name": "Main.CAF:main_stretchN",
"selfWeight": 0,
"totalWeight": 32,
},
Frame {
"col": undefined,
"file": "src/Main.hs:24:9-27",
"key": 14,
"line": undefined,
"name": "Main.main.stretchN",
"selfWeight": 32,
"totalWeight": 32,
},
Frame {
"col": undefined,
"file": "src/Main.hs:17:1-4",
"key": 36,
"line": undefined,
"name": "Main.CAF:minN",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": "src/Main.hs:17:1-8",
"key": 9,
"line": undefined,
"name": "Main.minN",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 146,
"line": undefined,
"name": "GC.GC",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": 147,
"line": undefined,
"name": "PROFILING.OVERHEAD_of",
"selfWeight": 2496,
"totalWeight": 2496,
},
Frame {
"col": undefined,
"file": undefined,
"key": 145,
"line": undefined,
"name": "SYSTEM.SYSTEM",
"selfWeight": 34736,
"totalWeight": 34736,
},
Frame {
"col": undefined,
"file": "src/Main.hs:35:26-54",
"key": 18,
"line": undefined,
"name": "Main.main.\\\\",
"selfWeight": 2224,
"totalWeight": 46672,
},
Frame {
"col": undefined,
"file": "src/Main.hs:19:1-56",
"key": 8,
"line": undefined,
"name": "Main.io",
"selfWeight": 67912,
"totalWeight": 67912,
},
Frame {
"col": undefined,
"file": "src/Main.hs:34:9-28",
"key": 17,
"line": undefined,
"name": "Main.main.vs",
"selfWeight": 0,
"totalWeight": 1891637344,
},
Frame {
"col": undefined,
"file": "src/Main.hs:(42,1)-(45,38)",
"key": 11,
"line": undefined,
"name": "Main.depth",
"selfWeight": 1120,
"totalWeight": 1891637344,
},
Frame {
"col": undefined,
"file": "src/Main.hs:(49,1)-(52,31)",
"key": 7,
"line": undefined,
"name": "Main.sumT",
"selfWeight": 0,
"totalWeight": 1891636224,
},
Frame {
"col": undefined,
"file": "src/Main.hs:51:9-31",
"key": 6,
"line": undefined,
"name": "Main.sumT.a",
"selfWeight": 1397760,
"totalWeight": 945818112,
},
Frame {
"col": undefined,
"file": "src/Main.hs:52:9-31",
"key": 5,
"line": undefined,
"name": "Main.sumT.b",
"selfWeight": 1397760,
"totalWeight": 945818112,
},
Frame {
"col": undefined,
"file": "src/Main.hs:45:9-38",
"key": 10,
"line": undefined,
"name": "Main.depth.n",
"selfWeight": 0,
"totalWeight": 0,
},
],
"name": "binary-trees allocation",
"stacks": Array [
"MAIN.MAIN;GHC.Conc.Signal.CAF 640 B",
"MAIN.MAIN;GHC.IO.Encoding.CAF 2.70 KB",
"MAIN.MAIN;GHC.IO.Encoding.Iconv.CAF 200 B",
"MAIN.MAIN;GHC.IO.Handle.FD.CAF 33.89 KB",
"MAIN.MAIN;Text.Printf.CAF 528 B",
"MAIN.MAIN;Main.CAF:eta1_r6nI;Main.main;Main.check 2.00 MB",
"MAIN.MAIN;Main.CAF:eta1_r6nI;Main.main 32 B",
"MAIN.MAIN;Main.CAF:eta_r6nG 1.07 KB",
"MAIN.MAIN;Main.CAF:io1 1.84 KB",
"MAIN.MAIN;Main.CAF:lvl2_r6nH;Main.main;Main.main.long;Main.make 7.50 MB",
"MAIN.MAIN;Main.CAF:lvl2_r6nH;Main.main;Main.main.long 32 B",
"MAIN.MAIN;Main.CAF:lvl8_r6nP 520 B",
"MAIN.MAIN;Main.CAF:main1 16 B",
"MAIN.MAIN;Main.CAF:main11;Main.main;Main.main.c;Main.check 4.00 MB",
"MAIN.MAIN;Main.CAF:main11;Main.main;Main.main.c 32 B",
"MAIN.MAIN;Main.CAF:main12;Main.main;Main.main.c;Main.make 15.00 MB",
"MAIN.MAIN;Main.CAF:main12;Main.main;Main.main.c 32 B",
"MAIN.MAIN;Main.CAF:main7 880 B",
"MAIN.MAIN;Main.CAF:main_maxN;Main.main;Main.main.maxN 32 B",
"MAIN.MAIN;Main.CAF:main_stretchN;Main.main;Main.main.stretchN 32 B",
"MAIN.MAIN;PROFILING.OVERHEAD_of 2.44 KB",
"MAIN.MAIN;SYSTEM.SYSTEM 33.92 KB",
"MAIN.MAIN;Main.main;Main.main.\\\\;Main.io 43.41 KB",
"MAIN.MAIN;Main.main;Main.main.\\\\ 2.17 KB",
"MAIN.MAIN;Main.main;Main.io 22.91 KB",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.a;Main.check 190.67 MB",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.a;Main.make 710.00 MB",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.a 1.33 MB",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.b;Main.check 190.67 MB",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.b;Main.make 710.00 MB",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth;Main.sumT;Main.sumT.b 1.33 MB",
"MAIN.MAIN;Main.main;Main.main.vs;Main.depth 1.09 KB",
"MAIN.MAIN 648 B",
],
}
`;
exports[`importFromHaskell: indexToView 1`] = `0`;
exports[`importFromHaskell: profileGroup.name 1`] = `"binary-trees"`;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,435 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importTraceEvents multiprocess 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "p0 (pid 0), p0t0 (tid 0)",
"stacks": Array [
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents multiprocess 2`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "p0 (pid 0), p0t1 (tid 1)",
"stacks": Array [
"beta 1.00µs",
],
}
`;
exports[`importTraceEvents multiprocess 3`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "gamma",
"line": undefined,
"name": "gamma",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "p1 (pid 1), p1t0 (tid 0)",
"stacks": Array [
"gamma 1.00µs",
],
}
`;
exports[`importTraceEvents multiprocess 4`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "delta",
"line": undefined,
"name": "delta",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "p1 (pid 1), p1t1 (tid 1)",
"stacks": Array [
"delta 1.00µs",
],
}
`;
exports[`importTraceEvents multiprocess 5`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "epsilon",
"line": undefined,
"name": "epsilon",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "p2t0 (pid 2, tid 0)",
"stacks": Array [
"epsilon 1.00µs",
],
}
`;
exports[`importTraceEvents multiprocess 6`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "phi",
"line": undefined,
"name": "phi",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "p2t1 (pid 2, tid 1)",
"stacks": Array [
"phi 1.00µs",
],
}
`;
exports[`importTraceEvents multiprocess 7`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "zeta",
"line": undefined,
"name": "zeta",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "p3 (pid 3, tid 0)",
"stacks": Array [
"zeta 1.00µs",
],
}
`;
exports[`importTraceEvents multiprocess 8`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "eta",
"line": undefined,
"name": "eta",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "p3 (pid 3, tid 1)",
"stacks": Array [
"eta 1.00µs",
],
}
`;
exports[`importTraceEvents multiprocess: indexToView 1`] = `0`;
exports[`importTraceEvents multiprocess: profileGroup.name 1`] = `"multiprocess.json"`;
exports[`importTraceEvents partial json import 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 3,
"totalWeight": 12,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma {\\"detail\\":\\"foobar\\"}",
"line": undefined,
"name": "gamma {\\"detail\\":\\"foobar\\"}",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "epsilon",
"line": undefined,
"name": "epsilon",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma {\\"detail\\":\\"foobar\\"} 5.00µs",
"alpha;beta;epsilon 4.00µs",
"alpha;beta 2.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents partial json import trailing comma 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 3,
"totalWeight": 12,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma {\\"detail\\":\\"foobar\\"}",
"line": undefined,
"name": "gamma {\\"detail\\":\\"foobar\\"}",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "epsilon",
"line": undefined,
"name": "epsilon",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma {\\"detail\\":\\"foobar\\"} 5.00µs",
"alpha;beta;epsilon 4.00µs",
"alpha;beta 2.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents partial json import trailing comma: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import trailing comma: profileGroup.name 1`] = `"simple-partial-trailing-comma.json"`;
exports[`importTraceEvents partial json import whitespace padding 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 3,
"totalWeight": 12,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma {\\"detail\\":\\"foobar\\"}",
"line": undefined,
"name": "gamma {\\"detail\\":\\"foobar\\"}",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "epsilon",
"line": undefined,
"name": "epsilon",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma {\\"detail\\":\\"foobar\\"} 5.00µs",
"alpha;beta;epsilon 4.00µs",
"alpha;beta 2.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents partial json import whitespace padding: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import whitespace padding: profileGroup.name 1`] = `"simple-partial-whitespace.json"`;
exports[`importTraceEvents partial json import: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import: profileGroup.name 1`] = `"simple-partial.json"`;
exports[`importTraceEvents simple 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 3,
"totalWeight": 12,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma {\\"detail\\":\\"foobar\\"}",
"line": undefined,
"name": "gamma {\\"detail\\":\\"foobar\\"}",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "epsilon",
"line": undefined,
"name": "epsilon",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma {\\"detail\\":\\"foobar\\"} 5.00µs",
"alpha;beta;epsilon 4.00µs",
"alpha;beta 2.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents simple object 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 8,
"totalWeight": 12,
},
Frame {
"col": undefined,
"file": undefined,
"key": "(unnamed)",
"line": undefined,
"name": "(unnamed)",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 6.00µs",
"alpha;beta;(unnamed) 4.00µs",
"alpha;beta 2.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents simple object: indexToView 1`] = `0`;
exports[`importTraceEvents simple object: profileGroup.name 1`] = `"simple-object.json"`;
exports[`importTraceEvents simple: indexToView 1`] = `0`;
exports[`importTraceEvents simple: profileGroup.name 1`] = `"simple.json"`;
+12
View File
@@ -11,3 +11,15 @@ test('importFromChromeTimeline', async () => {
test('importFromChromeTimeline Chrome 69', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/69/simple.json')
})
test('importFromV8Profiler Node 10', async () => {
await checkProfileSnapshot('./sample/profiles/node/10.11.0/example.cpuprofile')
})
test('importFromChromeTimeline Workers Chrome 66', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/66/worker.json')
})
test('importFromChromeTimeline Workers Chrome 70', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/70/worker.json')
})
+65 -15
View File
@@ -1,6 +1,7 @@
import {Profile, FrameInfo, CallTreeProfileBuilder} from '../lib/profile'
import {getOrInsert, lastOf, sortBy} from '../lib/utils'
import {Profile, FrameInfo, CallTreeProfileBuilder, ProfileGroup} from '../lib/profile'
import {getOrInsert, lastOf, sortBy, itForEach} from '../lib/utils'
import {TimeFormatter} from '../lib/value-formatters'
import {chromeTreeToNodes, OldCPUProfile} from './v8cpuFormatter'
// See: https://github.com/v8/v8/blob/master/src/inspector/js_protocol.json
@@ -15,6 +16,7 @@ interface TimelineEvent {
tdur: number
tts: number
args: {[key: string]: any}
id?: string
}
interface PositionTickInfo {
@@ -30,7 +32,7 @@ interface CPUProfileCallFrame {
url: string
}
interface CPUProfileNode {
export interface CPUProfileNode {
callFrame: CPUProfileCallFrame
hitCount: number
id: number
@@ -39,7 +41,7 @@ interface CPUProfileNode {
parent?: CPUProfileNode
}
interface CPUProfile {
export interface CPUProfile {
startTime: number
endTime: number
nodes: CPUProfileNode[]
@@ -61,33 +63,53 @@ export function isChromeTimeline(rawProfile: any): boolean {
return true
}
export function importFromChromeTimeline(events: TimelineEvent[]): Profile {
export function importFromChromeTimeline(events: TimelineEvent[], fileName: string): ProfileGroup {
// It seems like sometimes Chrome timeline files contain multiple CpuProfiles?
// For now, choose the first one in the list.
let cpuProfile: CPUProfile | null = null
const cpuProfileByID = new Map<string, CPUProfile>()
// Maps profile IDs (like "0x3") to pid/tid pairs formatted as `${pid}:${tid}`
const pidTidById = new Map<string, string>()
// Maps pid/tid pairs to thread names
const threadNameByPidTid = new Map<string, string>()
// 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') {
cpuProfile = event.args.data.cpuProfile as CPUProfile
if (event.name === 'CpuProfile') {
const pidTid = `${event.pid}:${event.tid}`
const id = event.id || pidTid
cpuProfileByID.set(id, event.args.data.cpuProfile as CPUProfile)
pidTidById.set(id, pidTid)
}
if (event.name == 'Profile') {
cpuProfile = {
if (event.name === 'Profile') {
const pidTid = `${event.pid}:${event.tid}`
cpuProfileByID.set(event.id || pidTid, {
startTime: 0,
endTime: 0,
nodes: [],
samples: [],
timeDeltas: [],
...event.args.data,
})
if (event.id) {
pidTidById.set(event.id, `${event.pid}:${event.tid}`)
}
}
if (event.name == 'ProfileChunk') {
if (event.name === 'thread_name') {
threadNameByPidTid.set(`${event.pid}:${event.tid}`, event.args.name)
}
if (event.name === 'ProfileChunk') {
const pidTid = `${event.pid}:${event.tid}`
const cpuProfile = cpuProfileByID.get(event.id || pidTid)
if (cpuProfile) {
const chunk = event.args.data
if (chunk.cpuProfile) {
@@ -108,13 +130,36 @@ export function importFromChromeTimeline(events: TimelineEvent[]): Profile {
cpuProfile.endTime = chunk.endTime
}
} else {
console.log('Ignoring ProfileChunk when no Profile is active')
console.warn(`Ignoring ProfileChunk for undeclared Profile with id ${event.id || pidTid}`)
}
}
}
if (cpuProfile) {
return importFromChromeCPUProfile(cpuProfile as CPUProfile)
if (cpuProfileByID.size > 0) {
const profiles: Profile[] = []
let indexToView = 0
itForEach(cpuProfileByID.keys(), profileId => {
let threadName: string | null = null
let pidTid = pidTidById.get(profileId)
if (pidTid) {
threadName = threadNameByPidTid.get(pidTid) || null
if (threadName) {
}
}
const profile = importFromChromeCPUProfile(cpuProfileByID.get(profileId)!)
if (threadName && cpuProfileByID.size > 1) {
profile.setName(`${fileName} - ${threadName}`)
if (threadName === 'CrRendererMain') {
indexToView = profiles.length
}
} else {
profile.setName(`${fileName}`)
}
profiles.push(profile)
})
return {name: fileName, indexToView, profiles}
} else {
throw new Error('Could not find CPU profile in Timeline')
}
@@ -198,7 +243,8 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
} else {
let timeDelta = chromeProfile.timeDeltas[i + 1]
if (timeDelta < 0) {
console.warn('Substituting zero for unexpected time delta:', timeDelta, 'at index', i)
// This is super noisy, but can be helpful when debugging strange data
// console.warn('Substituting zero for unexpected time delta:', timeDelta, 'at index', i)
timeDelta = 0
}
@@ -264,3 +310,7 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
profile.setValueFormatter(new TimeFormatter('microseconds'))
return profile.build()
}
export function importFromOldV8CPUProfile(content: OldCPUProfile): Profile {
return importFromChromeCPUProfile(chromeTreeToNodes(content))
}
+5
View File
@@ -0,0 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importFromHaskell', async () => {
await checkProfileSnapshot('./sample/profiles/haskell/simple.prof')
})
+98
View File
@@ -0,0 +1,98 @@
import {ProfileGroup, FrameInfo, CallTreeProfileBuilder} from '../lib/profile'
import {TimeFormatter, ByteFormatter} from '../lib/value-formatters'
// See https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/profiling.html#json-profile-format
// for information on the GHC profiler JSON output format.
interface CostCentre {
id: number
label: string
module: string
src_loc: string
is_caf: boolean
}
interface ProfileTree {
id: number
entries: number
alloc: number
ticks: number
children: ProfileTree[]
}
interface HaskellProfile {
program: string
arguments: string[]
rts_arguments: string[]
end_time: string
initial_capabilities: number
total_time: number
total_ticks: number
tick_interval: number
total_alloc: number
cost_centres: CostCentre[]
profile: ProfileTree
}
// The profiler already collapses recursion before output so using the JS stack here should be fine
function addToProfile(
tree: ProfileTree,
startVal: number,
profile: CallTreeProfileBuilder,
infos: Map<number, FrameInfo>,
attribute: (tree: ProfileTree) => number,
): number {
// If the expression never did anything we don't care about it
if (tree.ticks === 0 && tree.entries === 0 && tree.alloc === 0 && tree.children.length === 0)
return startVal
let curVal = startVal
let frameInfo = infos.get(tree.id)!
profile.enterFrame(frameInfo, curVal)
for (let child of tree.children) {
curVal = addToProfile(child, curVal, profile, infos, attribute)
}
curVal += attribute(tree)
profile.leaveFrame(frameInfo, curVal)
return curVal
}
export function importFromHaskell(haskellProfile: HaskellProfile): ProfileGroup {
const idToFrameInfo = new Map<number, FrameInfo>()
for (let centre of haskellProfile.cost_centres) {
const frameInfo: FrameInfo = {
key: centre.id,
name: `${centre.module}.${centre.label}`,
}
// Ignore things like <entire-module> and <no location info>
if (!centre.src_loc.startsWith('<')) {
// This also contains line and column information, but sometimes it contains ranges,
// and in varying formats, so it's a better experience just to leave it as is
frameInfo.file = centre.src_loc
}
idToFrameInfo.set(centre.id, frameInfo)
}
const timeProfile = new CallTreeProfileBuilder(haskellProfile.total_ticks)
addToProfile(haskellProfile.profile, 0, timeProfile, idToFrameInfo, tree => tree.ticks)
timeProfile.setValueFormatter(new TimeFormatter('milliseconds'))
timeProfile.setName(`${haskellProfile.program} time`)
const allocProfile = new CallTreeProfileBuilder(haskellProfile.total_ticks)
addToProfile(haskellProfile.profile, 0, allocProfile, idToFrameInfo, tree => tree.alloc)
allocProfile.setValueFormatter(new ByteFormatter())
allocProfile.setName(`${haskellProfile.program} allocation`)
return {
name: haskellProfile.program,
indexToView: 0,
profiles: [timeProfile.build(), allocProfile.build()],
}
}
+4
View File
@@ -5,4 +5,8 @@ test('importProfileGroup', async () => {
expect(await importProfileGroupFromText('unknown', '')).toBe(null)
expect(await importProfileGroupFromText('unknown', 'Hello world')).toBe(null)
expect(await importProfileGroupFromText('unknown', 'Hello\n\nWorld')).toBe(null)
// Importing from a version of stackprof which was missing raw_timestamp_deltas should return null
const oldStackprof = `{"version":1.2,"mode":"wall","interval":1000,"samples":0,"gc_samples":0,"missed_samples":0,"frames":{}}`
expect(await importProfileGroupFromText('unknown', oldStackprof)).toBe(null)
})
+43 -10
View File
@@ -1,7 +1,12 @@
import {Profile, ProfileGroup} from '../lib/profile'
import {FileSystemDirectoryEntry} from './file-system-entry'
import {importFromChromeCPUProfile, importFromChromeTimeline, isChromeTimeline} from './chrome'
import {
importFromChromeCPUProfile,
importFromChromeTimeline,
isChromeTimeline,
importFromOldV8CPUProfile,
} from './chrome'
import {importFromStackprof} from './stackprof'
import {importFromInstrumentsDeepCopy, importFromInstrumentsTrace} from './instruments'
import {importFromBGFlameGraph} from './bg-flamegraph'
@@ -9,10 +14,12 @@ import {importFromFirefox} from './firefox'
import {importSpeedscopeProfiles} from '../lib/file-format'
import {importFromV8ProfLog} from './v8proflog'
import {importFromLinuxPerf} from './linux-tools-perf'
import {importFromHaskell} from './haskell'
import {ProfileDataSource, TextProfileDataSource, MaybeCompressedDataReader} from './utils'
import {importAsPprofProfile} from './pprof'
import {decodeBase64} from '../lib/utils'
import {importFromChromeHeapProfile} from './v8heapalloc'
import {isTraceEventFormatted, importTraceEvents} from './trace-event'
export async function importProfileGroupFromText(
fileName: string,
@@ -64,6 +71,26 @@ function toGroup(profile: Profile | null): ProfileGroup | null {
return {name: profile.getName(), indexToView: 0, profiles: [profile]}
}
function fixUpJSON(content: string): string {
// This code is similar to the code from here:
// https://github.com/catapult-project/catapult/blob/27e047e0494df162022be6aa8a8862742a270232/tracing/tracing/extras/importer/trace_event_importer.html#L197-L208
//
// If the event data begins with a [, then we know it should end with a ]. The
// reason we check for this is because some tracing implementations cannot
// guarantee that a ']' gets written to the trace file. So, we are forgiving
// and if this is obviously the case, we fix it up before throwing the string
// at JSON.parse.
//
content = content.trim()
if (content[0] === '[') {
content = content.replace(/,\s*$/, '')
if (content[content.length - 1] !== ']') {
content += ']'
}
}
return content
}
async function _importProfileGroup(dataSource: ProfileDataSource): Promise<ProfileGroup | null> {
const fileName = await dataSource.name()
@@ -83,12 +110,9 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
if (fileName.endsWith('.speedscope.json')) {
console.log('Importing as speedscope json file')
return importSpeedscopeProfiles(JSON.parse(contents))
} else if (fileName.endsWith('.cpuprofile')) {
console.log('Importing as Chrome CPU Profile')
return toGroup(importFromChromeCPUProfile(JSON.parse(contents)))
} else if (fileName.endsWith('.chrome.json') || /Profile-\d{8}T\d{6}/.exec(fileName)) {
console.log('Importing as Chrome Timeline')
return toGroup(importFromChromeTimeline(JSON.parse(contents)))
return importFromChromeTimeline(JSON.parse(contents), fileName)
} else if (fileName.endsWith('.stackprof.json')) {
console.log('Importing as stackprof profile')
return toGroup(importFromStackprof(JSON.parse(contents)))
@@ -112,7 +136,7 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
// Second pass: Try to guess what file format it is based on structure
let parsed: any
try {
parsed = JSON.parse(contents)
parsed = JSON.parse(fixUpJSON(contents))
} catch (e) {}
if (parsed) {
if (parsed['$schema'] === 'https://www.speedscope.app/file-format-schema.json') {
@@ -122,12 +146,18 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
console.log('Importing as Firefox profile')
return toGroup(importFromFirefox(parsed))
} 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) {
console.log('Importing as Chrome Timeline')
return importFromChromeTimeline(parsed, fileName)
} else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) {
console.log('Importing as Chrome CPU Profile')
return toGroup(importFromChromeCPUProfile(parsed))
} else if ('mode' in parsed && 'frames' in parsed) {
} else if (isTraceEventFormatted(parsed)) {
console.log('Importing as Trace Event Format profile')
return importTraceEvents(parsed)
} else if ('head' in parsed && 'samples' in parsed && 'timestamps' in parsed) {
console.log('Importing as Chrome CPU Profile (old format)')
return toGroup(importFromOldV8CPUProfile(parsed))
} else if ('mode' in parsed && 'frames' in parsed && 'raw_timestamp_deltas' in parsed) {
console.log('Importing as stackprof profile')
return toGroup(importFromStackprof(parsed))
} else if ('code' in parsed && 'functions' in parsed && 'ticks' in parsed) {
@@ -136,6 +166,9 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
} else if ('head' in parsed && 'selfSize' in parsed['head']) {
console.log('Importing as Chrome Heap Profile')
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
} else if ('rts_arguments' in parsed && 'initial_capabilities' in parsed) {
console.log('Importing as Haskell GHC JSON Profile')
return importFromHaskell(parsed)
}
} else {
// Format is not JSON
+3
View File
@@ -98,4 +98,7 @@ describe('importFromInstrumentsTrace', () => {
test('Instruments 9.3.1', async () => {
await importFromTrace('./sample/profiles/Instruments/9.3.1/simple-time-profile.trace.zip')
})
test('Instruments 10.0', async () => {
await importFromTrace('./sample/profiles/Instruments/10.0/simple-time-profile.trace.zip')
})
})
+4 -1
View File
@@ -361,7 +361,10 @@ async function readFormTemplate(tree: TraceDirectoryTree): Promise<FormTemplateD
const archive = readInstrumentsKeyedArchive(await readAsArrayBuffer(formTemplate))
const version = archive['com.apple.xray.owner.template.version']
const selectedRunNumber = archive['com.apple.xray.owner.template'].get('_selectedRunNumber')
let selectedRunNumber = 1
if ('com.apple.xray.owner.template' in archive) {
selectedRunNumber = archive['com.apple.xray.owner.template'].get('_selectedRunNumber')
}
let instrument = archive['$1']
if ('stubInfoByUUID' in archive) {
instrument = Array.from(archive['stubInfoByUUID'].keys())[0]
+25
View File
@@ -0,0 +1,25 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importTraceEvents simple', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple.json')
})
test('importTraceEvents simple object', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-object.json')
})
test('importTraceEvents multiprocess', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/multiprocess.json')
})
test('importTraceEvents partial json import', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial.json')
})
test('importTraceEvents partial json import trailing comma', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-trailing-comma.json')
})
test('importTraceEvents partial json import whitespace padding', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-whitespace.json')
})
+305
View File
@@ -0,0 +1,305 @@
import {sortBy, zeroPad} from '../lib/utils'
import {ProfileGroup, CallTreeProfileBuilder, FrameInfo} from '../lib/profile'
import {TimeFormatter} from '../lib/value-formatters'
// This file concerns import from the "Trace Event Format", authored by Google
// and used for Google's own chrome://trace.
//
// The file format is extremely general, and we only support the parts of it
// that logically map onto speedscope's visualization capabilities.
// Specifically, we only support the "B", "E", and "X" event types. Everything
// else is ignored. We do, however, support import of profiles that are
// multi-process/multi-threaded. Each process is split into a separate profile.
//
// Note that Chrome Developer Tools uses this format as well, but all the
// relevant data used in those profiles is stored in events with the name
// "CpuProfile", "Profile", or "ProfileChunk". If we detect those, we prioritize
// importing the profile as a Chrome Developer Tools profile. Otherwise,
// we try to import it as a "Trace Event Format" file.
//
// Spec: https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
interface TraceEvent {
// The process ID for the process that output this event.
pid: number
// The thread ID for the thread that output this event.
tid: number
// The event type. This is a single character which changes depending on the type of event being output. The valid values are listed in the table below. We will discuss each phase type below.
ph: string
// The tracing clock timestamp of the event. The timestamps are provided at microsecond granularity.
ts: number
// The thread clock timestamp of the event. The timestamps are provided at microsecond granularity.
tts?: number
// The name of the event, as displayed in Trace Viewer
name?: string
// The event categories. This is a comma separated list of categories for the event. The categories can be used to hide events in the Trace Viewer UI.
cat?: string
// Any arguments provided for the event. Some of the event types have required argument fields, otherwise, you can put any information you wish in here. The arguments are displayed in Trace Viewer when you view an event in the analysis section.
args: any
// A fixed color name to associate with the event. If provided, cname must be one of the names listed in trace-viewer's base color scheme's reserved color names list
cname?: string
}
interface BTraceEvent extends TraceEvent {
ph: 'B'
}
interface ETraceEvent extends TraceEvent {
ph: 'E'
}
interface XTraceEvent extends TraceEvent {
ph: 'X'
dur?: number
tdur?: number
}
// The trace format supports a number of event types that we ignore.
type ImportableTraceEvent = BTraceEvent | ETraceEvent | XTraceEvent
type DurationEvent = BTraceEvent | ETraceEvent
function filterIgnoredEventTypes(events: TraceEvent[]): ImportableTraceEvent[] {
const ret: ImportableTraceEvent[] = []
for (let ev of events) {
switch (ev.ph) {
case 'B':
case 'E':
case 'X':
ret.push(ev as ImportableTraceEvent)
}
}
return ret
}
function convertToDurationEvents(events: ImportableTraceEvent[]): DurationEvent[] {
const ret: DurationEvent[] = []
for (let ev of events) {
switch (ev.ph) {
case 'B':
ret.push(ev)
break
case 'E':
ret.push(ev)
break
case 'X':
let dur: number | null = null
if (ev.dur != null) dur = ev.dur
else if (ev.tdur != null) dur = ev.tdur
if (dur == null) {
console.warn('Found a complete event (X) with no duration. Skipping: ', ev)
continue
}
ret.push({...ev, ph: 'B'} as BTraceEvent)
ret.push({...ev, ph: 'E', ts: ev.ts + dur} as ETraceEvent)
break
default:
const _exhaustiveCheck: never = ev
return _exhaustiveCheck
}
}
return ret
}
function getProcessNamesByPid(events: TraceEvent[]): Map<number, string> {
const processNamesByPid = new Map<number, string>()
for (let ev of events) {
if (ev.ph === 'M' && ev.name === 'process_name' && ev.args && ev.args.name) {
processNamesByPid.set(ev.pid, ev.args.name)
}
}
return processNamesByPid
}
function getThreadNamesByPidTid(events: TraceEvent[]): Map<string, string> {
const threadNameByPidTid = new Map<string, string>()
for (let ev of events) {
if (ev.ph === 'M' && ev.name === 'thread_name' && ev.args && ev.args.name) {
const key = `${ev.pid}:${ev.tid}`
threadNameByPidTid.set(key, ev.args.name)
}
}
return threadNameByPidTid
}
function keyForEvent(event: TraceEvent): string {
let name = `${event.name || '(unnamed)'}`
if (event.args) {
name += ` ${JSON.stringify(event.args)}`
}
return name
}
function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
const profileByPidTid = new Map<string, CallTreeProfileBuilder>()
const importableEvents = filterIgnoredEventTypes(events)
const durationEvents = convertToDurationEvents(importableEvents)
const processNamesByPid = getProcessNamesByPid(events)
const threadNamesByPidTid = getThreadNamesByPidTid(events)
durationEvents.sort((a, b) => {
if (a.ts < b.ts) return -1
if (a.ts > b.ts) return 1
// We have to be careful with events that have the same timestamp
if (a.pid === b.pid && a.tid === b.tid) {
const aKey = keyForEvent(a)
const bKey = keyForEvent(b)
if (aKey === bKey) {
// If the two elements have the same key, we need to process the begin
// event before the end event. This will be a zero-duration event.
if (a.ph === 'B' && b.ph === 'E') return -1
if (a.ph === 'E' && b.ph === 'B') return 1
} else {
// If the two elements have *different* keys, we want to process
// the end of an event before the beginning of the event to prevent
// out-of-order push/pops from the callstack.
if (a.ph === 'B' && b.ph === 'E') return 1
if (a.ph === 'E' && b.ph === 'B') return -1
}
}
return -1
})
if (durationEvents.length > 0) {
const firstTs = durationEvents[0].ts
for (let ev of durationEvents) {
ev.ts -= firstTs
}
}
function getOrCreateProfile(pid: number, tid: number) {
// We zero-pad the PID and TID to make sorting them by pid/tid pair later easier.
const pidTid = `${zeroPad('' + pid, 10)}:${zeroPad('' + tid, 10)}`
let profile = profileByPidTid.get(pidTid)
if (profile != null) return profile
profile = new CallTreeProfileBuilder()
profile.setValueFormatter(new TimeFormatter('microseconds'))
profileByPidTid.set(pidTid, profile)
const processName = processNamesByPid.get(pid)
const threadName = threadNamesByPidTid.get(`${pid}:${tid}`)
if (processName != null && threadName != null) {
profile.setName(`${processName} (pid ${pid}), ${threadName} (tid ${tid})`)
} else if (processName != null) {
profile.setName(`${processName} (pid ${pid}, tid ${tid})`)
} else if (threadName != null) {
profile.setName(`${threadName} (pid ${pid}, tid ${tid})`)
} else {
profile.setName(`pid ${pid}, tid ${tid}`)
}
return profile
}
for (let ev of durationEvents) {
const profile = getOrCreateProfile(ev.pid, ev.tid)
const key = keyForEvent(ev)
const frameInfo: FrameInfo = {
key: key,
name: key,
}
switch (ev.ph) {
case 'B':
profile.enterFrame(frameInfo, ev.ts)
break
case 'E':
profile.leaveFrame(frameInfo, ev.ts)
break
default:
const _exhaustiveCheck: never = ev
return _exhaustiveCheck
}
}
// For now, we just sort processes by pid & tid.
// TODO: The standard specifies that metadata events with the name
// "process_sort_index" and "thread_sort_index" can be used to influence the
// order, but for simplicity we'll ignore that until someone complains :)
const profilePairs = Array.from(profileByPidTid.entries())
sortBy(profilePairs, p => p[0])
return {name: '', indexToView: 0, profiles: profilePairs.map(p => p[1])}
}
function isTraceEventList(maybeEventList: any): maybeEventList is TraceEvent[] {
if (!Array.isArray(maybeEventList)) return false
if (maybeEventList.length === 0) return false
// Both ph and ts should be provided for every event. In theory, many other
// fields are mandatory, but without these fields, we won't usefully be able
// to import the data, so we'll rely upon these.
for (let el of maybeEventList) {
if (!('ph' in el)) {
return false
}
switch (el.ph) {
case 'B':
case 'E':
case 'X':
// All B, E, and X events must have a timestamp specified, otherwise we
// won't be able to import correctly.
if (!('ts' in el)) {
return false
}
case 'M':
// It's explicitly okay for "M" (metadata) events not to specify a "ts"
// field, since usually there is no logical timestamp for them to have
break
}
}
return true
}
function isTraceEventObject(
maybeTraceEventObject: any,
): maybeTraceEventObject is {traceEvents: TraceEvent[]} {
if (!('traceEvents' in maybeTraceEventObject)) return false
return isTraceEventList(maybeTraceEventObject['traceEvents'])
}
export function isTraceEventFormatted(
rawProfile: any,
): rawProfile is {traceEvents: TraceEvent[]} | TraceEvent[] {
// We're only going to support the JSON formatted profiles for now.
// The spec also discusses support for data embedded in ftrace supported data: https://lwn.net/Articles/365835/.
return isTraceEventObject(rawProfile) || isTraceEventList(rawProfile)
}
export function importTraceEvents(
rawProfile: {traceEvents: TraceEvent[]} | TraceEvent[],
): ProfileGroup {
if (isTraceEventObject(rawProfile)) {
return eventListToProfileGroup(rawProfile.traceEvents)
} else if (isTraceEventList(rawProfile)) {
return eventListToProfileGroup(rawProfile)
} else {
const _exhaustiveCheck: never = rawProfile
return _exhaustiveCheck
}
}
+11 -6
View File
@@ -53,13 +53,18 @@ export class MaybeCompressedDataReader implements ProfileDataSource {
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])
if (typeof TextDecoder !== 'undefined') {
const decoder = new TextDecoder()
return decoder.decode(buffer)
} else {
// 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
}
return ret
}
static fromFile(file: File): MaybeCompressedDataReader {
+71
View File
@@ -0,0 +1,71 @@
import {CPUProfile, CPUProfileNode} from './chrome'
/**
* This importer handles an old format used by the C++ API of V8. This format is still used by v8-profiler-node8.
* There are two differences between the two formats:
* - Nodes are a tree in the old format and a flat array in the new format
* - Weights are timestamps in the old format and deltas in the new format.
*
* For more information, see https://github.com/hyj1991/v8-profiler-node8
*/
interface OldCPUProfileNode {
functionName: string
lineNumber: number
scriptId: string
url: string
hitCount: number
bailoutReason: string
id: number
children: OldCPUProfileNode[]
}
export interface OldCPUProfile {
startTime: number
endTime: number
head: OldCPUProfileNode
samples: number[]
timestamps: number[]
}
function treeToArray(root: OldCPUProfileNode): CPUProfileNode[] {
const nodes: CPUProfileNode[] = []
function visit(node: OldCPUProfileNode) {
nodes.push({
id: node.id,
callFrame: {
columnNumber: 0,
functionName: node.functionName,
lineNumber: node.lineNumber,
scriptId: node.scriptId,
url: node.url,
},
hitCount: node.hitCount,
children: node.children.map(child => child.id),
})
node.children.forEach(visit)
}
visit(root)
return nodes
}
function timestampsToDeltas(timestamps: number[], startTime: number): number[] {
return timestamps.map((timestamp, index) => {
const lastTimestamp = index === 0 ? startTime * 1000000 : timestamps[index - 1]
return timestamp - lastTimestamp
})
}
/**
* Convert the old tree-based format to the new flat-array based format
*/
export function chromeTreeToNodes(content: OldCPUProfile): CPUProfile {
// Note that both startTime and endTime are now in microseconds
return {
samples: content.samples,
startTime: content.startTime * 1000000,
endTime: content.endTime * 1000000,
nodes: treeToArray(content.head),
timeDeltas: timestampsToDeltas(content.timestamps, content.startTime),
}
}
+13 -2
View File
@@ -33,6 +33,13 @@ async function importProfilesFromBase64(
return (await importModule).importProfileGroupFromBase64(fileName, contents)
}
async function importProfilesFromArrayBuffer(
fileName: string,
contents: ArrayBuffer,
): Promise<ProfileGroup | null> {
return (await importModule).importProfilesFromArrayBuffer(fileName, contents)
}
async function importProfilesFromFile(file: File): Promise<ProfileGroup | null> {
return (await importModule).importProfilesFromFile(file)
}
@@ -303,11 +310,15 @@ export class Application extends StatelessComponent<ApplicationProps> {
return
}
// TODO(jlfwong): Make these into nicer overlays
if (profileGroup == null) {
// TODO(jlfwong): Make this a nicer overlay
alert('Unrecognized format! See documentation about supported formats.')
this.props.setLoading(false)
return
} else if (profileGroup.profiles.length === 0) {
alert("Successfully imported profile, but it's empty!")
this.props.setLoading(false)
return
}
if (this.props.hashParams.title) {
@@ -512,7 +523,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
if (filename.includes('/')) {
filename = filename.slice(filename.lastIndexOf('/') + 1)
}
return await importProfilesFromText(filename, await response.text())
return await importProfilesFromArrayBuffer(filename, await response.arrayBuffer())
})
} else if (this.props.hashParams.localProfilePath) {
// There isn't good cross-browser support for XHR of local files, even from
+9 -4
View File
@@ -660,15 +660,20 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
if (ev.ctrlKey || ev.shiftKey || ev.metaKey) return
// NOTE: We intentionally use ev.code rather than ev.key for
// WASD in order to have the keys retain the same layout even
// if the keyboard layout is not QWERTY.
//
// See: https://github.com/jlfwong/speedscope/pull/184
if (ev.key === '0') {
this.zoom(new Vec2(width / 2, height / 2), 1e9)
} else if (ev.key === 'ArrowRight' || ev.key === 'd') {
} else if (ev.key === 'ArrowRight' || ev.code === 'KeyD') {
this.pan(new Vec2(100, 0))
} else if (ev.key === 'ArrowLeft' || ev.key === 'a') {
} else if (ev.key === 'ArrowLeft' || ev.code === 'KeyA') {
this.pan(new Vec2(-100, 0))
} else if (ev.key === 'ArrowUp' || ev.key === 'w') {
} else if (ev.key === 'ArrowUp' || ev.code === 'KeyW') {
this.pan(new Vec2(0, -100))
} else if (ev.key === 'ArrowDown' || ev.key === 's') {
} else if (ev.key === 'ArrowDown' || ev.code === 'KeyS') {
this.pan(new Vec2(0, 100))
} else if (ev.key === 'Escape') {
this.props.onNodeSelect(null)