Compare commits

...
69 Commits
Author SHA1 Message Date
Jamie Wong 8a4f38a8cb 1.10.0 2020-09-29 18:51:49 -07:00
Jamie Wong 9361a6baf2 Switch from Travis CI to GitHub Actions for test runs (#316)
Switch from Travis CI to GitHub Actions for test runs
2020-09-29 16:39:18 -07:00
Jamie Wong ede9c74d50 Remove accidentally added/retained dependencies on react and react-redux (#315)
speedscope no longer relies upon react-redux, and never depended upon react. Let's clean these up.
2020-09-29 16:20:27 -07:00
Jamie Wong f758130455 Add support for imports of UTF-16 encoded text w/ Byte Order Mark (BOM) (#314)
Before this PR, we blindly assumed that all text imported into speedscope was UTF-8 encoded. This, unsurprisingly, is not always true. After this PR, we support text that's UTF-16 encoded, with either the little-endian or big-endian byte-order-mark.

Fixed #291
2020-09-29 15:40:49 -07:00
Jamie Wong f3a1c09c9b Add support for Safari profiles (#313)
Closes #294 

This adds import for Safari/webkit profiler. Well, for Safari 13.1 for sure, I haven't done any work to check if there's been changes to the syntax.

It seems to work OK, and is already a huge improvement over profiling in Safari (which doesn't even have a flame graph, let alone something like left heavy). Sadly, the sampler resolution is only 1kHz, which is not super useful for a lot of profiling work. I made a ticket on webkit bug tracker to ask for 10kHz/configurable sampling rate: https://bugs.webkit.org/show_bug.cgi?id=214866

Another thing that's missing is that I cut out all the idle time. We could also insert layout/paint samples into the timeline by parsing `events`. But I'll leave that for another time.

<img width="1280" alt="Captura de pantalla 2020-07-28 a las 11 02 06" src="https://user-images.githubusercontent.com/183747/88643560-20c16700-d0c2-11ea-9c73-d9159e68fab9.png">
2020-09-29 14:26:01 -07:00
Sebastian Wahl 069c0194a6 #168 Fix browser not opening on Windows when using the CLI (#307) 2020-09-14 11:53:36 -07:00
Jamie Wong 64fe369c42 1.9.0 2020-08-05 01:02:17 -07:00
Jamie Wong f55c53f699 Small followup tweaks to #305 2020-08-05 00:50:02 -07:00
E-Liang Tan a0b3fe8420 Add patch to fix accumulated negative deltas (#305)
## Context

Hi! I'm working on an experimental React [concurrent mode profiler](https://react-scheduling-profiler.vercel.app) in partnership with the React core team, and we're using a [custom build of Speedscope](https://github.com/taneliang/speedscope/compare/master...taneliang:fork-for-scheduling-profiler) that exposes Speedscope's internals to support our custom flamechart rendering. Specifically, Speedscope is used to import and process Chrome profiles, which are then fed to our rendering code that draws everything to a canvas.

Here's a screenshot of our app for context. The stuff above the thick gray bar is React data (some React Fiber lanes, React events, and other user timing marks), and a flamechart is drawn below.

![image](https://user-images.githubusercontent.com/12784593/89261576-e2e3b600-d660-11ea-9b90-6c6991d061d6.png)

## Problem

Early on, we had [an issue](https://github.com/MLH-Fellowship/scheduling-profiler-prototype/issues/42) where our flamechart was not aligned with the React data. The discrepancy between the flamechart frames and our React data grew over the time of the profile.

We tracked down the cause to https://github.com/jlfwong/speedscope/pull/80, which resolves https://github.com/jlfwong/speedscope/issues/70. It seems like zeroing out those negative time deltas resulted in the accumulation of errors over the time of these profiles, which resulted in the very visible misalignment in our profiler.

I am confident that the React data's timestamps are correct because they are obtained from User Timing marks, which have absolute timestamps and are thus independent of any `timeDelta` stuff. This would mean that Speedscope is likely displaying incorrect timestamps for Chrome profiles.

## Solution

This PR takes a different approach to solving the negative `timeDelta` problem: we add a `lastElapsed` variable as a sort of backstop, preventing `elapsed` from traveling backwards in time, while still ensuring that `elapsed` is always accurate.

We've been using this patch in our custom build for about a month now and it seems to work well.
2020-08-05 00:47:49 -07:00
Jamie Wong 9452aeae82 Provide prev/next buttons to cycle through search results, make search results more visually prominent (#304)
This PR addresses two key pieces of feedback provided on search in #38 

1. Make the search results more visually prominent
2. Make it easier to find the matches by having some way of jumping to next

For the visual prominence facet, I switched from yellow outlines to orange backgrounds.

|Before|After|
|-|-|
|![image](https://user-images.githubusercontent.com/150329/89276105-14746700-d5f8-11ea-9c9d-1dfdfc3bd6d7.png)|![image](https://user-images.githubusercontent.com/150329/89276070-045c8780-d5f8-11ea-8664-9da0af569cec.png)|

For the easy identification portion, I added prev/next buttons to each view, which can also be operated by hitting Enter for next or Shift+Enter for previous.

![Kapture 2020-08-04 at 2 16 57](https://user-images.githubusercontent.com/150329/89276542-a1b7bb80-d5f8-11ea-8642-a172a6561734.gif)
2020-08-04 02:20:26 -07:00
Gabriele N. Tornetta b26cdb5be4 Add link to Austin to README (#303)
Add a link to Austin's Speedscope section of the README to Speedscope's README.
2020-07-30 11:03:12 -07:00
Jamie Wong 1c5bdba36e Increase contrast for matching research results by fading text for unmatched frames (#298)
Before:
![image](https://user-images.githubusercontent.com/150329/88493052-ee99f300-cf63-11ea-9522-8de032e920ac.png)

After:
![image](https://user-images.githubusercontent.com/150329/88493062-f9548800-cf63-11ea-9e7e-5c87a1dba836.png)

Works towards #38
2020-07-26 17:21:38 -07:00
Jamie Wong c3b35d7b0f 1.8.0 2020-07-19 21:27:00 -07:00
Jamie Wong dfaefe54fd Implement search highlighting in time order & left heavy views (#297)
This implements the next step towards full featured search in speedscope: visual highlighting of matching search results in the time ordered & left heavy views. This doesn't yet add the ability to click prev/next to select the next matching element in the editor, but I'm still planning on doing something like that. I haven't figured out yet what I want the user experience to be like for that.

![speedscope-flamegraph-search](https://user-images.githubusercontent.com/150329/87898991-9ebba900-ca04-11ea-9bd9-31ad8d4c6d2a.gif)

This works towards fixing #38
2020-07-19 21:20:14 -07:00
Jamie Wong 7514f4c0c9 Fix performance issues for the caller/callee flamegraphs in the sandwich view (#296)
This fixes two unrelated problems which together caused performance issues in the sandwich view & made hover tooltips appear to be broken.

The first issue was caused by continuously priming the `requestAnimationFrame` loop when it should be a no-op, and the second issue was caused by using different cache keys when trying to access a memoized value in the caller & callee flamegraph components. This resulted in thrash, and especially bad performance because the cache miss was resulting in us re-allocating the WebGL framebuffer on every frame, which is unsurprisingly quite slow.

Fixes #212 
Fixes #155 
Fixes #74 (though this was maybe already fixed)
2020-07-18 22:37:15 -07:00
Jamie Wong ff447c2719 1.7.0 2020-07-13 22:10:28 -07:00
Jamie Wong 668bb032ba Introduce filtering via Ctrl+F/Cmd+F into the sandwich view (#293)
This is the first step towards fixing #38. 

I started with the easiest part from a UI-paradigm perspective, and also the place that's the most confusing that search doesn't work. Before this PR, browers' Cmd+F/Ctrl+F would *look* like it worked in the Sandwich view, but they wouldn't work fully because the view in the sandwich view is a virtualized table, meaning that it doesn't put all of the rows in the DOM. Instead, it only renders enough to fill the viewport to make rendering much faster.

Here's what the changes from this PR look like in action:

![Kapture 2020-07-12 at 23 17 33](https://user-images.githubusercontent.com/150329/87276802-ef2b8780-c495-11ea-9856-9c834ea7f028.gif)

Before closing #38, I'll be adding search functionality to the flamechart views too.
2020-07-13 22:04:19 -07:00
Jamie Wong 9ed1eb192c 1.6.0 2020-05-30 21:54:44 -07:00
Jamie Wong 8620432cbc Introduce a profile selector dropdown (#282)
This adds much better UI for selecting different profiles within a single import.

![Kapture 2020-05-30 at 21 34 06](https://user-images.githubusercontent.com/150329/83344564-595ce400-a2bd-11ea-8306-e5d8f647b65e.gif)

You can now hover over the middle of the toolbar or hit `t` on your keyboard to bring up the profile selector. From there, you can use fuzzy-find to switch to the profile you want, and hit "enter" to select it. The up and down arrow keys can be used while the profile selector filter input is focused to move through the list of profiles.

I think the "next" and "prev" buttons are now totally useless, so I removed them.

Fixes #167
2020-05-30 21:42:27 -07:00
Jamie Wong dead3f9ad9 Fix bug with bad caching of action creators (#281)
Profile switching was subtly broken because action creators weren't being correctly re-bound due to a missing dependency in a `useCallback` call.

I also tried to reduce boilerplate in this PR by adding additional exhaustive deps protection via eslint for `useSelector`, `useAppSelector`, and `useActionCreator`. The removes the need for using `useCallback` or each of those.

Fixes #280
2020-05-25 19:10:40 -07:00
Jamie Wong 80b747a55e Fix hot module reload issues caused by subtle bug in useSelector (#279)
To test this, load a profile, then save a `.tsx` file locally. Before this change, it would bring you back to the welcome screen after hot reload. After this change, application state is still displayed. This is because before the change, the `setGLCanvas` action wasn't resulting in a re-render because it occurred between the initial render and the `useLayoutEffect` callback.

Fixes #276
2020-05-25 15:42:19 -07:00
Jamie Wong 351994972d Upgrade to Preact X, partially convert to using hooks (#267)
I'd like to try writing new components using hooks, and to do that I need to upgrade from preact 8 to preact X.

For reasons that are... complicated, in order to upgrade without breaking part of my build process, I had to remove the dependency on `preact-redux` altogether. This led me to write my own implementation, and as part of that I realized I could remove `createContainer` in favour of some simple hooks that use redux.

Before landing:
- [x] Investigate performance issues in the sandwich views
- [x] Investigate es-lint checks for exhaustive hook dependencies
2020-05-23 16:42:31 -07:00
Jamie Wong ca1abfdd32 Fix schema generation in new TypeScript version (#274)
Fixes #268 

I fixed it by dropping the dependency on quicktype entirely, and using its dependency directly. I still don't understand why the version of typescript used in this repository affects what quicktype is doing, but it seems like the issue is in quicktype, not its dependency.

I validated this change was correct by diffing the output of `node scripts/generate-file-format-schema-json.js` with what's currently on http://speedscope.app/file-format-schema.json. There's no difference.

This PR also includes changes to the CI script to ensure that we can catch this before hitting master next time.
2020-05-23 16:07:38 -07:00
Jamie Wong dee9e5ade4 Fail loudly when profile is imported with unmatched open/close events (#273)
Before this change, profiles like those in #272 would import but would display misleading data. Let's fail hard instead.

Fixes #272
2020-05-23 15:56:17 -07:00
Jamie Wong 2077a905a9 Upgrade TypeScript from 3.2.4 to 3.9.2 (#266) 2020-05-16 17:02:51 -07:00
Jamie Wong e969178e65 More npm audit fixes 2020-05-16 16:46:01 -07:00
Justin Beckwith 56f6459af9 Bump parcel and audit fix (#264) 2020-05-16 16:44:45 -07:00
Jamie Wong 3f79e0fe96 Update README.md to include link for importing from ruby-prof
Fixes #265
2020-04-28 11:00:53 -07:00
Jamie Wong d30bb2ef7e Fix the build for node 13.x, make travis test 10, 12, 13, stable (#263)
@JustinBeckwith pointed out in #262 that `npm install` was broken in node 13.x, and @DanielRuf pointed in #254 that test fail for node 11+ because of a change to stability of sorting.

This PR seeks to address both of those.

The installation issue was fixed by just regenerating `package-lock.json` without needing to bump any of the direct dependency versions. The test failure issue requires manual intervention.

To fix the sort stability issue, I updated the tests to use the stable sort values (these were all the correct values, though some of the test values were incorrect).

To make the suite still pass for node 10, I added a hack where I override `Array.prototype.sort` with a stable implementation that's *only* used in tests (See comments in code for a justification for why)

## Test Plan

Before this PR: `npm install` on node 13.x fails & `npm run jest` results in test failures
After this PR: `npm install` on node 13.x passes & `npm run jest` passes for node 10, 12, and 13.
2020-04-20 08:26:59 -07:00
Jamie Wong fd4195da10 Update CHANGELOG.md 2020-01-16 00:17:47 -08:00
Jamie Wong 707462e9cf 1.5.3 2020-01-16 00:09:07 -08:00
Jamie Wong 375040e892 Bump dependency versions to unbreak build (#253)
I ended up in a horrible peer dependency hell and apparently needed to bump the versions of quicktype, typescript, ts-jest, *and* jest to get out of it. But I think I got out of it!

Local builds and deployment builds both seem to work after these changes.
2020-01-15 23:32:14 -08:00
Jamie Wong 5ae9abcf1d Trace event: Prevent event re-ordering from generating incorrect flamegraphs (#252)
The code to import trace formatted events intentionally re-orders events in order to make it easier at flamegraph construction time to order the pushes and pops of frames.

It turns out that this re-ordering results in incorrect flamegraphs being generated as shown in #251.

This PR fixes this by avoiding re-ordering in situations where it isn't necessary.
2020-01-15 22:03:23 -08:00
miso11 bdd9301c59 make tooltip width wider (#239)
Issue #191

It shouldn't be ellipsized, or at least it should be to the right, because I think it's more interesting the file name and function name than the system path where it is found.

Make max width bigger

Before: 
![before](https://user-images.githubusercontent.com/52132927/67621727-ff6dda80-f812-11e9-8c10-533542fe0302.png)

After:

![after](https://user-images.githubusercontent.com/52132927/67621730-04cb2500-f813-11e9-8d36-80e8c58a529e.png)
2019-10-28 11:36:15 -07:00
Jamie Wong cc9750923a Update README.md 2019-10-16 00:15:06 -07:00
Jonathan Chan c3074b7343 1.5.2 2019-10-10 18:27:28 -07:00
Jonathan Chan b15a08b3ff Support newer Emscripten .symbols with hex escapes (#233)
Apparently Emscripten now generates `.symbols` files where names are not mangled using Clang's mangling scheme, but rather hex-escaped! So 'a\20b' means 'a b'. Currently we can't import these symbol maps into Speedscope because a regex rejects them, and they look weird because we don't unescape.
2019-10-10 14:31:34 -07:00
Jamie Wong 68683aa054 Add pyspeedscope & flamescope to README 2019-10-06 15:10:00 -07:00
Jamie Wong eb0e1ce731 Add py-spy to README 2019-10-06 14:59:17 -07:00
Jamie Wong bd546ca893 1.5.1 2019-06-06 00:07:14 -07:00
Jamie Wong 0c1e477f35 Support import from trace event format event when there are too many "E" events. (#222)
Fixes #221
2019-06-06 00:03:08 -07:00
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
Jamie Wong 0fe0c454d3 1.2.0 2018-10-08 09:49:15 -07:00
vmarchaud 4b292b2acf Add import of v8 heap allocation profile (#170)
This adds support for importing heap profiles from Chrome: https://developers.google.com/web/tools/chrome-devtools/memory-problems/#allocation-profile
2018-10-08 09:15:39 -07:00
Jamie Wong d78d20e005 Update README.md 2018-09-26 13:38:36 -07:00
Jamie Wong 7d0a3a2c59 1.1.0 2018-09-26 11:46:41 -07:00
Jamie Wong 3f205ec3e9 Add go tool pprof import support (#165)
This PR adds support for importing from Google's pprof format, which is a gzipped, protobuf encoded file format (that's incredibly well documented!) The [pprof http library](https://golang.org/pkg/net/http/pprof/) also offers an output of the trace file format, which continues to not be supported in speedscope to date (See #77). This will allow importing of profiles generated by the standard library go profiler for analysis of profiles containing heap allocation information, CPU profile information, and a few other things like coroutine creation information.

In order to add support for that a number of dependent bits of functionality were added, which should each provide an easier path for future binary input sources

- A protobuf decoding library was included ([protobufjs](https://www.npmjs.com/package/protobufjs)) which includes both a protobuf parser generator based on a .proto file & TypeScript definition generation from the resulting generated JavaScript file
- More generic binary file import. Before this PR, all supported sources were plaintext, with the exception of Instruments 10 support, which takes a totally different codepath. Now binary file import should work when files are dropped, opened via file browsing, or opened via invocation of the speedscope CLI.
- Transparent gzip decoding of imported files (this means that if you were to gzip compress another JSON file, then importing it should still work fine)

Fixes #60.

--

This is a [donation motivated](https://github.com/jlfwong/speedscope/issues/60#issuecomment-419660710) PR motivated by donations by @davecheney & @jmoiron to [/dev/color](https://www.devcolor.org/welcome) 🎉
2018-09-26 11:33:34 -07:00
128 changed files with 36826 additions and 9453 deletions
+11 -4
View File
@@ -1,13 +1,20 @@
module.exports = {
parser: 'typescript-eslint-parser',
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['prettier'],
plugins: ['prettier', '@typescript-eslint', 'react-hooks'],
rules: {
'prettier/prettier': 'error',
'@typescript-eslint/explicit-function-return-type': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': [
'error',
{
additionalHooks: '(useSelector|useAppSelector|useActionCreator)',
},
],
},
};
}
+43
View File
@@ -0,0 +1,43 @@
name: Node.js CI
on:
push:
branches:
- master
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
env:
CI: true
- name: Coveralls Parallel
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
flag-name: run-${{ matrix.node-version }}
parallel: true
finish:
needs: test
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
-3
View File
@@ -1,3 +0,0 @@
language: node_js
node_js:
- '9'
+153 -23
View File
@@ -1,85 +1,215 @@
## Unreleased
## [1.10.0] - 2020-09-29
### Added
- Support for importing profiles from Safari [[#300](https://github.com/jlfwong/speedscope/pull/300)] (by [@radex](https://github.com/radex))
### Fixed
- Fixed browser not opening on Windows when using the CLI [[#307](https://github.com/jlfwong/speedscope/pull/307)] (by [@spillerrec](https://github.com/spillerrec))
- Fixed import of UTF-16 encoded files w/ BOM [[#314](https://github.com/jlfwong/speedscope/pull/314)]
- Removed accidental dependency on React [[#315](https://github.com/jlfwong/speedscope/pull/315)]
## [1.9.0] - 2020-08-05
### Added
- Provide prev/next buttons to cycle through search results, make search results more visually prominen [[#304](https://github.com/jlfwong/speedscope/pull/304)]
### Fixed
- Fix accumulated errors in Chrome profile imports caused by zeroed negative timeDeltas [[#305](https://github.com/jlfwong/speedscope/pull/305)] (by [@taneliang](https://github.com/taneliang))
## [1.8.0] - 2020-07-19
### Added
- Added search highlighting in time order & left heavy views [[#297](https://github.com/jlfwong/speedscope/pull/297)]
### Fixed
- Fix performance issues for the caller/callee flamegraphs in the sandwich view [[#296](https://github.com/jlfwong/speedscope/pull/296)]
## [1.7.0] - 2020-07-13
### Added
- Introduced filtering via Ctrl+F/Cmd+F into the sandwich view [[#293](https://github.com/jlfwong/speedscope/pull/293)]
## [1.6.0] - 2020-05-30
### Added
- Improved profile/thread selection UI [[#282](https://github.com/jlfwong/speedscope/pull/282)]
### Fixed
- Crash instead of incorrectly interpreting profiles with incorrectly ordered events [[#273](https://github.com/jlfwong/speedscope/pull/273)]
- A large refactor to upgrade to Preact X was performed [[#267](https://github.com/jlfwong/speedscope/pull/267)]
## [1.5.3] - 2020-01-16
### Fixed
- Bump dependency versions to unbreak build [[#253](https://github.com/jlfwong/speedscope/pull/253)] (by [@jlfwong](https://github.com/jlfwong), with changes from [@Archerlly](https://github.com/Archerlly)'s [#215](https://github.com/jlfwong/speedscope/pull/215))
- Trace event: Prevent event re-ordering from generating incorrect flamegraphs ([#252](https://github.com/jlfwong/speedscope/pull/252), with changes from [@hwajaywang](https://github.com/hwajaywang)'s [#249](https://github.com/jlfwong/speedscope/pull/249))
- Make tooltip width wider [[#239](https://github.com/jlfwong/speedscope/pull/239)] (by [@miso11](https://github.com/miso11))
## [1.5.2] - 2019-10-10
### Fixed
- Fix emscripten remapping when symbols are hex-escaped, like `a\20b` [[#233](https://github.com/jlfwong/speedscope/pull/233)] (by [@jyc](https://github.com/jyc))
## [1.5.1] - 2019-06-06
### Fixed
- Fixed import of trace event files which contain unmatched "E" events ([#222](https://github.com/jlfwong/speedscope/pull/222)) (by [@jlfwong](https://github.com/jlfwong))
## [1.5.0] - 2019-02-17
### Added
- Support importing unterminated JSON in simple cases ([#208](https://github.com/jlfwong/speedscope/pull/208)) (by [@jlfwong](https://github.com/jlfwong))
### Fixed
- Fix crash when importing from stackprof without raw_timestamp_deltas ([#207](https://github.com/jlfwong/speedscope/pull/207)) (by [@jlfwong](https://github.com/jlfwong))
- Alert instead of crash when importing a file containing no profiles ([#205](https://github.com/jlfwong/speedscope/pull/205)) (by [@jlfwong](https://github.com/jlfwong))
- Fixed import of multithreaded profiles from Chrome 66 ([#206](https://github.com/jlfwong/speedscope/pull/206)) (by [@jlfwong](https://github.com/jlfwong))
- Fixed import of instruments trace files with missing run number ([#203](https://github.com/jlfwong/speedscope/pull/203)) (by [@Archerlly](https://github.com/Archerlly))
## [1.4.1] - 2019-01-22
### Fixed
- Fix importing of Trace Event Format files with no ts field on M events [[#198](https://github.com/jlfwong/speedscope/pull/198)] (by [@jlfwong](https://github.com/jlfwong))
## [1.4.0] - 2019-01-22
### Added
- Import v8 cpu profile (old format) [[#177](https://github.com/jlfwong/speedscope/pull/177)] (by [@vmarchaud](https://github.com/vmarchaud))
- Import basic "Trace Event Format" profiles [[#197](https://github.com/jlfwong/speedscope/pull/197)] (by [@jlfwong](https://github.com/jlfwong))
## [1.3.2] - 2018-12-03
### Fixed
- Fixed import of multithreaded Chrome profiles [[#19](https://github.com/jlfwong/speedscope/pull/19)] (by [@jlfwong](https://github.com/jlfwong))
## [1.3.1] - 2018-11-08
### Fixed
- Fixed a file import performance regression by using TextDecoder [[#188](https://github.com/jlfwong/speedscope/pull/188)] (by [@jlfwong](https://github.com/jlfwong))
## [1.3.0] - 2018-10-29
### Added
- Support import from Haskell GHC JSON format support [[#183](https://github.com/jlfwong/speedscope/pull/183)] (by [@trishume](https://github.com/trishume))
### Fixed
- Make the wasd keymappings work on azerty keyboards [[#184](https://github.com/jlfwong/speedscope/pull/184)] (by [@vrischmann](https://github.com/vrischmann))
- Fix import of binary formats via profileURL [[#179](https://github.com/jlfwong/speedscope/pull/179)] (by [@f](https://github.com/f)-hj)
## [1.2.0] - 2018-10-08
### Added
- Add import of v8 heap allocation profile [[#170](https://github.com/jlfwong/speedscope/pull/170)] (by [@vmarchaud](https://github.com/vmarchaud))
## [1.1.0] - 2018-09-26
### Added
- Add go tool pprof import support [[#165](https://github.com/jlfwong/speedscope/pull/165)]
## [1.0.4] - 2018-09-12
### Fixed
* Fix import from Chrome < 69 when there are multiple profiles [#161]
- Fix import from Chrome < 69 when there are multiple profiles [[#161](https://github.com/jlfwong/speedscope/pull/161)]
## [1.0.3] - 2018-09-10
### Fixed
* Fix import for Chrome 69, support leading idle time before first call [#160]
- Fix import for Chrome 69, support leading idle time before first call [[#160](https://github.com/jlfwong/speedscope/pull/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]
- Allow optional CR before LF when probing collapsed stacks files [[#154](https://github.com/jlfwong/speedscope/pull/154)]
- Fix import for Firefox 63 [[#156](https://github.com/jlfwong/speedscope/pull/156)]
- Change time formatting for minutes from 1.50min to 1:30 [[#153](https://github.com/jlfwong/speedscope/pull/153)] (by [@Alex](https://github.com/Alex)-Diez)
## [1.0.1] - 2018-08-23
* Fixed an issue where flamegraph bounds were not always being cleared correctly, leading to visual artifacts [#150]
- Fixed an issue where flamegraph bounds were not always being cleared correctly, leading to visual artifacts [[#150](https://github.com/jlfwong/speedscope/pull/150)]
## [1.0.0] - 2018-08-23
### Fixed
* Fixed rendering issues when switching between screens w/ different `devicePixelRatios` [#147]
- Fixed rendering issues when switching between screens w/ different `devicePixelRatios` [[#147](https://github.com/jlfwong/speedscope/pull/147)]
## [0.7.1] - 2018-08-20
### Fixed
* Removed dependency on regl in order to allow speedscope to run in strict content-security-policy environments [#140]
* Fixed text culling bug [#143]
- Removed dependency on regl in order to allow speedscope to run in strict content-security-policy environments [[#140](https://github.com/jlfwong/speedscope/pull/140)]
- Fixed text culling bug [[#143](https://github.com/jlfwong/speedscope/pull/143)]
## [0.7.0] - 2018-08-16
### Added
* Added support to import from linux `perf script` [#135]
- Added support to import from linux `perf script` [[#135](https://github.com/jlfwong/speedscope/pull/135)]
## [0.6.0] - 2018-08-14
### Added
* Added support for multiple threads/processes [#130]
* Import all runs & threads from Instruments .trace files instead of just main thread from selected run [#130]
- Added support for multiple threads/processes [[#130](https://github.com/jlfwong/speedscope/pull/130)]
- Import all runs & threads from Instruments .trace files instead of just main thread from selected run [[#130](https://github.com/jlfwong/speedscope/pull/130)]
### Fixed
* Ensure the JSON schema has actual contents [#133]
- Ensure the JSON schema has actual contents [[#133](https://github.com/jlfwong/speedscope/pull/133)]
## [0.5.1] - 2018-08-09
### Fixed
* Fixed broken CLI
- Fixed broken CLI
## [0.5.0] - 2018-08-09
### Fixed
* Fix emscripten remapping when symbols contain dashes, like `527:i32s-div` [#129]
* Improved firefox import speed and fixed bugs in it [#128]
* Prevent non-contiguous blocks in the time ordered flamechart from appearing as a single node for selection [#123]
* Prevent dragging from changing selection [#122]
* Clamp zoom to prevent floating point issues [#121]
* Preserve view state when switching tabs [#100]
- Fix emscripten remapping when symbols contain dashes, like `527:i32s-div` [[#129](https://github.com/jlfwong/speedscope/pull/129)]
- Improved firefox import speed and fixed bugs in it [[#128](https://github.com/jlfwong/speedscope/pull/128)]
- Prevent non-contiguous blocks in the time ordered flamechart from appearing as a single node for selection [[#123](https://github.com/jlfwong/speedscope/pull/123)]
- Prevent dragging from changing selection [[#122](https://github.com/jlfwong/speedscope/pull/122)]
- Clamp zoom to prevent floating point issues [[#121](https://github.com/jlfwong/speedscope/pull/121)]
- Preserve view state when switching tabs [[#100](https://github.com/jlfwong/speedscope/pull/100)]
## [0.4.0] - 2018-07-21
### Added
* Support for importing v8 logs from node [#98]
* Optionally read from stdin via cli [#99]
- Support for importing v8 logs from node [[#98](https://github.com/jlfwong/speedscope/pull/98)]
- Optionally read from stdin via cli [[#99](https://github.com/jlfwong/speedscope/pull/99)]
## [0.3.0] - 2018-07-18
### Added
* Support for remapping profiles using a wasm symbol file [#93]
- Support for remapping profiles using a wasm symbol file [[#93](https://github.com/jlfwong/speedscope/pull/93)]
+14
View File
@@ -36,13 +36,25 @@ speedscope is designed to ingest profiles from a variety of different profilers
- JavaScript
- [Importing from Chrome](https://github.com/jlfwong/speedscope/wiki/Importing-from-Chrome)
- [Importing from Firefox](https://github.com/jlfwong/speedscope/wiki/Importing-from-Firefox)
- [Importing from Safari](https://github.com/jlfwong/speedscope/wiki/Importing-from-Safari)
- [Importing from Node.js](https://github.com/jlfwong/speedscope/wiki/Importing-from-Node.js)
- Ruby
- [Importing from stackprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-stackprof-(ruby))
- [Importing from rbspy](https://github.com/jlfwong/speedscope/wiki/Importing-from-rbspy-(ruby))
- [Importing from ruby-prof](https://github.com/jlfwong/speedscope/wiki/Importing-from-ruby-prof)
- Python
- [Importing from py-spy](https://github.com/jlfwong/speedscope/wiki/Importing-from-py-spy-(python))
- [pyspeedscope](https://github.com/windelbouwman/pyspeedscope)
- [Importing from Austin](https://github.com/p403n1x87/austin#speedscope)
- Go
- [Importing from pprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-pprof-(go))
- Rust
- [flamescope](https://github.com/coolreader18/flamescope)
- 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 .NET Core](https://github.com/jlfwong/speedscope/wiki/Importing-from-.NET-Core)
- [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).
@@ -105,6 +117,8 @@ Once a profile has loaded, the main view is split into two: the top area is the
* `Cmd+O`/`Ctrl+O` to open a new profile
* `n`: Go to next profile/thread if one is available
* `p`: Go to previous profile/thread if one is available
* `t`: Open the profile/thread selector if available
* `Cmd+F`/`Ctrl+F`: to open search. While open, `Enter` and `Shift+Enter` cycle through results
## Contributing
+6 -2
View File
@@ -4,7 +4,7 @@ const fs = require('fs')
const os = require('os')
const stream = require('stream')
const opn = require('opn')
const open = require('open')
const helpString = `Usage: speedscope [filepath]
@@ -89,7 +89,11 @@ async function main() {
console.log('Opening', urlToOpen, 'in your default browser')
await opn(urlToOpen, {wait: false})
// We'd like to avoid blocking the terminal on the browsing closing,
// but for some reason this doesn't work at all on Windows if we
// don't use wait: true.
const wait = process.platform === "win32";
await open(urlToOpen, {wait})
}
main()
+6304 -8063
View File
File diff suppressed because it is too large Load Diff
+43 -20
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "1.0.4",
"version": "1.10.0",
"description": "",
"repository": "jlfwong/speedscope",
"main": "index.js",
@@ -13,47 +13,70 @@
"prettier": "prettier --write 'src/**/*.ts' 'src/**/*.tsx'",
"lint": "eslint 'src/**/*.ts' 'src/**/*.tsx'",
"jest": "./scripts/test-setup.sh && jest --runInBand",
"coverage": "npm run jest -- --coverage && coveralls < coverage/lcov.info",
"test": "tsc --noEmit && npm run lint && npm run coverage",
"coverage": "npm run jest -- --coverage",
"typecheck": "tsc --noEmit",
"test": "./scripts/ci.sh",
"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": {
"@types/jest": "22.2.3",
"@types/jszip": "3.1.4",
"@types/node": "10.1.4",
"@types/node": "14.0.1",
"@types/pako": "1.0.0",
"@typescript-eslint/eslint-plugin": "2.33.0",
"@typescript-eslint/parser": "2.33.0",
"acorn": "7.2.0",
"aphrodite": "2.1.0",
"coveralls": "3.0.1",
"eslint": "4.19.1",
"eslint": "6.0.0",
"eslint-plugin-prettier": "2.6.0",
"jest": "23.0.1",
"eslint-plugin-react-hooks": "4.0.2",
"jest": "24.3.0",
"jsverify": "0.8.3",
"jszip": "3.1.5",
"pako": "1.0.6",
"parcel-bundler": "1.9.2",
"preact": "8.2.7",
"preact-redux": "jlfwong/preact-redux#a56dcc4",
"prettier": "1.12.0",
"quicktype": "15.0.45",
"redux": "^4.0.0",
"ts-jest": "22.4.6",
"typescript": "2.8.1",
"parcel-bundler": "1.12.4",
"preact": "10.4.1",
"prettier": "2.0.4",
"protobufjs": "6.8.8",
"redux": "^4.0.5",
"ts-jest": "24.3.0",
"typescript": "3.9.2",
"typescript-eslint-parser": "17.0.1",
"typescript-json-schema": "0.42.0",
"uglify-es": "3.2.2"
},
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"setupFilesAfterEnv": [
"./src/jest-setup.js"
],
"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"
"open": "7.2.0"
}
}
+2 -1
View File
@@ -4,4 +4,5 @@ module.exports = {
semi: false,
singleQuote: true,
trailingComma: 'all',
};
arrowParens: 'avoid'
}
File diff suppressed because one or more lines are too long
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
File diff suppressed because one or more lines are too long
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]}
Binary file not shown.
@@ -0,0 +1,16 @@
{
"$schema": "https://www.speedscope.app/file-format-schema.json",
"shared": {
"frames": [{"name": "A"}]
},
"profiles": [
{
"type": "evented",
"name": "p1",
"unit": "none",
"startValue": 0,
"endValue": 100,
"events": [{"type": "O", "frame": 0, "at": 0}]
}
]
}
@@ -0,0 +1,25 @@
{
"$schema": "https://www.speedscope.app/file-format-schema.json",
"shared": {
"frames": [{"name": "A"}, {"name": "B"}]
},
"profiles": [
{
"type": "evented",
"name": "p1",
"unit": "none",
"startValue": 0,
"endValue": 100,
"events": [
{"type": "O", "frame": 0, "at": 0},
{"type": "C", "frame": 0, "at": 1},
{"type": "O", "frame": 1, "at": 2},
{"type": "O", "frame": 0, "at": 2},
{"type": "O", "frame": 0, "at": 3},
{"type": "C", "frame": 0, "at": 4},
{"type": "C", "frame": 1, "at": 4},
{"type": "C", "frame": 0, "at": 5}
]
}
]
}
Binary file not shown.
Binary file not shown.
@@ -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,16 @@
[
{"tid": 1, "ph": "B", "pid": 0, "name": "A", "ts": 0},
{"tid": 1, "ph": "B", "pid": 0, "name": "B", "ts": 0},
{"tid": 1, "ph": "B", "pid": 0, "name": "C", "ts": 0},
{"tid": 1, "ph": "E", "pid": 0, "name": "C", "ts": 1},
{"tid": 1, "ph": "E", "pid": 0, "name": "B", "ts": 2},
{"tid": 1, "ph": "E", "pid": 0, "name": "A", "ts": 3},
{"tid": 1, "ph": "B", "pid": 0, "name": "A", "ts": 4},
{"tid": 1, "ph": "B", "pid": 0, "name": "B", "ts": 5},
{"tid": 1, "ph": "B", "pid": 0, "name": "C", "ts": 6},
{"tid": 1, "ph": "E", "pid": 0, "name": "C", "ts": 7},
{"tid": 1, "ph": "E", "pid": 0, "name": "B", "ts": 7},
{"tid": 1, "ph": "E", "pid": 0, "name": "A", "ts": 7},
{"tid": 1, "ph": "B", "pid": 0, "name": "X", "ts": 7},
{"tid": 1, "ph": "E", "pid": 0, "name": "X", "ts": 10}
]
@@ -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,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": "E", "name": "beta", "ts": 13},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14},
{"pid": 0, "tid": 0, "ph": "E", "name": "gamma", "ts": 5},
{"pid": 0, "tid": 0, "ph": "E", "name": "delta", "ts": 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()
}
}
@@ -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')
}
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
set -euxo pipefail
npm run typecheck
npm run lint
npm run coverage
node scripts/generate-file-format-schema-json.js > /dev/null
+1 -1
View File
@@ -3,7 +3,7 @@ const child_process = require('child_process')
// Convert the file-format-spec.ts file into a json schema file
let jsonSchema = child_process.execSync(
'node_modules/.bin/quicktype --lang schema ./src/lib/file-format-spec.ts',
'node_modules/.bin/typescript-json-schema ./src/lib/file-format-spec.ts --titles --required --topRef "*"',
{
encoding: 'utf8',
},
+1 -3
View File
@@ -24,9 +24,7 @@ export class CanvasContext {
const webGLInfo = this.gl.getWebGLInfo()
if (webGLInfo) {
console.log(
`WebGL initialized. renderer: ${webGLInfo.renderer}, vendor: ${
webGLInfo.vendor
}, version: ${webGLInfo.version}`,
`WebGL initialized. renderer: ${webGLInfo.renderer}, vendor: ${webGLInfo.vendor}, version: ${webGLInfo.version}`,
)
}
;(window as any)['testContextLoss'] = () => {
+4 -4
View File
@@ -175,8 +175,8 @@ export class FlamechartRenderer {
// and the blue channel to indicate the color bucket to render.
// We add one to each so we have zero reserved for the background color.
const color = new Color(
(1 + i % 255) / 256,
(1 + stackDepth % 255) / 256,
(1 + (i % 255)) / 256,
(1 + (stackDepth % 255)) / 256,
(1 + this.flamechart.getColorBucketForFrame(frame.node.frame)) / 256,
)
batch.addRect(configSpaceBounds, color)
@@ -288,10 +288,10 @@ export class FlamechartRenderer {
const configSpaceContentWidth = this.flamechart.getTotalWeight()
const numAtlasEntriesPerLayer = Math.pow(2, zoomLevel)
const left = Math.floor(
numAtlasEntriesPerLayer * configSpaceSrcRect.left() / configSpaceContentWidth,
(numAtlasEntriesPerLayer * configSpaceSrcRect.left()) / configSpaceContentWidth,
)
const right = Math.ceil(
numAtlasEntriesPerLayer * configSpaceSrcRect.right() / configSpaceContentWidth,
(numAtlasEntriesPerLayer * configSpaceSrcRect.right()) / configSpaceContentWidth,
)
const nLayers = this.flamechart.getLayers().length
+8 -2
View File
@@ -455,7 +455,8 @@ export namespace WebGL {
widthInAppUnits: number,
heightInAppUnits: number,
) {
const bounds = this._gl.canvas.getBoundingClientRect()
let canvas = this._gl.canvas as HTMLCanvasElement
const bounds = canvas.getBoundingClientRect()
if (
this._width === widthInAppUnits &&
@@ -467,7 +468,6 @@ export namespace WebGL {
return
}
let canvas = this._gl.canvas
let style = canvas.style
canvas.width = widthInPixels
canvas.height = heightInPixels
@@ -1086,11 +1086,17 @@ export namespace WebGL {
_compileShader(gl: WebGLRenderingContext, type: GLenum, source: string) {
let shader = gl.createShader(type)
if (!shader) {
throw new Error('Failed to create shader')
}
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(`${gl.getShaderInfoLog(shader)}`)
}
if (!this._program) {
throw new Error('Tried to attach shader before program was created')
}
gl.attachShader(this._program, shader)
}
+6 -1
View File
@@ -65,7 +65,12 @@ export class ViewportRectangleRenderer {
private buffer: Graphics.VertexBuffer
constructor(private gl: Graphics.Context) {
const vertices = [[-1, 1], [1, 1], [-1, -1], [1, -1]]
const vertices = [
[-1, 1],
[1, 1],
[-1, -1],
[1, -1],
]
const floats: number[] = []
for (let v of vertices) {
floats.push(v[0])
+8 -1
View File
@@ -45,7 +45,14 @@ export class RectangleBatch {
return this.buffer
}
const corners = [[0, 0], [1, 0], [0, 1], [1, 0], [0, 1], [1, 1]]
const corners = [
[0, 0],
[1, 0],
[0, 1],
[1, 0],
[0, 1],
[1, 1],
]
const bytes = new Uint8Array(vertexFormat.stride * corners.length * this.rects.length)
const floats = new Float32Array(bytes.buffer)
@@ -104,6 +104,114 @@ exports[`importFromBGFlameGraph with CRLF: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with CRLF: profileGroup.name 1`] = `"simple-crlf.txt"`;
exports[`importFromBGFlameGraph with UTF-16, Big Endian 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-utf16-be.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
"a;b;c 3",
"a;b 5",
],
}
`;
exports[`importFromBGFlameGraph with UTF-16, Big Endian: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with UTF-16, Big Endian: profileGroup.name 1`] = `"simple-utf16-be.txt"`;
exports[`importFromBGFlameGraph with UTF-16, Little Endian 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-utf16-le.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
"a;b;c 3",
"a;b 5",
],
}
`;
exports[`importFromBGFlameGraph with UTF-16, Little Endian: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with UTF-16, Little Endian: profileGroup.name 1`] = `"simple-utf16-le.txt"`;
exports[`importFromBGFlameGraph: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph: profileGroup.name 1`] = `"simple.txt"`;
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
+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"`;
@@ -0,0 +1,101 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importFromSafari 1`] = `
Object {
"frames": Array [
Frame {
"col": 13,
"file": "__InjectedScript_InjectedScriptSource.js",
"key": "injectModule:__InjectedScript_InjectedScriptSource.js:109:13",
"line": 109,
"name": "injectModule",
"selfWeight": 0,
"totalWeight": 0.001,
},
Frame {
"col": 10,
"file": "__InjectedScript_CommandLineAPIModuleSource.js",
"key": ":__InjectedScript_CommandLineAPIModuleSource.js:2:10",
"line": 2,
"name": "(anonymous)",
"selfWeight": 0.001,
"totalWeight": 0.001,
},
Frame {
"col": 1,
"file": "file:///speedscope/sample/programs/javascript/simple.js",
"key": "(program):file:///speedscope/sample/programs/javascript/simple.js:1:1",
"line": 1,
"name": "(program)",
"selfWeight": 0,
"totalWeight": 0.03248933597933502,
},
Frame {
"col": 15,
"file": "file:///speedscope/sample/programs/javascript/simple.js",
"key": "alpha:file:///speedscope/sample/programs/javascript/simple.js:1:15",
"line": 1,
"name": "alpha",
"selfWeight": 0,
"totalWeight": 0.03248933597933502,
},
Frame {
"col": 15,
"file": "file:///speedscope/sample/programs/javascript/simple.js",
"key": "delta:file:///speedscope/sample/programs/javascript/simple.js:14:15",
"line": 14,
"name": "delta",
"selfWeight": 0.003094222474222382,
"totalWeight": 0.020112446082445484,
},
Frame {
"col": 15,
"file": "file:///speedscope/sample/programs/javascript/simple.js",
"key": "gamma:file:///speedscope/sample/programs/javascript/simple.js:20:15",
"line": 20,
"name": "gamma",
"selfWeight": 0.029395113505112636,
"totalWeight": 0.029395113505112636,
},
Frame {
"col": 14,
"file": "file:///speedscope/sample/programs/javascript/simple.js",
"key": "beta:file:///speedscope/sample/programs/javascript/simple.js:8:14",
"line": 8,
"name": "beta",
"selfWeight": 0,
"totalWeight": 0.012376889896889526,
},
Frame {
"col": 102,
"file": "",
"key": "firstOpenSearchURLString::4:102",
"line": 4,
"name": "firstOpenSearchURLString",
"selfWeight": 0.0005174240213818848,
"totalWeight": 0.0005174240213818848,
},
],
"name": "Grabación de Control temporal 1",
"stacks": Array [
"injectModule;(anonymous) 1.00ms",
" 39.93ms",
"(program);alpha;delta;gamma 10.83ms",
" 2.46ms",
"(program);alpha;delta 3.09ms",
"(program);alpha;beta;gamma 4.64ms",
"(program);alpha;delta;gamma 1.55ms",
"(program);alpha;beta;gamma 1.55ms",
"(program);alpha;delta;gamma 3.09ms",
"(program);alpha;beta;gamma 4.64ms",
"(program);alpha;delta;gamma 1.55ms",
"(program);alpha;beta;gamma 1.55ms",
" 253.50ms",
"firstOpenSearchURLString 517.42µs",
],
}
`;
exports[`importFromSafari: indexToView 1`] = `0`;
exports[`importFromSafari: profileGroup.name 1`] = `"Grabación de Control temporal 1"`;
@@ -0,0 +1,528 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importTraceEvents bad E events 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": 12,
"totalWeight": 12,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 12.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents bad E events: indexToView 1`] = `0`;
exports[`importTraceEvents bad E events: profileGroup.name 1`] = `"too-many-end-events.json"`;
exports[`importTraceEvents event re-ordering 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "A",
"line": undefined,
"name": "A",
"selfWeight": 2,
"totalWeight": 6,
},
Frame {
"col": undefined,
"file": undefined,
"key": "B",
"line": undefined,
"name": "B",
"selfWeight": 2,
"totalWeight": 4,
},
Frame {
"col": undefined,
"file": undefined,
"key": "C",
"line": undefined,
"name": "C",
"selfWeight": 2,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "X",
"line": undefined,
"name": "X",
"selfWeight": 3,
"totalWeight": 3,
},
],
"name": "pid 0, tid 1",
"stacks": Array [
"A;B;C 1.00µs",
"A;B 1.00µs",
"A 1.00µs",
" 1.00µs",
"A 1.00µs",
"A;B 1.00µs",
"A;B;C 1.00µs",
"X 3.00µs",
],
}
`;
exports[`importTraceEvents event re-ordering: indexToView 1`] = `0`;
exports[`importTraceEvents event re-ordering: profileGroup.name 1`] = `"must-retain-original-order.json"`;
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"`;
File diff suppressed because it is too large Load Diff
+8
View File
@@ -7,3 +7,11 @@ test('importFromBGFlameGraph', async () => {
test('importFromBGFlameGraph with CRLF', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-crlf.txt')
})
test('importFromBGFlameGraph with UTF-16, Little Endian', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-utf16-le.txt')
})
test('importFromBGFlameGraph with UTF-16, Big Endian', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-utf16-be.txt')
})
+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')
})
+80 -22
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')
}
@@ -179,6 +224,10 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
// Ref: https://github.com/v8/v8/blob/44bd8fd7/src/inspector/js_protocol.json#L1485
let elapsed = chromeProfile.timeDeltas[0]
// Prevents negative time deltas from causing bad data. See
// https://github.com/jlfwong/speedscope/pull/305 for details.
let lastValidElapsed = elapsed
let lastNodeId = NaN
// The chrome CPU profile format doesn't collapse identical samples. We'll do that
@@ -187,21 +236,26 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
const nodeId = chromeProfile.samples[i]
if (nodeId != lastNodeId) {
samples.push(nodeId)
sampleTimes.push(elapsed)
if (elapsed < lastValidElapsed) {
sampleTimes.push(lastValidElapsed)
} else {
sampleTimes.push(elapsed)
lastValidElapsed = elapsed
}
}
if (i === chromeProfile.samples.length - 1) {
if (!isNaN(lastNodeId)) {
samples.push(lastNodeId)
sampleTimes.push(elapsed)
if (elapsed < lastValidElapsed) {
sampleTimes.push(lastValidElapsed)
} else {
sampleTimes.push(elapsed)
lastValidElapsed = 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
}
const timeDelta = chromeProfile.timeDeltas[i + 1]
elapsed += timeDelta
lastNodeId = nodeId
}
@@ -264,3 +318,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()],
}
}
+8 -4
View File
@@ -1,8 +1,12 @@
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)
// 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)
})
+103 -16
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,12 +14,45 @@ 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 {importFromSafari} from './safari'
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 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,20 +72,48 @@ 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> {
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()
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')
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)))
@@ -63,12 +129,18 @@ async function _importProfileGroup(
} else if (fileName.endsWith('.v8log.json')) {
console.log('Importing as --prof-process v8 log')
return toGroup(importFromV8ProfLog(JSON.parse(contents)))
} else if (fileName.endsWith('.heapprofile')) {
console.log('Importing as Chrome Heap Profile')
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
} else if (fileName.endsWith('-recording.json')) {
console.log('Importing as Safari profile')
return toGroup(importFromSafari(JSON.parse(contents)))
}
// 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') {
@@ -78,17 +150,32 @@ async function _importProfileGroup(
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) {
console.log('Importing as --prof-process v8 log')
return toGroup(importFromV8ProfLog(parsed))
} else if ('head' in parsed && 'selfSize' in parsed['head']) {
console.log('Importing as Chrome Heap Profile')
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
} else if ('rts_arguments' in parsed && 'initial_capabilities' in parsed) {
console.log('Importing as Haskell GHC JSON Profile')
return importFromHaskell(parsed)
} else if ('recording' in parsed && 'sampleStackTraces' in parsed.recording) {
console.log('Importing as Safari profile')
return toGroup(importFromSafari(JSON.parse(contents)))
}
} 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')
})
})
+12 -50
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 {
@@ -405,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]
@@ -674,7 +633,7 @@ export function readInstrumentsKeyedArchive(buffer: ArrayBuffer): any {
////////////////////////////////////////////////////////////////////////////////
export function decodeUTF8(bytes: Uint8Array): string {
let text = String.fromCharCode.apply(String, bytes)
let text = String.fromCharCode.apply(String, Array.from(bytes))
if (text.slice(-1) === '\0') text = text.slice(0, -1) // Remove a single trailing null character if present
return decodeURIComponent(escape(text))
}
@@ -781,7 +740,10 @@ function paternMatchObjectiveC(
// Replace NSString with a string
case 'NSString':
case 'NSMutableString':
return decodeUTF8(value['NS.bytes'])
if (value['NS.string']) return value['NS.string']
if (value['NS.bytes']) return decodeUTF8(value['NS.bytes'])
console.warn(`Unexpected ${name} format: `, value)
return null
// Replace NSArray with an Array
case 'NSArray':
+1 -1
View File
@@ -1,6 +1,6 @@
import {checkProfileSnapshot} from '../lib/test-utils'
describe('importFromLinuxPerf', async () => {
describe('importFromLinuxPerf', () => {
test('simple.linux-perf.txt', async () => {
await checkProfileSnapshot('./sample/profiles/linux-perf/simple.linux-perf.txt')
})
+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
+5
View File
@@ -0,0 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importFromSafari', async () => {
await checkProfileSnapshot('./sample/profiles/Safari/13.1/simple.html-recording.json')
})
+120
View File
@@ -0,0 +1,120 @@
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
import {TimeFormatter} from '../lib/value-formatters'
interface Record {
type: string
eventType?: string
startTime?: number
endTime?: number
// timeline-record-type-cpu
timestamp?: number
usage?: number
threads?: any[]
// timeline-record-type-script
details?: number | string | any
extraDetails?: null | any
// timeline-record-type-network
archiveStartTime?: number
entry?: any
// timeline-record-type-layout
quad?: number[]
}
interface ExprLocation {
line: number
column: number
}
interface StackFrame {
sourceID: string
name: string
line: number
column: number
url: string
expressionLocation?: ExprLocation
}
interface Sample {
timestamp: number
stackFrames: StackFrame[]
}
interface Recording {
displayName: string
startTime: number
endTime: number
discontinuities: any[]
instrumentTypes: string[]
records: Record[]
markers: any[]
memoryPressureEvents: any[]
sampleStackTraces: Sample[]
sampleDurations: number[]
}
interface Overview {
secondsPerPixel: number
scrollStartTime: number
selectionStartTime: number
selectionDuration: number
}
interface SafariProfile {
version: number
recording: Recording
overview: Overview
}
function makeStack(frames: StackFrame[]): FrameInfo[] {
return frames
.map(({name, url, line, column}) => ({
key: `${name}:${url}:${line}:${column}`,
file: url,
line,
col: column,
name: name || '(anonymous)',
}))
.reverse()
}
export function importFromSafari(contents: SafariProfile): Profile | null {
if (contents.version !== 1) {
console.warn(`Unknown Safari profile version ${contents.version}... Might be incompatible.`)
}
const {recording} = contents
const {sampleStackTraces, sampleDurations} = recording
const count = sampleStackTraces.length
if (count < 1) {
console.warn('Empty profile')
return null
}
const profileDuration =
sampleStackTraces[count - 1].timestamp - sampleStackTraces[0].timestamp + sampleDurations[0]
const profile = new StackListProfileBuilder(profileDuration)
let previousEndTime = Number.MAX_VALUE
sampleStackTraces.forEach((sample, i) => {
const endTime = sample.timestamp
const duration = sampleDurations[i]
const startTime = endTime - duration
const idleDurationBefore = startTime - previousEndTime
// FIXME: 2ms is a lot, but Safari's timestamps and durations don't line up very well and will create
// phantom idle time
if (idleDurationBefore > 0.002) {
profile.appendSampleWithWeight([], idleDurationBefore)
}
profile.appendSampleWithWeight(makeStack(sample.stackFrames), duration)
previousEndTime = endTime
})
profile.setValueFormatter(new TimeFormatter('seconds'))
profile.setName(recording.displayName)
return profile.build()
}
+33
View File
@@ -0,0 +1,33 @@
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')
})
test('importTraceEvents bad E events', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/too-many-end-events.json')
})
test('importTraceEvents event re-ordering', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/must-retain-original-order.json')
})
+323
View File
@@ -0,0 +1,323 @@
import {sortBy, zeroPad, lastOf} 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
}
type TraceEventProfileState = {profile: CallTreeProfileBuilder; eventStack: BTraceEvent[]}
function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
const stateByPidTid = new Map<string, TraceEventProfileState>()
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
if (a.pid < b.pid) return -1
if (a.pid > b.pid) return 1
if (a.tid < b.tid) return -1
if (a.tid > b.tid) return 1
// We have to be careful with events that have the same timestamp
// and the same pid/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 call-stack.
if (a.ph === 'B' && b.ph === 'E') return 1
if (a.ph === 'E' && b.ph === 'B') return -1
}
// In all other cases, retain the original sort order.
return 0
})
if (durationEvents.length > 0) {
const firstTs = durationEvents[0].ts
for (let ev of durationEvents) {
ev.ts -= firstTs
}
}
function getOrCreateProfileState(pid: number, tid: number): TraceEventProfileState {
// 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 state = stateByPidTid.get(pidTid)
if (state != null) return state
let profile = new CallTreeProfileBuilder()
state = {profile, eventStack: []}
profile.setValueFormatter(new TimeFormatter('microseconds'))
stateByPidTid.set(pidTid, state)
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 state
}
for (let ev of durationEvents) {
const {profile, eventStack} = getOrCreateProfileState(ev.pid, ev.tid)
const key = keyForEvent(ev)
const frameInfo: FrameInfo = {
key: key,
name: key,
}
switch (ev.ph) {
case 'B':
eventStack.push(ev)
profile.enterFrame(frameInfo, ev.ts)
break
case 'E':
const lastEvent = lastOf(eventStack)
if (lastEvent != null && lastEvent.name === ev.name) {
profile.leaveFrame(frameInfo, ev.ts)
eventStack.pop()
} else {
console.warn(
'Event discarded because it did not match top-of-stack. Discarded event:',
ev,
'Top of stack:',
lastEvent,
)
}
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(stateByPidTid.entries())
sortBy(profilePairs, p => p[0])
return {name: '', indexToView: 0, profiles: profilePairs.map(p => p[1].profile)}
}
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
}
}
+103
View File
@@ -0,0 +1,103 @@
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()
// By default, we assume the file is utf-8 encoded.
let encoding = 'utf-8'
const array = new Uint8Array(buffer)
if (array.length > 2) {
if (array[0] === 0xff && array[1] === 0xfe) {
// UTF-16, Little Endian encoding
encoding = 'utf-16le'
} else if (array[0] === 0xfe && array[1] === 0xff) {
// UTF-16, Big Endian encoding
encoding = 'utf-16be'
}
}
if (typeof TextDecoder !== 'undefined') {
const decoder = new TextDecoder(encoding)
return decoder.decode(buffer)
} else {
// JavaScript strings are UTF-16 encoded, but we're reading data from disk
// that we're going to blindly assume it's ASCII encoded. This codepath
// only exists for older browser support.
console.warn('This browser does not support TextDecoder. Decoding text as ASCII.')
let ret: string = ''
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))
}
}
+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),
}
}
+9
View File
@@ -0,0 +1,9 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importV8HeapAlloc from Chrome', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/69/Heap-20181005T144546.heapprofile')
})
test('importV8HeapAlloc from NodeJS', async () => {
await checkProfileSnapshot('./sample/profiles/node/10.11.0/Heap-20181003T105432.heapprofile')
})
+109
View File
@@ -0,0 +1,109 @@
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
import {getOrInsert} from '../lib/utils'
import {ByteFormatter} from '../lib/value-formatters'
/**
* The V8 Heap Allocation profile is a way to represent heap allocation for each
* javascript function. The format is a simple tree where the weight of each node
* represent the memory allocated by the function and all its callee.
* You can find more information on how to get a profile there :
* https://developers.google.com/web/tools/chrome-devtools/memory-problems/#allocation-profile
* You need to scroll down to "Investigate memory allocation by function"
*
* Note that Node.JS can retrieve this kind of profile via the Inspector protocol.
*/
interface HeapProfileCallFrame {
columnNumber: number
functionName: string
lineNumber: number
scriptId: string
url: string
}
interface HeapProfileNode {
callFrame: HeapProfileCallFrame
selfSize: number
children: HeapProfileNode[]
id: number
parent?: number
totalSize: number
}
interface HeapProfile {
head: HeapProfileNode
}
const callFrameToFrameInfo = new Map<HeapProfileCallFrame, FrameInfo>()
function frameInfoForCallFrame(callFrame: HeapProfileCallFrame) {
return getOrInsert(callFrameToFrameInfo, callFrame, callFrame => {
const name = callFrame.functionName || '(anonymous)'
const file = callFrame.url
const line = callFrame.lineNumber
const col = callFrame.columnNumber
return {
key: `${name}:${file}:${line}:${col}`,
name,
file,
line,
col,
}
})
}
export function importFromChromeHeapProfile(chromeProfile: HeapProfile): Profile {
const nodeById = new Map<number, HeapProfileNode>()
let currentId = 0
const computeId = (node: HeapProfileNode, parent?: HeapProfileNode) => {
node.id = currentId++
nodeById.set(node.id, node)
if (parent) {
node.parent = parent.id
}
node.children.forEach(children => computeId(children, node))
}
computeId(chromeProfile.head)
// Compute the total size
const computeTotalSize = (node: HeapProfileNode): number => {
if (node.children.length === 0) return node.selfSize || 0
const totalChild = node.children.reduce((total: number, children) => {
total += computeTotalSize(children)
return total
}, node.selfSize)
node.totalSize = totalChild
return totalChild
}
const total = computeTotalSize(chromeProfile.head)
// Compute all stacks by taking each last node and going upward
const stacks: HeapProfileNode[][] = []
for (let currentNode of nodeById.values()) {
let stack: HeapProfileNode[] = []
stack.push(currentNode)
// While we found a parent
while (true) {
if (currentNode.parent === undefined) break
const parent = nodeById.get(currentNode.parent)
if (parent === undefined) break
// Push the parent at the beginning of the stack
stack.unshift(parent)
currentNode = parent
}
stacks.push(stack)
}
const profile = new StackListProfileBuilder(total)
for (let stack of stacks) {
const lastFrame = stack[stack.length - 1]
profile.appendSampleWithWeight(
stack.map(frame => frameInfoForCallFrame(frame.callFrame)),
lastFrame.selfSize,
)
}
profile.setValueFormatter(new ByteFormatter())
return profile.build()
}
+57
View File
@@ -0,0 +1,57 @@
// Versions of node before 10 had an unstable sort. This isn't really an issue in browsers
// that speedscope supports, but for the purposes of supporting node 10, we'll polyfill
// a stable sort to make the tests pass.
//
// See:
// - https://v8.dev/features/stable-sort
// - https://v8.dev/blog/array-sort
// - https://github.com/jlfwong/speedscope/pull/254#issuecomment-575116995
//
// Once we stop supporting node 10, this can be removed.
//
// An alternative would be to change our sort implementation to be stable by definition
// rather than relying upon native sort being stable. I don't want to do that because
// we'd take a perf hit.
//
// Because we're not going to use this in our actual build, it's okay for this
// to be inefficient.
;(function () {
const nodeVersion = process.versions.node
const versionParts = nodeVersion.split('.')
const majorVersion = parseInt(versionParts[0], 10)
if (majorVersion > 10) {
// Don't need to do the patch for newer node versions
return
}
const defaultCompareFunction = (a, b) => {
const sa = '' + a
const sb = '' + b
if (sa < sb) return -1
if (sa > sb) return 1
return 0
}
const originalSort = Array.prototype.sort
Array.prototype.sort = function (compareFunction) {
const arrayWithIndices = this.map((x, i) => [x, i])
originalSort.call(arrayWithIndices, (a, b) => {
if (!compareFunction) {
compareFunction = defaultCompareFunction
}
const res = compareFunction(a[0], b[0])
if (res !== 0) return res
return a[1] < b[1] ? -1 : 1
})
this.splice(0, this.length, ...arrayWithIndices.map(x => x[0]))
return this
}
})()
;(function () {
// TextDecoder is a global API in browsers, but an imported API in node.
//
// Let's emulate it being a global API during tests.
global.TextDecoder = require('util').TextDecoder
})()
@@ -211,3 +211,7 @@ Object {
exports[`importSpeedscopeProfiles 0.6.0 multiple profiles: indexToView 1`] = `1`;
exports[`importSpeedscopeProfiles 0.6.0 multiple profiles: profileGroup.name 1`] = `"Two Samples"`;
exports[`importSpeedscopeProfiles invalid due to incomplete trace 1`] = `"Tried to complete profile construction with a non-empty stack"`;
exports[`importSpeedscopeProfiles invalid due to out of order events 1`] = `"Tried to leave frame \\"B\\" while frame \\"A\\" was at the top at 4"`;
+69
View File
@@ -0,0 +1,69 @@
// This file contains a collection of classes which make it easier to perform
// batch rendering of Canvas2D primitives. The advantage of this over just doing
// ctx.beginPath() ... ctx.rect(...) ... ctx.endPath() is that you can construct
// several different batch renderers are the same time, then decide on their
// paint order at the end.
//
// See FlamechartPanZoomView.renderOverlays for an example of how this is used.
export interface TextArgs {
text: string
x: number
y: number
}
export class BatchCanvasTextRenderer {
private argsBatch: TextArgs[] = []
text(args: TextArgs) {
this.argsBatch.push(args)
}
fill(ctx: CanvasRenderingContext2D, color: string) {
if (this.argsBatch.length === 0) return
ctx.fillStyle = color
for (let args of this.argsBatch) {
ctx.fillText(args.text, args.x, args.y)
}
this.argsBatch = []
}
}
export interface RectArgs {
x: number
y: number
w: number
h: number
}
export class BatchCanvasRectRenderer {
private argsBatch: RectArgs[] = []
rect(args: RectArgs) {
this.argsBatch.push(args)
}
private drawPath(ctx: CanvasRenderingContext2D) {
ctx.beginPath()
for (let args of this.argsBatch) {
ctx.rect(args.x, args.y, args.w, args.h)
}
ctx.closePath()
this.argsBatch = []
}
fill(ctx: CanvasRenderingContext2D, color: string) {
if (this.argsBatch.length === 0) return
ctx.fillStyle = color
this.drawPath(ctx)
ctx.fill()
}
stroke(ctx: CanvasRenderingContext2D, color: string, lineWidth: number) {
if (this.argsBatch.length === 0) return
ctx.strokeStyle = color
ctx.lineWidth = lineWidth
this.drawPath(ctx)
ctx.stroke()
}
}
+9 -9
View File
@@ -15,19 +15,19 @@ export class Color {
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue
const hPrime = H / 60
const X = C * (1 - Math.abs(hPrime % 2 - 1))
const X = C * (1 - Math.abs((hPrime % 2) - 1))
const [R1, G1, B1] =
hPrime < 1
? [C, X, 0]
: hPrime < 2
? [X, C, 0]
: hPrime < 3
? [0, C, X]
: hPrime < 4
? [0, X, C]
: hPrime < 5
? [X, 0, C]
: [C, 0, X]
? [X, C, 0]
: hPrime < 3
? [0, C, X]
: hPrime < 4
? [0, X, C]
: hPrime < 5
? [X, 0, C]
: [C, 0, X]
const m = L - (0.3 * R1 + 0.59 * G1 + 0.11 * B1)
+23 -2
View File
@@ -11,7 +11,13 @@ test('importEmscriptenSymbolMap', () => {
'c:C',
].join('\n'),
),
).toEqual(new Map([['a', 'A'], ['b', 'B'], ['c', 'C']]))
).toEqual(
new Map([
['a', 'A'],
['b', 'B'],
['c', 'C'],
]),
)
// Valid symbol map with trailing newline
expect(
@@ -25,7 +31,14 @@ test('importEmscriptenSymbolMap', () => {
'',
].join('\n'),
),
).toEqual(new Map([['a', 'A'], ['b', 'B'], ['c', 'C'], ['d', 'D-D']]))
).toEqual(
new Map([
['a', 'A'],
['b', 'B'],
['c', 'C'],
['d', 'D-D'],
]),
)
// Valid symbol map with non-alpha characters
expect(importEmscriptenSymbolMap('u6:__ZN8tinyxml210XMLCommentD0Ev\n')).toEqual(
@@ -41,6 +54,10 @@ test('importEmscriptenSymbolMap', () => {
'1:B',
'2:C',
'3:D-D',
'4:a\\20b',
'5:a\\2',
'6:a\\3z',
'7:a\\20b\\20c',
].join('\n'),
),
).toEqual(
@@ -49,6 +66,10 @@ test('importEmscriptenSymbolMap', () => {
['wasm-function[1]', 'B'],
['wasm-function[2]', 'C'],
['wasm-function[3]', 'D-D'],
['wasm-function[4]', 'a b'],
['wasm-function[5]', 'a\\2'],
['wasm-function[6]', 'a\\3z'],
['wasm-function[7]', 'a b c'],
]),
)
+13 -3
View File
@@ -1,5 +1,15 @@
type EmscriptenSymbolMap = Map<string, string>
// Returns `input` with hex escapes expanded (e.g. `\20` becomes ` `.)
//
// NOTE: This will fail to ignore escaped backslahes (e.g. `\\20`).
function unescapeHex(input: string): string {
return input.replace(/\\([a-fA-F0-9]{2})/g, (_match, group) => {
const scalar = parseInt(group, 16)
return String.fromCharCode(scalar)
})
}
// This imports symbol maps generated by emscripten using the "--emit-symbol-map" flag.
// It allows you to visualize a profile captured in a release build as long as you also
// have the associated symbol map. To do this, first drop the profile into speedscope
@@ -14,21 +24,21 @@ export function importEmscriptenSymbolMap(contents: string): EmscriptenSymbolMap
if (!lines.length) return null
const map: EmscriptenSymbolMap = new Map()
const intRegex = /^(\d+):([\$\w-]+)$/
const intRegex = /^(\d+):(.+)$/
const idRegex = /^([\$\w]+):([\$\w-]+)$/
for (const line of lines) {
// Match lines like "103:__ZN8tinyxml210XMLCommentD0Ev"
const intMatch = intRegex.exec(line)
if (intMatch) {
map.set(`wasm-function[${intMatch[1]}]`, intMatch[2])
map.set(`wasm-function[${intMatch[1]}]`, unescapeHex(intMatch[2]))
continue
}
// Match lines like "u6:__ZN8tinyxml210XMLCommentD0Ev"
const idMatch = idRegex.exec(line)
if (idMatch) {
map.set(idMatch[1], idMatch[2])
map.set(idMatch[1], unescapeHex(idMatch[2]))
continue
}
+11 -2
View File
@@ -1,6 +1,6 @@
import {checkProfileSnapshot} from './test-utils'
import {checkProfileSnapshot, expectImportFailure} from './test-utils'
describe('importSpeedscopeProfiles', async () => {
describe('importSpeedscopeProfiles', () => {
test('0.0.1 evented profile', async () => {
await checkProfileSnapshot('./sample/profiles/speedscope/0.0.1/simple.speedscope.json')
})
@@ -12,4 +12,13 @@ describe('importSpeedscopeProfiles', async () => {
test('0.6.0 multiple profiles', async () => {
await checkProfileSnapshot('./sample/profiles/speedscope/0.6.0/two-sampled.speedscope.json')
})
test('invalid due to out of order events', async () => {
// See: https://github.com/jlfwong/speedscope/issues/272
await expectImportFailure('./sample/profiles/speedscope/invalid/out-of-order-events.json')
})
test('invalid due to incomplete trace', async () => {
await expectImportFailure('./sample/profiles/speedscope/invalid/incomplete-trace.json')
})
})
+4 -1
View File
@@ -138,7 +138,10 @@ function importSpeedscopeProfile(
for (let i = 0; i < samples.length; i++) {
const stack = samples[i]
const weight = weights[i]
profile.appendSampleWithWeight(stack.map(n => frameInfos[n]), weight)
profile.appendSampleWithWeight(
stack.map(n => frameInfos[n]),
weight,
)
}
return profile.build()
+22 -1
View File
@@ -1,7 +1,7 @@
import {Frame, CallTreeNode} from './profile'
import {lastOf} from './utils'
import {clamp} from './math'
import {clamp, Rect, Vec2} from './math'
export interface FlamechartFrame {
node: CallTreeNode
@@ -90,6 +90,27 @@ export class Flamechart {
return clamp(viewportWidth, minWidth, maxWidth)
}
// Given a desired config-space viewport rectangle, clamp the rectangle so
// that it fits within the given flamechart. This prevents the viewport from
// extending past the bounds of the flamechart or zooming in too far.
getClampedConfigSpaceViewportRect({
configSpaceViewportRect,
renderInverted,
}: {
configSpaceViewportRect: Rect
renderInverted?: boolean
}) {
const configSpaceSize = new Vec2(this.getTotalWeight(), this.getLayers().length)
const width = this.getClampedViewportWidth(configSpaceViewportRect.size.x)
const size = configSpaceViewportRect.size.withX(width)
const origin = Vec2.clamp(
configSpaceViewportRect.origin,
new Vec2(0, renderInverted ? 0 : -1),
Vec2.max(Vec2.zero, configSpaceSize.minus(size).plus(new Vec2(0, 1))),
)
return new Rect(origin, configSpaceViewportRect.size.withX(width))
}
constructor(private source: FlamechartDataSource) {
const stack: FlamechartFrame[] = []
const openFrame = (node: CallTreeNode, value: number) => {
+81
View File
@@ -0,0 +1,81 @@
import {fuzzyMatchStrings} from './fuzzy-find'
import {sortBy} from './utils'
function assertMatches(texts: string[], pattern: string, expectedResults: string[]) {
const results: {score: number; highlighted: string}[] = []
for (let text of texts) {
const match = fuzzyMatchStrings(text, pattern)
if (match == null) {
continue
}
let highlighted = ''
let last = 0
for (let range of match.matchedRanges) {
highlighted += `${text.slice(last, range[0])}[${text.slice(range[0], range[1])}]`
last = range[1]
}
highlighted += text.slice(last)
results.push({score: match.score, highlighted})
}
// Sort scores in descending order
sortBy(results, r => -r.score)
expect(results.map(r => r.highlighted)).toEqual(expectedResults)
}
function assertMatch(text: string, pattern: string, expected: string) {
assertMatches([text], pattern, [expected])
}
function assertNoMatch(text: string, pattern: string) {
assertMatches([text], pattern, [])
}
describe('fuzzyMatchStrings', () => {
test('no match', () => {
assertNoMatch('a', 'b')
assertNoMatch('aa', 'ab')
assertNoMatch('a', 'aa')
assertNoMatch('ca', 'ac')
})
test('full text match', () => {
assertMatch('hello', 'hello', '[hello]')
assertMatch('multiple words', 'multiple words', '[multiple words]')
})
test('case sensitivity', () => {
assertMatch('HELLO', 'hello', '[HELLO]')
assertMatch('Hello', 'hello', '[Hello]')
assertNoMatch('hello', 'Hello')
assertNoMatch('hello', 'HELLO')
})
test('multiple occurrences', () => {
assertMatch('hello hello', 'hello', '[hello] hello')
assertMatch('hellohello', 'hello', '[hello]hello')
})
test('prefer earlier matches', () => {
assertMatches(['cab', 'ab'], 'ab', ['[ab]', 'c[ab]'])
})
test('prefer shorter matches', () => {
assertMatches(['abbc', 'abc', 'abbbc'], 'ac', ['[a]b[c]', '[a]bb[c]', '[a]bbb[c]'])
})
test('prefer word boundaries', () => {
assertMatches(['abc', 'a c'], 'ac', ['[a] [c]', '[a]b[c]'])
})
test('prefer camelCase matches', () => {
assertMatches(['downtown', 'OutNode'], 'n', ['Out[N]ode', 'dow[n]town'])
})
test('prefer number prefix matches', () => {
assertMatches(['211', 'a123'], '1', ['a[1]23', '2[1]1'])
})
})
+246
View File
@@ -0,0 +1,246 @@
/**
* This file contains an implementation of fuzzy string matching.
*/
export interface FuzzyMatch {
// List of [start, end] indices in the haystack string that match the needle string
matchedRanges: [number, number][]
// The score of the match for relative ranking. Higher scores indicate
// "better" matches.
score: number
}
export function fuzzyMatchStrings(text: string, pattern: string): FuzzyMatch | null {
return fzfFuzzyMatchV1(text, pattern)
}
// The implementation here is based on FuzzyMatchV1, as described here:
// https://github.com/junegunn/fzf/blob/f81feb1e69e5cb75797d50817752ddfe4933cd68/src/algo/algo.go#L8-L15
//
// This is a hand-port to better understand what the code is doing and for added
// clarity.
//
// Capitalized letters only match capitalized letters, but lower-case letters
// match both.
//
// Note: fzf includes a normalization table for homoglyphs. I'm going to ignore that too
// https://github.com/junegunn/fzf/blob/master/src/algo/normalize.go
const charCodeLowerA = 'a'.charCodeAt(0)
const charCodeLowerZ = 'z'.charCodeAt(0)
const charCodeUpperA = 'A'.charCodeAt(0)
const charCodeUpperZ = 'Z'.charCodeAt(0)
const charCodeDigit0 = '0'.charCodeAt(0)
const charCodeDigit9 = '9'.charCodeAt(0)
enum fzfCharClass {
charNonWord,
charLower,
charUpper,
charNumber,
}
function fzfCharClassOf(char: string): fzfCharClass {
const code = char.charCodeAt(0)
if (charCodeLowerA <= code && code <= charCodeLowerZ) {
return fzfCharClass.charLower
} else if (charCodeUpperA <= code && code <= charCodeUpperZ) {
return fzfCharClass.charUpper
} else if (charCodeDigit0 <= code && code <= charCodeDigit9) {
return fzfCharClass.charNumber
}
return fzfCharClass.charNonWord
}
function charsMatch(textChar: string, patternChar: string): boolean {
if (textChar === patternChar) return true
const patternCharCode = patternChar.charCodeAt(0)
if (charCodeLowerA <= patternCharCode && patternCharCode <= charCodeLowerZ) {
return textChar.charCodeAt(0) === patternCharCode - charCodeLowerA + charCodeUpperA
}
return false
}
function fzfFuzzyMatchV1(text: string, pattern: string): FuzzyMatch | null {
if (pattern.length == 0) {
return {matchedRanges: [], score: 0}
}
// I removed the fzfAsciiFuzzyIndex code because it's not actually clear to
// me that it's a very helpful optimization.
let pidx = 0
let sidx = -1
let eidx = -1
let lenRunes = text.length
let lenPattern = pattern.length
// Forward pass: scan over the text pattern, identifying the earliest start
// and the latest end to consider.
for (let index = 0; index < lenRunes; index++) {
let char = text[index]
let pchar = pattern[pidx]
if (charsMatch(char, pchar)) {
if (sidx < 0) {
sidx = index
}
pidx++
if (pidx == lenPattern) {
// We found the last character in the pattern! eidx is exclusive, so
// we'll set it to the current index + 1.
eidx = index + 1
break
}
}
}
if (eidx == -1) {
// We couldn't find all the characters in the pattern. No match.
return null
}
// Assuming we found all the characters in the pattern, perform the backwards
// pass.
pidx--
for (let index = eidx - 1; index >= sidx; index--) {
const char = text[index]
const pchar = pattern[pidx]
if (charsMatch(char, pchar)) {
pidx--
if (pidx < 0) {
// We found the first character of the pattern, scanning
// backwards. This *may* have narrowed the match further.
// For example, for the following inputs:
//
// text = "xxx a b c abc xxx"
// pattern = "abc"
//
// For the forward pass, you get:
//
// "xxx a b c abc xxx"
// start^ ^end
//
// But after the backward pass, we can narrow this to:
//
// "xxx a b c abc xxx"
// start^ ^end
sidx = index
return fzfCalculateScore(text, pattern, sidx, eidx)
}
}
}
// This should be unreachable.
throw new Error('Implementation error. This must be a bug in fzfFuzzyMatchV1')
}
const fzfScoreMatch = 16
const fzfScoreGapStart = -3
const fzfScoreGapExtension = -1
const fzfBonusBoundary = fzfScoreMatch / 2
const fzfBonusNonWord = fzfScoreMatch / 2
const fzfBonusCamel123 = fzfBonusBoundary + fzfScoreGapExtension
const fzfBonusConsecutive = -(fzfScoreGapStart + fzfScoreGapExtension)
const fzfBonusFirstCharMultiplier = 2
function bonusFor(prevClass: fzfCharClass, curClass: fzfCharClass): number {
if (prevClass === fzfCharClass.charNonWord && curClass !== fzfCharClass.charNonWord) {
// Prefer matching at word boundaries
//
// This should prefer "a c" over "abc" for a pattern of "ac".
return fzfBonusBoundary
}
if (
(prevClass === fzfCharClass.charLower && curClass == fzfCharClass.charUpper) ||
(prevClass !== fzfCharClass.charNumber && curClass == fzfCharClass.charNumber)
) {
// Prefer matching at the transition point between lower & upper for camelCase,
// and from transition from letter to number for identifiers like letter123.
//
// This should prefer "OutNode" over "phone" for a pattern of "n",
// and "abc123" over "x211" for a pattern of "1".
return fzfBonusCamel123
}
if (curClass === fzfCharClass.charNonWord) {
return fzfBonusNonWord
}
return 0
}
function fzfCalculateScore(text: string, pattern: string, sidx: number, eidx: number): FuzzyMatch {
let pidx = 0
let score = 0
let inGap = false
let consecutive = 0
let firstBonus = 0
let pos: number[] = new Array(pattern.length)
let prevClass = fzfCharClass.charNonWord
if (sidx > 0) {
prevClass = fzfCharClassOf(text[sidx - 1])
}
for (let idx = sidx; idx < eidx; idx++) {
let char = text[idx]
let curClass = fzfCharClassOf(char)
if (charsMatch(char, pattern[pidx])) {
pos[pidx] = idx
score += fzfScoreMatch
let bonus = bonusFor(prevClass, curClass)
if (consecutive == 0) {
firstBonus = bonus
} else {
// Break consecutive chunk
if (bonus === fzfBonusBoundary) {
firstBonus = bonus
}
bonus = Math.max(bonus, firstBonus, fzfBonusConsecutive)
}
if (pidx === 0) {
score += bonus * fzfBonusFirstCharMultiplier
} else {
score += bonus
}
inGap = false
consecutive++
pidx++
} else {
if (inGap) {
// Penalize gaps (this bonus is negative)
score += fzfScoreGapExtension
} else {
// Penalize the beginning of gaps more harshly
score += fzfScoreGapStart
}
inGap = true
consecutive = 0
firstBonus = 0
}
prevClass = curClass
}
if (pidx !== pattern.length) {
throw new Error(
'fzfCalculateScore should only be called when pattern is found between sidx and eidx',
)
}
let matchedRanges: [number, number][] = [[pos[0], pos[0] + 1]]
for (let i = 1; i < pos.length; i++) {
const curPos = pos[i]
const curRange = matchedRanges[matchedRanges.length - 1]
if (curRange[1] === curPos) {
curRange[1] = curPos + 1
} else {
matchedRanges.push([curPos, curPos + 1])
}
}
return {
score,
matchedRanges,
}
}
+25 -45
View File
@@ -13,9 +13,10 @@ test('clamp', () => {
// Change this to jsc.integer to debug failures more easily
let numericType = jsc.number
const arbitraryVec2 = jsc
.record({x: jsc.integer, y: numericType})
.smap(v => new Vec2(v.x, v.y), v => v)
const arbitraryVec2 = jsc.record({x: jsc.integer, y: numericType}).smap(
v => new Vec2(v.x, v.y),
v => v,
)
const positiveVec2 = jsc.suchthat(arbitraryVec2, v => v.x > 0 && v.y > 0)
@@ -28,23 +29,25 @@ const arbitraryTransform = jsc
m11: numericType,
m12: numericType,
})
.smap(t => new AffineTransform(t.m00, t.m01, t.m02, t.m10, t.m11, t.m12), t => t)
.smap(
t => new AffineTransform(t.m00, t.m01, t.m02, t.m10, t.m11, t.m12),
t => t,
)
const invertibleTransform = jsc.suchthat(arbitraryTransform, t => t.det() != 0)
const simpleTransform = jsc.suchthat(
jsc
.record({scale: arbitraryVec2, translation: arbitraryVec2})
.smap(
t => AffineTransform.withScale(t.scale).withTranslation(t.translation),
t => ({scale: t.getScale(), translation: t.getTranslation()}),
),
jsc.record({scale: arbitraryVec2, translation: arbitraryVec2}).smap(
t => AffineTransform.withScale(t.scale).withTranslation(t.translation),
t => ({scale: t.getScale(), translation: t.getTranslation()}),
),
t => t.det() != 0,
)
const arbitraryRect = jsc
.record({origin: arbitraryVec2, size: positiveVec2})
.smap(r => new Rect(r.origin, r.size), r => r)
const arbitraryRect = jsc.record({origin: arbitraryVec2, size: positiveVec2}).smap(
r => new Rect(r.origin, r.size),
r => r,
)
describe('Vec2', () => {
test('constructor', () => {
@@ -263,51 +266,35 @@ describe('AffineTransform', () => {
expect(new AffineTransform(0, 0, 0, 0, 0, 0).inverted()).toBe(null)
jsc.assertForall(invertibleTransform, t => {
return t
.inverted()!
.inverted()!
.approxEquals(t)
return t.inverted()!.inverted()!.approxEquals(t)
})
})
test('translation', () => {
jsc.assertForall(arbitraryTransform, arbitraryVec2, (t, v1) => {
return t
.withTranslation(v1)
.getTranslation()
.equals(v1)
return t.withTranslation(v1).getTranslation().equals(v1)
})
jsc.assertForall(arbitraryTransform, arbitraryVec2, (t, v1) => {
const initialTranslation = t.getTranslation()
return t
.translatedBy(v1)
.getTranslation()
.approxEquals(initialTranslation.plus(v1))
return t.translatedBy(v1).getTranslation().approxEquals(initialTranslation.plus(v1))
})
})
test('scale', () => {
jsc.assertForall(arbitraryTransform, arbitraryVec2, (t, v1) => {
return t
.withScale(v1)
.getScale()
.equals(v1)
return t.withScale(v1).getScale().equals(v1)
})
})
test('transformVector', () => {
// Vector transformation are translation-invariant
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
return AffineTransform.withTranslation(v1)
.transformVector(v2)
.approxEquals(v2)
return AffineTransform.withTranslation(v1).transformVector(v2).approxEquals(v2)
})
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
return AffineTransform.withScale(v1)
.transformVector(v2)
.approxEquals(v2.timesPointwise(v1))
return AffineTransform.withScale(v1).transformVector(v2).approxEquals(v2.timesPointwise(v1))
})
})
@@ -319,15 +306,11 @@ describe('AffineTransform', () => {
test('transformPosition', () => {
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
return AffineTransform.withTranslation(v1)
.transformPosition(v2)
.approxEquals(v2.plus(v1))
return AffineTransform.withTranslation(v1).transformPosition(v2).approxEquals(v2.plus(v1))
})
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
return AffineTransform.withScale(v1)
.transformPosition(v2)
.approxEquals(v2.timesPointwise(v1))
return AffineTransform.withScale(v1).transformPosition(v2).approxEquals(v2.timesPointwise(v1))
})
})
@@ -359,10 +342,7 @@ describe('AffineTransform', () => {
test('times', () => {
jsc.assertForall(invertibleTransform, invertibleTransform, (t1, t2) => {
return t1
.times(t2)
.times(t2.inverted()!)
.approxEquals(t1)
return t1.times(t2).times(t2.inverted()!).approxEquals(t1)
})
})
})
+74
View File
@@ -0,0 +1,74 @@
/**
* As of Preact 10.x, they no longer have an officially supported preact-redux library.
* It's possible to use react-redux with some hacks, but these hacks cause npm run pack
* to error out because of (intentionally) unmet peer dependencies.
*
* I could stack more hacks to fix this problem, but I'd rather just drop the dependency
* and remove the need to do any dependency hacking by writing the very small part of
* react-redux that I actually need myself.
*/
import {h} from 'preact'
import * as redux from 'redux'
import {createContext, ComponentChildren} from 'preact'
import {Dispatch, Action} from './typed-redux'
import {useState, useContext, useCallback, useLayoutEffect} from 'preact/hooks'
const PreactRedux = createContext<redux.Store<any> | null>(null)
interface ProviderProps {
store: redux.Store<any>
children?: ComponentChildren
}
export function Provider(props: ProviderProps) {
return <PreactRedux.Provider value={props.store} children={props.children} />
}
function useStore<T>(): redux.Store<T> {
const store = useContext(PreactRedux)
if (store == null) {
throw new Error('Called useStore when no store exists in context')
}
return store
}
export function useDispatch(): Dispatch {
const store = useStore()
return store.dispatch
}
export function useActionCreator<T, U>(
creator_: (payload: T) => Action<U>,
cacheArgs: any[],
): (t: T) => void {
const dispatch = useDispatch()
/* eslint-disable react-hooks/exhaustive-deps */
const creator = useCallback(creator_, cacheArgs)
return useCallback((t: T) => dispatch(creator(t)), [dispatch, creator])
}
export function useSelector<T, U>(selector_: (t: T) => U, cacheArgs: any[]): U {
const store = useStore<T>()
/* eslint-disable react-hooks/exhaustive-deps */
const selector = useCallback(selector_, cacheArgs)
const getValueFromStore = useCallback(() => selector(store.getState()), [store, selector])
const [value, setValue] = useState(getValueFromStore)
useLayoutEffect(() => {
// We need to setValue here because it's possible something has changed the
// value in the store between the useSelector call and layout. In most cases
// this should no-op.
setValue(getValueFromStore())
return store.subscribe(() => {
setValue(getValueFromStore())
})
}, [store, getValueFromStore])
return value
}
+90
View File
@@ -0,0 +1,90 @@
import {Profile, Frame, CallTreeNode} from './profile'
import {FuzzyMatch, fuzzyMatchStrings} from './fuzzy-find'
import {Flamechart, FlamechartFrame} from './flamechart'
import {Rect, Vec2} from './math'
export enum FlamechartType {
CHRONO_FLAME_CHART,
LEFT_HEAVY_FLAME_GRAPH,
}
// A utility class for storing cached search results to avoid recomputation when
// the search results & profile did not change.
export class ProfileSearchResults {
constructor(readonly profile: Profile, readonly searchQuery: string) {}
private matches: Map<Frame, FuzzyMatch> | null = null
getMatchForFrame(frame: Frame): FuzzyMatch | null {
if (!this.matches) {
this.matches = new Map()
this.profile.forEachFrame(frame => {
const match = fuzzyMatchStrings(frame.name, this.searchQuery)
if (match == null) return
this.matches!.set(frame, match)
})
}
return this.matches.get(frame) || null
}
}
export interface FlamechartSearchMatch {
configSpaceBounds: Rect
node: CallTreeNode
}
interface CachedFlamechartResult {
matches: FlamechartSearchMatch[]
indexForNode: Map<CallTreeNode, number>
}
export class FlamechartSearchResults {
constructor(readonly flamechart: Flamechart, readonly profileResults: ProfileSearchResults) {}
private matches: CachedFlamechartResult | null = null
private getResults(): CachedFlamechartResult {
if (this.matches == null) {
const matches: FlamechartSearchMatch[] = []
const indexForNode = new Map<CallTreeNode, number>()
const visit = (frame: FlamechartFrame, depth: number) => {
const {node} = frame
if (this.profileResults.getMatchForFrame(node.frame)) {
const configSpaceBounds = new Rect(
new Vec2(frame.start, depth),
new Vec2(frame.end - frame.start, 1),
)
indexForNode.set(node, matches.length)
matches.push({configSpaceBounds, node})
}
frame.children.forEach(child => {
visit(child, depth + 1)
})
}
const layers = this.flamechart.getLayers()
if (layers.length > 0) {
layers[0].forEach(frame => visit(frame, 0))
}
this.matches = {matches, indexForNode}
}
return this.matches
}
count(): number {
return this.getResults().matches.length
}
indexOf(node: CallTreeNode): number | null {
const result = this.getResults().indexForNode.get(node)
return result === undefined ? null : result
}
at(index: number): FlamechartSearchMatch {
const matches = this.getResults().matches
if (index < 0 || index >= matches.length) {
throw new Error(`Index ${index} out of bounds in list of ${matches.length} matches.`)
}
return matches[index]
}
}
+49 -46
View File
@@ -26,19 +26,22 @@ function toStackList(profile: Profile, grouped: boolean): string[] {
const curStack: (number | string)[] = []
let lastValue = 0
function openFrame(node: CallTreeNode, value: number) {
function maybeEmit(value: number) {
if (lastValue != value) {
stackList.push(curStack.map(k => `${k}`).join(';'))
stackList.push(
curStack.map(k => `${k}`).join(';') + ` ${profile.formatValue(value - lastValue)}`,
)
lastValue = value
}
curStack.push(node.frame.key)
}
function openFrame(node: CallTreeNode, value: number) {
maybeEmit(value)
curStack.push(node.frame.name)
}
function closeFrame(node: CallTreeNode, value: number) {
if (lastValue != value) {
stackList.push(curStack.map(k => `${k}`).join(';'))
lastValue = value
}
maybeEmit(value)
curStack.pop()
}
@@ -82,49 +85,49 @@ function verifyProfile(profile: Profile) {
expect(toStackList(profile, false)).toEqual([
// prettier-ignore
'a',
'a;b',
'a;b;d',
'a;b;c',
'',
'a',
'a;b',
'a;b;b',
'a;b;e',
'a',
'a 1',
'a;b 2',
'a;b;d 1',
'a;b;c 1',
' 1',
'a 1',
'a;b 1',
'a;b;b 1',
'a;b;e 1',
'a 1',
])
expect(toStackList(profile, true)).toEqual([
// prettier-ignore
'a;b;e',
'a;b;b',
'a;b;c',
'a;b;d',
'a;b',
'a',
'a;b;d 1',
'a;b;c 1',
'a;b;b 1',
'a;b;e 1',
'a;b 3',
'a 3',
])
const flattened = profile.getProfileWithRecursionFlattened()
expect(toStackList(flattened, false)).toEqual([
// prettier-ignore
'a',
'a;b',
'a;b;d',
'a;b;c',
'',
'a',
'a;b',
'a;b;e',
'a',
'a 1',
'a;b 2',
'a;b;d 1',
'a;b;c 1',
' 1',
'a 1',
'a;b 2',
'a;b;e 1',
'a 1',
])
expect(toStackList(flattened, true)).toEqual([
// prettier-ignore
'a;b;e',
'a;b;c',
'a;b;d',
'a;b',
'a',
'a;b;d 1',
'a;b;c 1',
'a;b;e 1',
'a;b 4',
'a 3',
])
}
@@ -259,9 +262,9 @@ test('getInvertedProfileForCallersOf', () => {
expect(toStackList(inverted, false)).toEqual([
// prettier-ignore
'b',
'b;a',
'b;d',
'b 1',
'b;a 3',
'b;d 1',
])
})
@@ -287,10 +290,10 @@ test('getProfileForCalleesOf', () => {
expect(toStackList(inverted, false)).toEqual([
// prettier-ignore
'b',
'b;c',
'b;d',
'b',
'b 2',
'b;c 1',
'b;d 1',
'b 1',
])
})
@@ -313,8 +316,8 @@ test('getProfileWithRecursionFlattened', () => {
expect(toStackList(inverted, false)).toEqual([
// prettier-ignore
'a',
'a;b',
'a 1',
'a;b 3',
])
const framesInProfile = new Set<string | number>()
+33 -14
View File
@@ -116,6 +116,13 @@ export class Profile {
protected frames = new KeyedSet<Frame>()
// Profiles store two call-trees.
//
// The "append order" call tree is the one in which nodes are ordered in
// whatever order they were appended to their parent.
//
// The "grouped" call tree is one in which each node has at most one child per
// frame. Nodes are ordered in decreasing order of weight
protected appendOrderCalltreeRoot = new CallTreeNode(Frame.root, null)
protected groupedCalltreeRoot = new CallTreeNode(Frame.root, null)
@@ -169,6 +176,17 @@ export class Profile {
return this.totalNonIdleWeight
}
// This is private because it should only be called in the ProfileBuilder
// classes. Once a Profile instance has been constructed, it should be treated
// as immutable.
protected sortGroupedCallTree() {
function visit(node: CallTreeNode) {
node.children.sort((a, b) => -(a.getTotalWeight() - b.getTotalWeight()))
node.children.forEach(visit)
}
visit(this.groupedCalltreeRoot)
}
forEachCallGrouped(
openFrame: (node: CallTreeNode, value: number) => void,
closeFrame: (node: CallTreeNode, value: number) => void,
@@ -180,10 +198,7 @@ export class Profile {
let childTime = 0
const children = [...node.children]
children.sort((a, b) => (a.getTotalWeight() > b.getTotalWeight() ? -1 : 1))
children.forEach(function(child) {
node.children.forEach(function (child) {
visit(child, start + childTime)
childTime += child.getTotalWeight()
})
@@ -250,12 +265,6 @@ export class Profile {
this.frames.forEach(fn)
}
forEachSample(fn: (sample: CallTreeNode, weight: number) => void) {
for (let i = 0; i < this.samples.length; i++) {
fn(this.samples[i], this.weights[i])
}
}
getProfileWithRecursionFlattened(): Profile {
const builder = new CallTreeProfileBuilder()
@@ -507,7 +516,11 @@ export class StackListProfileBuilder extends Profile {
this.setValueFormatter(new RawValueFormatter())
}
}
this.totalWeight = Math.max(this.totalWeight, this.weights.reduce((a, b) => a + b, 0))
this.totalWeight = Math.max(
this.totalWeight,
this.weights.reduce((a, b) => a + b, 0),
)
this.sortGroupedCallTree()
return this
}
}
@@ -557,9 +570,7 @@ export class CallTreeProfileBuilder extends Profile {
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}`,
)
}
}
@@ -602,6 +613,13 @@ export class CallTreeProfileBuilder extends Profile {
throw new Error(`Trying to leave a ${frame.key} before any have been entered`)
}
leavingStackTop.freeze()
if (leavingStackTop.frame.key !== frame.key) {
throw new Error(
`Tried to leave frame "${frame.name}" while frame "${leavingStackTop.frame.name}" was at the top at ${value}`,
)
}
const delta = value - this.lastValue
if (delta > 0) {
this.samples.push(leavingStackTop)
@@ -643,6 +661,7 @@ export class CallTreeProfileBuilder extends Profile {
if (this.appendOrderStack.length > 1 || this.groupedOrderStack.length > 1) {
throw new Error('Tried to complete profile construction with a non-empty stack')
}
this.sortGroupedCallTree()
return this
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ export class StatsPanel {
this.msPanel.update(time - this.beginTime, 200)
if (time >= this.prevTime + 1000) {
this.fpsPanel.update(this.frames * 1000 / (time - this.prevTime), 100)
this.fpsPanel.update((this.frames * 1000) / (time - this.prevTime), 100)
this.prevTime = time
this.frames = 0
}
+16 -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
@@ -96,3 +97,14 @@ export async function checkProfileSnapshot(filepath: string) {
const reexported = exportProfileGroup(reimportedGroup)
expect(exported).toEqual(reexported)
}
export async function expectImportFailure(filepath: string) {
const buffer = fs.readFileSync(filepath)
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
try {
await importProfilesFromArrayBuffer(path.basename(filepath), arrayBuffer)
fail('Expected import to fail but it succeeded')
} catch (error) {
expect(error.message).toMatchSnapshot()
}
}
+116
View File
@@ -0,0 +1,116 @@
import {buildTrimmedText, ELLIPSIS, remapRangesToTrimmedText} from './text-utils'
import {fuzzyMatchStrings} from './fuzzy-find'
function assertTrimmed(text: string, length: number, expectedTrimmed: string) {
expect(buildTrimmedText(text, length).trimmedString).toEqual(
expectedTrimmed.replace('...', ELLIPSIS),
)
}
test('buildTrimmedText', () => {
assertTrimmed('hello world', 1, '...')
assertTrimmed('hello world', 2, 'h...')
assertTrimmed('hello world', 3, 'h...d')
assertTrimmed('hello world', 4, 'he...d')
assertTrimmed('hello world', 10, 'hello...orld')
assertTrimmed('hello world', 11, 'hello world')
assertTrimmed('hello world', 100, 'hello world')
})
function highlightText(text: string, highlightedRanges: [number, number][]): string {
let last = 0
let highlighted = ''
for (let range of highlightedRanges) {
highlighted += `${text.slice(last, range[0])}[${text.slice(range[0], range[1])}]`
last = range[1]
}
highlighted += text.slice(last)
return highlighted
}
function assertTrimmedHighlight({
text,
pattern,
expectedHighlighted,
length,
expectedHighlightedTrimmed,
}: {
text: string
pattern: string
expectedHighlighted: string
length: number
expectedHighlightedTrimmed: string
}) {
const match = fuzzyMatchStrings(text, pattern)
const trimmed = buildTrimmedText(text, length)
if (!match) {
fail()
return
}
const matchedRangesForTrimmedText = remapRangesToTrimmedText(trimmed, match.matchedRanges)
const highlighted = highlightText(text, match.matchedRanges)
const highlightedTrimmed = highlightText(trimmed.trimmedString, matchedRangesForTrimmedText)
expect(highlighted).toEqual(expectedHighlighted)
expect(highlightedTrimmed).toEqual(expectedHighlightedTrimmed.replace('...', ELLIPSIS))
}
test('remapRangesToTrimmedText', () => {
assertTrimmedHighlight({
text: 'hello world',
pattern: 'he',
length: 4,
expectedHighlighted: '[he]llo world',
expectedHighlightedTrimmed: `[he]...d`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'o w',
length: 4,
expectedHighlighted: 'hell[o w]orld',
expectedHighlightedTrimmed: `he[...]d`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'ow',
length: 4,
expectedHighlighted: 'hell[o] [w]orld',
expectedHighlightedTrimmed: `he[...]d`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'hello',
length: 4,
expectedHighlighted: '[hello] world',
expectedHighlightedTrimmed: `[he...]d`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'hello world',
length: 4,
expectedHighlighted: '[hello world]',
expectedHighlightedTrimmed: `[he...d]`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'helloworld',
length: 4,
expectedHighlighted: '[hello] [world]',
expectedHighlightedTrimmed: `[he...][d]`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'world',
length: 4,
expectedHighlighted: 'hello [world]',
expectedHighlightedTrimmed: `he[...d]`,
})
})
+174 -8
View File
@@ -18,22 +18,188 @@ export function cachedMeasureTextWidth(ctx: CanvasRenderingContext2D, text: stri
return measureTextCache.get(text)!
}
function buildTrimmedText(text: string, length: number) {
const prefixLength = Math.floor(length / 2)
const prefix = text.substr(0, prefixLength)
const suffix = text.substr(text.length - prefixLength, prefixLength)
return prefix + ELLIPSIS + suffix
interface TrimmedTextResult {
trimmedString: string
trimmedLength: number
prefixLength: number
suffixLength: number
originalLength: number
originalString: string
}
export function trimTextMid(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) return text
// Trim text, placing an ellipsis in the middle, with a slight bias towards
// keeping text from the beginning rather than the end
export function buildTrimmedText(text: string, length: number): TrimmedTextResult {
if (text.length <= length) {
return {
trimmedString: text,
trimmedLength: text.length,
prefixLength: text.length,
suffixLength: 0,
originalString: text,
originalLength: text.length,
}
}
let prefixLength = Math.floor(length / 2)
const suffixLength = length - prefixLength - 1
const prefix = text.substr(0, prefixLength)
const suffix = text.substr(text.length - suffixLength, suffixLength)
const trimmedString = prefix + ELLIPSIS + suffix
return {
trimmedString,
trimmedLength: trimmedString.length,
prefixLength: prefix.length,
suffixLength: suffix.length,
originalString: text,
originalLength: text.length,
}
}
// Trim text to fit within the given number of pixels on the canvas
export function trimTextMid(
ctx: CanvasRenderingContext2D,
text: string,
maxWidth: number,
): TrimmedTextResult {
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) {
return buildTrimmedText(text, text.length)
}
const [lo] = binarySearch(
0,
text.length,
n => {
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n))
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n).trimmedString)
},
maxWidth,
)
return buildTrimmedText(text, lo)
}
enum IndexTypeInTrimmed {
IN_PREFIX,
IN_SUFFIX,
ELIDED,
}
function getIndexTypeInTrimmed(result: TrimmedTextResult, index: number): IndexTypeInTrimmed {
if (index < result.prefixLength) {
return IndexTypeInTrimmed.IN_PREFIX
} else if (index < result.originalLength - result.suffixLength) {
return IndexTypeInTrimmed.ELIDED
} else {
return IndexTypeInTrimmed.IN_SUFFIX
}
}
export function remapRangesToTrimmedText(
trimmedText: TrimmedTextResult,
ranges: [number, number][],
): [number, number][] {
// We intentionally don't just re-run fuzzy matching on the trimmed
// text, beacuse if the search query is "helloWorld", the frame name
// is "application::helloWorld", and that gets trimmed down to
// "appl...oWorld", we still want "oWorld" to be highlighted, even
// though the string "appl...oWorld" is not matched by the query
// "helloWorld".
//
// There's a weird case to consider here: what if the trimmedText is
// also matched by the query, but results in a different match than
// the original query? Consider, e.g. the search string of "ab". The
// string "hello ab shabby" will be matched at the first "ab", but
// may be trimmed to "hello...shabby". In this case, should we
// highlight the "ab" hidden by the ellipsis, or the "ab" in
// "shabby"? The code below highlights the ellipsis so that the
// matched characters don't change as you zoom in and out.
const rangesToHighlightInTrimmedText: [number, number][] = []
const lengthLoss = trimmedText.originalLength - trimmedText.trimmedLength
let highlightedEllipsis = false
for (let [origStart, origEnd] of ranges) {
let startPosType = getIndexTypeInTrimmed(trimmedText, origStart)
let endPosType = getIndexTypeInTrimmed(trimmedText, origEnd - 1)
switch (startPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
switch (endPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
// The entire range fits in the prefix. Add it unmodified.
rangesToHighlightInTrimmedText.push([origStart, origEnd])
break
}
case IndexTypeInTrimmed.ELIDED: {
// The range starts in the prefix, but ends in the elided
// section. Add just the prefix + one char for the ellipsis.
rangesToHighlightInTrimmedText.push([
origStart,
origStart + trimmedText.prefixLength + 1,
])
highlightedEllipsis = true
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
// The range crosses from the prefix to the suffix.
// Highlight everything including the ellipsis.
rangesToHighlightInTrimmedText.push([origStart, origEnd - lengthLoss])
break
}
}
break
}
case IndexTypeInTrimmed.ELIDED: {
switch (endPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
// This should be impossible
throw new Error('Unexpected highlight range starts in elided and ends in prefix')
}
case IndexTypeInTrimmed.ELIDED: {
// The match starts & ends within the elided section.
if (!highlightedEllipsis) {
rangesToHighlightInTrimmedText.push([
trimmedText.prefixLength,
trimmedText.prefixLength + 1,
])
highlightedEllipsis = true
}
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
// The match starts in elided, but ends in suffix.
if (highlightedEllipsis) {
rangesToHighlightInTrimmedText.push([
trimmedText.trimmedLength - trimmedText.suffixLength,
origEnd - lengthLoss,
])
} else {
rangesToHighlightInTrimmedText.push([trimmedText.prefixLength, origEnd - lengthLoss])
highlightedEllipsis = true
}
break
}
}
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
switch (endPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
// This should be impossible
throw new Error('Unexpected highlight range starts in suffix and ends in prefix')
}
case IndexTypeInTrimmed.ELIDED: {
// This should be impossible
throw new Error('Unexpected highlight range starts in suffix and ends in elided')
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
// Match starts & ends in suffix
rangesToHighlightInTrimmedText.push([origStart - lengthLoss, origEnd - lengthLoss])
break
}
}
break
}
}
}
return rangesToHighlightInTrimmedText
}
+1 -27
View File
@@ -1,6 +1,5 @@
import {connect} from 'preact-redux'
import * as redux from 'redux'
import {ComponentConstructor, Component} from 'preact'
import {Component} from 'preact'
export interface Action<TPayload> extends redux.Action<string> {
payload: TPayload
@@ -53,31 +52,6 @@ export function setter<T>(
export type Dispatch = redux.Dispatch<Action<any>>
// We make this into a single function invocation instead of the connect(map, map)(Component)
// syntax to make better use of type inference.
//
// NOTE: To avoid this returning objects which do not compare shallow equal, it's the
// responsibility of the caller to ensure that the props returned by map compare shallow
// equal. This most importantly mean memoizing functions which wrap dispatch to avoid
// all callback props from being regenerated on every call.
export function createContainer<OwnProps, State, ComponentProps, ComponentType>(
component: {
new (props: ComponentProps): ComponentType
},
map: (state: State, dispatch: Dispatch, ownProps: OwnProps) => ComponentProps,
): ComponentConstructor<OwnProps, {}> {
const mapStateToProps = (state: State) => state
const mapDispatchToProps = (dispatch: Dispatch) => ({dispatch})
const mergeProps = (
stateProps: State,
dispatchProps: {dispatch: Dispatch},
ownProps: OwnProps,
) => {
return map(stateProps, dispatchProps.dispatch, ownProps)
}
return connect(mapStateToProps, mapDispatchToProps, mergeProps)(component)
}
export type VoidState = {
__dummyField: void
}
+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
})
})
+131 -1
View File
@@ -4,7 +4,9 @@ export function lastOf<T>(ts: T[]): T | null {
export function sortBy<T>(ts: T[], key: (t: T) => number | string): void {
function comparator(a: T, b: T) {
return key(a) < key(b) ? -1 : 1
const keyA = key(a)
const keyB = key(b)
return keyA < keyB ? -1 : keyA > keyB ? 1 : 0
}
ts.sort(comparator)
}
@@ -167,3 +169,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
}
+3 -3
View File
@@ -1,7 +1,7 @@
import {h, render} from 'preact'
import {createApplicationStore} from './store'
import {Provider} from 'preact-redux'
import {createAppStore} from './store'
import {ApplicationContainer} from './views/application-container'
import {Provider} from './lib/preact-redux'
console.log(`speedscope v${require('../package.json').version}`)
@@ -15,7 +15,7 @@ if (module.hot) {
}
const lastStore: any = (window as any)['store']
const store = createApplicationStore(lastStore ? lastStore.getState() : {})
const store = lastStore ? createAppStore(lastStore.getState()) : createAppStore()
;(window as any)['store'] = store
render(
+3 -20
View File
@@ -8,40 +8,23 @@ import {HashParams} from '../lib/hash-params'
import {actionCreatorWithIndex} from './profiles-state'
export namespace actions {
// Set the top-level profile group from which other data will be derived
export const setProfileGroup = actionCreator<ProfileGroup>('setProfileGroup')
// Set the index into the profile group to view
export const setProfileIndexToView = actionCreator<number>('setProfileIndexToView')
export const setGLCanvas = actionCreator<HTMLCanvasElement | null>('setGLCanvas')
// Set which top-level view should be displayed
export const setViewMode = actionCreator<ViewMode>('setViewMode')
// Set whether or not recursion should be flattened when viewing flamegraphs
export const setFlattenRecursion = actionCreator<boolean>('setFlattenRecursion')
// Set whether a file drag is currently active. Used to indicate that the
// application is a valid drop target.
export const setSearchQuery = actionCreator<string>('setSearchQuery')
export const setSearchIsActive = actionCreator<boolean>('setSearchIsActive')
export const setDragActive = actionCreator<boolean>('setDragActive')
// Set whether the application is currently in a loading state. Used to
// display a loading progress bar.
export const setLoading = actionCreator<boolean>('setLoading')
// Set whether the application is in an errored state.
export const setError = actionCreator<boolean>('setError')
// Set whether parameters defined by the URL encoded k=v pairs after the # in the URL
export const setHashParams = actionCreator<HashParams>('setHashParams')
export namespace sandwichView {
// Set the table sorting method used for the sandwich view.
export const setTableSortMethod = actionCreator<SortMethod>('sandwichView.setTableSortMethod')
export const setSelectedFrame = actionCreatorWithIndex<Frame | null>(
'sandwichView.setSelectedFarmr',
'sandwichView.setSelectedFrame',
)
}

Some files were not shown because too many files have changed in this diff Show More