Compare commits

...
47 Commits
Author SHA1 Message Date
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
Jamie Wong c70171836c 1.0.4 2018-09-12 18:22:59 -07:00
Jamie Wong ee0c2c5025 Fix import from Chrome < 69 when there are multiple profiles
It seems like #160 accidentally broken import of profiles in some circumstances from Chrome < 69. Before #160, we always took the first profile in the list *but* the profiles were not sorted chronologically. After #160 but before this PR, we were taking the chronologically first.

After this PR, we always take the chronologically last `CpuProfile` event in the trace.
2018-09-12 18:21:04 -07:00
Jamie Wong 3ba60a424d Update CHANGELOG.md 2018-09-10 14:40:18 -07:00
Jamie Wong c9ba143eb6 1.0.3 2018-09-10 13:54:32 -07:00
Jamie Wong 802fc2d358 Add test for Chrome 69 import 2018-09-09 18:03:49 -07:00
Jamie Wong b910a2069b Fix import for Chrome 69, support leading idle time before first call (#160)
This PR fixes #159, and also fixes various small things about how profiles were imported for previous versions of Chrome & for Firefox.

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

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

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

[copy.json.zip](https://github.com/jlfwong/speedscope/files/2350583/copy.json.zip)
2018-09-04 17:03:55 -07:00
Jamie Wong a09f27d816 Update CHANGELOG.md 2018-09-04 16:14:41 -07:00
Alex Dukhno 944a6cb126 Change time formatting for minutes from 1.50min to 1:30 (#153) 2018-09-04 13:30:02 -07:00
Jamie Wong 828beb7ccf Update README-ADMINS.md 2018-08-23 10:04:42 -07:00
Jamie Wong 82867ab234 1.0.1 2018-08-23 10:01:13 -07:00
Jamie Wong 6116371ffb Fix flamechart bleeding (#151)
Fixes #150
2018-08-23 09:59:16 -07:00
Jamie Wong da2de64d97 1.0.0 2018-08-23 09:38:56 -07:00
Jamie Wong 9c6f88a9f2 Update README.md 2018-08-23 09:22:24 -07:00
Jamie Wong 48ae79a6a7 Revert "Update hero GIF in README"
This reverts commit 11585fed92.
2018-08-23 08:44:12 -07:00
Jamie Wong 11585fed92 Update hero GIF in README 2018-08-23 08:42:36 -07:00
Jamie Wong f7279e088e Update README.md 2018-08-23 08:15:59 -07:00
Jamie Wong 3fc631cf79 Fix a regression in resize behavior from #147 (#149)
The problem was that I was using `canvas.getBoundingClientRect()` to get the size to resize to, but that was changing as the result of CSS properties set on the canvas! Instead, we take the measurements of its container now which is set to fill the screen, and the canvas has its size entirely managed by `graphics.ts`.
2018-08-23 08:09:22 -07:00
Jamie Wong 5ab320b4cf Update README.md 2018-08-23 08:07:03 -07:00
Jamie Wong f60ab630be Fix rendering bugs when device pixel ratio changes (#147)
Fixes #102
2018-08-22 20:36:09 -07:00
Jamie Wong 453f793042 Update instructions to push tags with release 2018-08-22 18:50:25 -07:00
Jamie Wong a5c3184880 Add a way of generating a self-contained zip-file 2018-08-22 18:50:25 -07:00
Jamie Wong 3193b34c46 Update README.md 2018-08-21 10:20:08 -07:00
Jamie Wong 2df69b6713 Update README.md 2018-08-21 09:55:27 -07:00
Jamie Wong 777e605c7b Update README.md to link to wiki pages for import instructions
Fixes #115
2018-08-21 09:55:06 -07:00
Jamie Wong 100578c536 Add contributor guidelines & documentation (#144)
Fixes #4
2018-08-20 09:42:44 -07:00
Jamie Wong 03578ea7a4 0.7.1 2018-08-20 08:46:13 -07:00
Jamie Wong 3210f8fc08 Fix text culling of ellipses along the left side of the viewport (#143)
Before:
![image](https://user-images.githubusercontent.com/150329/44312631-2c716280-a3b0-11e8-8834-2574b3a4a627.png)

After:
![image](https://user-images.githubusercontent.com/150329/44312639-48750400-a3b0-11e8-8017-41640fe56a22.png)

Fixes #134
2018-08-19 13:12:28 -07:00
Jamie Wong 2c89cefcdc Replace regl with GPU apis ported from evanw/sky (#140)
Fixes #101 
Fixes #137
2018-08-19 11:37:47 -07:00
74 changed files with 19006 additions and 4399 deletions
+69 -3
View File
@@ -1,12 +1,78 @@
## [Unreleased]
## Unreleased
## [0.7.0]
## [1.3.1] - 2018-11-08
### Fixed
* Fixed a file import performance regression by using TextDecoder [#188] (by @jlfwong)
## [1.3.0] - 2018-10-29
### Added
* Support import from Haskell GHC JSON format support [#183] (by @trishume)
### Fixed
* Make the wasd keymappings work on azerty keyboards [#184] (by @vrischmann)
* Fix import of binary formats via profileURL [#179] (by @f-hj)
## [1.2.0] - 2018-10-08
### Added
* Add import of v8 heap allocation profile [#170] (by @vmarchaud)
## [1.1.0] - 2018-09-26
### Added
* Add go tool pprof import support [#165]
## [1.0.4] - 2018-09-12
### Fixed
* Fix import from Chrome < 69 when there are multiple profiles [#161]
## [1.0.3] - 2018-09-10
### Fixed
* Fix import for Chrome 69, support leading idle time before first call [#160]
## [1.0.2] - 2018-09-04
### Fixed
* Allow optional CR before LF when probing collapsed stacks files [#154]
* Fix import for Firefox 63 [#156]
* Change time formatting for minutes from 1.50min to 1:30 [#153] (by @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]
## [1.0.0] - 2018-08-23
### Fixed
* Fixed rendering issues when switching between screens w/ different `devicePixelRatios` [#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]
## [0.7.0] - 2018-08-16
### Added
* Added support to import from linux `perf script` [#135]
## [0.6.0]
## [0.6.0] - 2018-08-14
### Added
+73
View File
@@ -0,0 +1,73 @@
# Contributor Covenant Code of Conduct:
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project
and our community a harassment-free experience for everyone, regardless of
age, body size, disability, ethnicity, gender identity and expression, level
of experience, nationality, personal appearance, race, religion, or sexual
identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of
acceptable behavior and are expected to take appropriate and fair corrective
action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an
appointed representative at an online or offline event. Representation of a
project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting
[jamie.lf.wong@gmail.com](mailto:jamie.lf.wong@gmail.com). All complaints
will be reviewed and investigated and will result in a response that is
deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an
incident. Further details of specific enforcement policies may be posted
separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor-Covenant][homepage],
version 1.4, available at
[https://contributor-covenant.org/version/1/4][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/4/
+70
View File
@@ -0,0 +1,70 @@
# Contributing to speedscope
Hi! This is a short guide & set of guidelines for contributing to speedscope.
Contributors of all skill levels are welcome to submit pull requests.
This project adheres to the Contributor Covenant [code of conduct](./CODE_OF_CONDUCT.md).
All contributors are expected to uphold this code of conduct.
## Setting up for development
To start running speedscope locally, run the following:
git clone https://github.com/jlfwong/speedscope.git
cd speedscope
npm install
npm run serve
This should open up a running version of speedscope in your default browser.
In your terminal, you should see something like this:
$ npm run serve
> speedscope@0.7.1 serve /Users/jlfwong/code/speedscope
> parcel assets/index.html --open --no-autoinstall
Server running at http://localhost:1234
✨ Built in 7.30s.
Most of speedscope is written in TypeScript. If you're unfamiliar with
TypeScript, then you can either just try to learn it as you go, then the
[official TypeScript
documentation](https://www.typescriptlang.org/docs/home.html) may be of use
to you!
If you're not sure where the code you want to modify lives, the [`README.md`
in the `src/` directory](./src/README.md) might be helpful.
## Code formatting
All TypeScript code in speedscope is automatically formatted with
[Prettier](https://prettier.io/). This means that while you're writing your code,
you don't have to worry about following a formatting guide, because a program will
format your code for you!
The easiest way to use Prettier is via an editor integration. See the [Editor
Integration](https://prettier.io/docs/en/editors.html) page from Prettier's
documentation for help with that.
If you don't want to do that, you can alternatively run the autoformatter by
running `npm run prettier`.
## Running tests
All TypeScript tests are written use [Jest](https://jestjs.io/). To run the
tests, run `npm run jest`.
## Contributing new features
Before contributing code to implement a new feature, please open an issue to
discuss it first. Large pull requests that are submitted without first getting
maintainer buy-in are unlikely to be reviewed or merged.
For features that will cause a visual change, please include visual mockups of
the change you're planning on making.
## Contributing bug fixes
If you discover a bug, please file an issue. If the code change required to
fix it is small (< ~20 lines), then feel free to just open a PR to fix the
issue without trying to get buy-in ahead of time.
+20 -5
View File
@@ -8,6 +8,7 @@ Publishing speedscope is a multi-step process:
2. Prepare the release
3. Publish to npm
4. Deploy the website
5. Upload a release to GitHub
At time of writing, deployment assumes you're running macOS. It probably
works if you're on a linux, and almost definitely does not work on Windows.
@@ -43,7 +44,7 @@ If everything looks good, proceed to "Prepare the release".
2. Update CHANGELOG.md to indicate the changes that were made as part of this release
3. Commit the changes with the version name as the commit message, e.g. `git commit -m 0.6.0`
4. `git tag` the release. We use tags like `v0.6.0`, e.g. `git tag v0.6.0`
5. `git push`
5. `git push && git push --tags`
## Publish to npm
@@ -54,17 +55,20 @@ a matter of running `npm publish`.
To verify that the publish was successful, run `npm install -g speedscope`.
Try `speedscope`, which should open speedscope in browser.
Try `speedscope sample/profiles/stackcollapse/simple.txt`, which should immediately load the profile
Try `speedscope sample/profiles/stackcollapse/simple.txt`, which should immediately load the profile.
## Deploying the website
This step must follow the "Publish to npm" step, since it uses assets from
the npm publish.
https://www.speedscope.app/ is hosted on GitHub pages, and is published via pushing
to the `gh-pages` branch. The `gh-pages` branch has totally different contents than
other branches of this repository: https://github.com/jlfwong/speedscope/tree/gh-pages.
It's populated by a deploy script which is invoked by running `npm run deploy` script.
This will do a build of the static resources, and boot a local server for you to test
the compiled assets. Please do not skip the manual testing in this step.
It's populated by a deploy script which is invoked by running `npm run deploy` script. This populate a directory with assets pulled from npm, and
boot a local server for you to test the compiled assets. Please do not skip
the manual testing in this step.
If everything looks good, you should be able to hit Ctrl+C, and you should see this prompt:
@@ -76,3 +80,14 @@ If everything looks good, type `yes` then enter. This will commit to the `gh-pag
To check if a deploy has happened, you can check https://www.speedscope.app/release.txt
which includes the version, the date, and the commit of the deploy.
## Upload a release to GitHub
This step must follow the "Publish to npm" step, since it uses assets from
the npm publish.
To make a zipfile suitable for uploading to GitHub as a release, run `scripts/prepare-zip-file.sh`.
Once that's done, you should have a zip file in `dist/release/`
Upload that file along with changelog notes to https://github.com/jlfwong/speedscope/releases/new
+37 -58
View File
@@ -1,6 +1,6 @@
# 🔬speedscope
A fast, interactive web-based viewer for [sampling profiles][0]. An alternative viewer for [FlameGraphs][1]. Will happily display multi-megabyte profiles without crashing your browser.
A fast, interactive web-based viewer for performance profiles. An alternative viewer for [FlameGraphs][1]. Will happily display multi-megabyte profiles without crashing your browser.
Given raw profiling data, speedscope allows you to interactively explore the data to get insight into what's slow in your application, or allocating all the memory, or whatever data is represented in the profiling data.
@@ -13,6 +13,8 @@ Given raw profiling data, speedscope allows you to interactively explore the dat
Visit https://www.speedscope.app, then either browse to find a profile file or drag-and-drop one onto the page. The profiles are not uploaded anywhere -- the application is totally in-browser.
## Command line usage
For offline use, or convenience in the terminal, you can also install speedscope
via npm:
@@ -20,59 +22,33 @@ via npm:
Invoking `speedscope /path/to/profile` will load speedscope in your default browser.
## Self-contained directory
If you don't have npm or node installed, you can also download a
self-contained version from https://github.com/jlfwong/speedscope/releases.
After you download the zip file from a release, simply unzip it and open the
contained `index.html` in Chrome or Firefox.
## Supported file formats
### Chrome
speedscope is designed to ingest profiles from a variety of different profilers for different programming languages & environments. Click the links below for documentation on how to import from a specific source.
Both the timeline format output by Chrome developers tools (https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference#save) and the `.cpuprofile` format output are supported. The `.cpuprofile` format is useful for viewing flamecharts generated by Node.js applications:
https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27
- 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 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))
- Go
- [Importing from pprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-pprof-(go))
- Native code
- [Importing from Instruments.app](https://github.com/jlfwong/speedscope/wiki/Importing-from-Instruments.app) (macOS)
- [Importing from `perf`](https://github.com/jlfwong/speedscope/wiki/Importing-from-perf-(linux)) (linux)
- [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)
### Firefox
The `profile.json` file format output by Firefox can be saved and import into speedscope: https://developer.mozilla.org/en-US/docs/Tools/Performance
### Node
If you record profiling information like so:
node --prof /path/to/my/script.js
Then this will generate one or more `isolate*.log` files. You can open
the resulting profile in speedscope by running the following command:
node --prof-process --preprocess -j isolate*.log | speedscope -
### Instruments.app
You can import call trees from OSX Instruments.app into speedscope by
selecting a row in the "Profile" view and select "Edit -> Deep Copy" from the
menu then pasting directly into speedscope. This data contains only aggregate
statistics, so the "Time Order" view and the "Left Heavy" view will look very
similar.
You can also import `.trace` files for time profiles by dragging them directly
into the browser from Chrome.
### `stackprof` Ruby profiler
If the `raw: true` flag is set when recording a dump, the resulting json dump can be imported into speedscope.
### Linux `perf`
You can import profiles recorded using `perf record` and formatted using `perf script`.
perf record -a -F 999 -g -p PID > perf.data
perf script -i perf.data > profile.linux-perf.txt
Then drop the resulting `perf.txt` into speedscope, or if you have speedscope installed
locally, you can run:
perf record -a -F 999 -g -p PID > perf.data
perf script -i perf.data | speedscope -
### `DTrace`
If you process the output of `DTrace` first with Brendan Gregg's `stackcollapse-*.pl` scripts (https://github.com/brendangregg/FlameGraph#2-fold-stacks), the result can be imported into speedscope.
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).
## Importing via URL
@@ -81,26 +57,25 @@ To load a specific profile by URL, you can append a hash fragment like `#profile
## Views
### 🕰Time Order
In the "Time Order" view (the default), the stacks are ordered left-to-right in the same order as the occurred in the input file, which is usually going to be the chronological order they were recorded in. This view is most helpful for understand the behavior of an application over time, e.g. "first the data is fetched from the database, then the data is prepared for serialization, then the data is serialized to JSON". This is the only flame graph order supported by Chrome developer tools.
In all flamegraph views, the horizontal axis represents the "weight" of each stack (most commonly CPU time), and the vertical axis shows you the stack active at the time of the sample.
If you click on one of the frames, you'll be able to see summary statistics about it.
![Detail View](https://user-images.githubusercontent.com/150329/42108613-e6ef6d3a-7b8f-11e8-93d4-541b2cb93fe5.png)
In the "Time Order" view (the default), call stacks are ordered left-to-right in the same order as they occurred in the input file, which is usually going to be the chronological order they were recorded in. This view is most helpful for understanding the behavior of an application over time, e.g. "first the data is fetched from the database, then the data is prepared for serialization, then the data is serialized to JSON".
The horizontal axis represents the "weight" of each stack (most commonly CPU time), and the vertical axis shows you the stack active at the time of the sample. If you click on one of the frames, you'll be able to see summary statistics about it.
### ⬅️Left Heavy
![Left Heavy View](https://user-images.githubusercontent.com/150329/44534434-a05f8380-a6ac-11e8-86ac-e3e05e577c52.png)
In the "Left Heavy" view, identical stacks are grouped together, regardless of whether they were recorded sequentially. Then, the stacks are sorted so that the heaviest stack for each parent is on the left -- hence "left heavy". This view is useful for understanding where all the time is going in situations where there are hundreds or thousands of function calls interleaved between other call stacks.
### 🥪 Sandwich
![Sandwich View](https://user-images.githubusercontent.com/150329/42108467-76a57baa-7b8f-11e8-815f-1df7b6ac3ede.png)
The Sandwich view is a table view in which you can find a list of all functions an their associated times. You can sort by self time or total time.
It's called "Sandwich" view because if you select one of the rows in the table, you can see flamegraphs for all the callers and callees of the selected
row.
![Sandwich View](https://user-images.githubusercontent.com/150329/42108467-76a57baa-7b8f-11e8-815f-1df7b6ac3ede.png)
## Navigation
@@ -133,3 +108,7 @@ 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
## Contributing
Do you want to contribute to speedscope? Sweeeeet. Check out [CONTRIBUTING.md](./CONTRIBUTING.md) for instructions on setting up your dev environment.
+2622 -2534
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "0.7.0",
"version": "1.3.1",
"description": "",
"repository": "jlfwong/speedscope",
"main": "index.js",
@@ -38,9 +38,9 @@
"preact": "8.2.7",
"preact-redux": "jlfwong/preact-redux#a56dcc4",
"prettier": "1.12.0",
"protobufjs": "6.8.8",
"quicktype": "15.0.45",
"redux": "^4.0.0",
"regl": "1.3.1",
"ts-jest": "22.4.6",
"typescript": "2.8.1",
"typescript-eslint-parser": "17.0.1",
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +1,81 @@
{"version":"0.0.1","$schema":"https://www.speedscope.app/file-format-schema.json","shared":{"frames":[{"name":"a"},{"name":"b"},{"name":"c"},{"name":"d"}]},"profiles":[{"type":"evented","name":"simple.txt","unit":"none","startValue":0,"endValue":14,"events":[{"type":"O","frame":0,"at":0},{"type":"O","frame":1,"at":0},{"type":"O","frame":2,"at":0},{"type":"C","frame":2,"at":2},{"type":"O","frame":3,"at":2},{"type":"C","frame":3,"at":6},{"type":"O","frame":2,"at":6},{"type":"C","frame":2,"at":9},{"type":"C","frame":1,"at":14},{"type":"C","frame":0,"at":14}]}]}
{
"$schema": "https://www.speedscope.app/file-format-schema.json",
"profiles": [
{
"endValue": 14,
"events": [
{
"at": 0,
"frame": 0,
"type": "O"
},
{
"at": 0,
"frame": 1,
"type": "O"
},
{
"at": 0,
"frame": 2,
"type": "O"
},
{
"at": 2,
"frame": 2,
"type": "C"
},
{
"at": 2,
"frame": 3,
"type": "O"
},
{
"at": 6,
"frame": 3,
"type": "C"
},
{
"at": 6,
"frame": 2,
"type": "O"
},
{
"at": 9,
"frame": 2,
"type": "C"
},
{
"at": 14,
"frame": 1,
"type": "C"
},
{
"at": 14,
"frame": 0,
"type": "C"
}
],
"name": "simple.txt",
"startValue": 0,
"type": "evented",
"unit": "none"
}
],
"shared": {
"frames": [
{
"name": "a"
},
{
"name": "b"
},
{
"name": "c"
},
{
"name": "d"
}
]
},
"version": "0.0.1"
}
@@ -0,0 +1,5 @@
a;b;c 1
a;b;c 1
a;b;d 4
a;b;c 3
a;b 5
+2
View File
@@ -0,0 +1,2 @@
simple
server
+15
View File
@@ -0,0 +1,15 @@
.PHONY: all clean linux-perf
all: simple server
clean:
rm -f simple server
simple: simple.go
go build $<
server: server.go
go build $<
simple.prof: simple
./$< -cpuprofile=$@
+71
View File
@@ -0,0 +1,71 @@
package main
import (
"log"
"fmt"
"sync"
"time"
"net/http"
)
import _ "net/http/pprof"
// See https://golang.org/pkg/net/http/pprof/ for details
func alpha() {
z := 3
for i := 0; i < 100000; i++ {
z *= 3
}
}
func beta() {
z := 3
for i := 0; i < 100000; i++ {
z *= 3
}
}
func delta() {
z := 3
for i := 0; i < 100000; i++ {
z *= 3
}
alpha()
beta()
}
func gamma() {
z := 3
for i := 0; i < 100000; i++ {
z *= 3
}
}
func main() {
// we need a webserver to get the pprof webserver
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
fmt.Println("hello world")
var wg sync.WaitGroup
wg.Add(1)
go leakyFunction(wg)
wg.Wait()
}
func leakyFunction(wg sync.WaitGroup) {
defer wg.Done()
s := make([]string, 3)
for i:= 0; i < 10000000; i++{
alpha()
beta()
delta()
gamma()
s = append(s, "magical pandas")
if (i % 100000) == 0 {
time.Sleep(50 * time.Millisecond)
}
}
}
+53
View File
@@ -0,0 +1,53 @@
package main
import "flag"
import "runtime/pprof"
import "os"
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
func alpha() {
z := 3
for i := 0; i < 100000; i++ {
z *= 3
}
}
func beta() {
z := 3
for i := 0; i < 100000; i++ {
z *= 3
}
}
func delta() {
z := 3
for i := 0; i < 100000; i++ {
z *= 3
}
alpha()
beta()
}
func gamma() {
z := 3
for i := 0; i < 100000; i++ {
z *= 3
}
}
func main() {
flag.Parse()
if *cpuprofile != "" {
f, _ := os.Create(*cpuprofile)
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
for i := 0; i < 10000; i++ {
alpha()
beta()
delta()
gamma()
}
}
-4
View File
@@ -25,8 +25,4 @@ function gamma() {
return prod
}
console.profile('simple')
alpha()
setTimeout(() => {
console.profileEnd('simple')
}, 0)
+3
View File
@@ -7,6 +7,9 @@ OUTDIR=`pwd`/dist/release
# Typecheck
node_modules/.bin/tsc --noEmit
# Run unit tests
npm run jest
# Clean out the release directory
rm -rf "$OUTDIR"
mkdir -p "$OUTDIR"
+12 -13
View File
@@ -1,26 +1,28 @@
#!/bin/bash
#
# type check, do a release build, then do a shallow clone of the
# repository into a temporary directory and copy the release build
# artifacts into there to commit & push to the gh-pages branch
# Do a shallow clone of the repository into a temporary directory and copy the
# artifacts pulled from npm into the shallow clone to commit & push to the
# gh-pages branch.
set -euxo pipefail
OUTDIR=`pwd`/dist/release
echo $OUTDIR
SRCDIR=`pwd`
OUTDIR=`mktemp -d -t speedscope-unpacked`
./scripts/build-release.sh
# Untar the package
pushd "$OUTDIR"
PACKEDNAME=`npm pack speedscope | tail -n1`
tar -xvvf "$PACKEDNAME"
# Create a shallow clone of the repository
TMPDIR=`mktemp -d -t speedscope-release`
echo "Entering $TMPDIR"
TMPDIR=`mktemp -d -t speedscope-deploy`
pushd "$TMPDIR"
git clone --depth 1 git@github.com:jlfwong/speedscope.git -b gh-pages
# Copy the build artifacts into the shallow clone
pushd speedscope
rm -rf *
cp -R "$OUTDIR"/* .
cp -R "$OUTDIR"/package/dist/release/** .
# Set the CNAME record
echo www.speedscope.app > CNAME
@@ -35,17 +37,14 @@ function ctrl_c() {
if [[ $REPLY =~ ^yes$ ]]
then
git add --all
git commit -m 'Release'
git commit -m "Deploy $PACKEDNAME"
git push origin HEAD:gh-pages
popd
rm -rf "$TMPDIR"
exit 0
else
set +x
echo "Aborting release."
set -x
popd
rm -rf "$TMPDIR"
exit 1
fi
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# Create a zip file containing a standalone copy of speedscope
# based on the contents of the package published to npm
set -euxo pipefail
SRCDIR=`pwd`
TMPDIR=`mktemp -d -t speedscope-test-installation`
# Untar the package
pushd "$TMPDIR"
PACKEDNAME=`npm pack speedscope | tail -n1`
tar -xvvf "$PACKEDNAME"
# Zip the parts we care about
ZIPNAME=`basename $PACKEDNAME .tgz`.zip
mkdir speedscope
mv package/dist/release/** speedscope
cp "$SRCDIR"/LICENSE speedscope
echo "This is a self-contained release of https://github.com/jlfwong/speedscope." > speedscope/README
echo "To use it, open index.html in Chrome or Firefox." >> speedscope/README
zip "$ZIPNAME" speedscope/**
# Switch back to the repository root
popd
mv "$TMPDIR"/"$ZIPNAME" dist/release/"$ZIPNAME"
# Clean up
rm -rf "$TMPDIR"
set +x
echo "Created dist/release/$ZIPNAME"
+13
View File
@@ -0,0 +1,13 @@
# Speedscope TypeScript source
This directory contains the bulk of speedscope's source code.
## Subdirectories
* `gl/`: WebGL code. This includes e.g. the code to render flamecharts.
* `import/`: Code to import profiles from varous profilers into speedscope. This include e.g. the code to import Chrome performance profiles.
* `lib/`: Mostly dependency-less utilities. This includes e.g. an LRU cache implementation, basic linear algebra classes,
and the definition of speedscope's file format.
* `store/`: Speedscope's application state management. Implemented using [`redux`](https://redux.js.org/).
* `typings/`: [TypeScript definition files](https://basarat.gitbooks.io/typescript/docs/types/ambient/d.ts.html)
* `views/`: View code to generate the HTML & CSS used to construct the UI. Implemented using [`preact`](https://preactjs.com/) and [`aphrodite`](https://github.com/Khan/aphrodite). Also contains code mapping from the `redux` store to views using [`preact-redux`](https://github.com/developit/preact-redux)
+43 -156
View File
@@ -1,105 +1,40 @@
import regl from 'regl'
import {
RectangleBatchRenderer,
RectangleBatch,
RectangleBatchRendererProps,
} from './rectangle-batch-renderer'
import {
ViewportRectangleRenderer,
ViewportRectangleRendererProps,
} from './overlay-rectangle-renderer'
import {
TextureCachedRenderer,
TextureRenderer,
TextureRendererProps,
} from './texture-cached-renderer'
import {StatsPanel} from '../lib/stats'
import {Vec2, Rect} from '../lib/math'
import {
FlamechartColorPassRenderer,
FlamechartColorPassRenderProps,
} from './flamechart-color-pass-renderer'
import {Graphics, WebGL} from './graphics'
import {RectangleBatchRenderer} from './rectangle-batch-renderer'
import {TextureRenderer} from './texture-renderer'
import {Rect, Vec2} from '../lib/math'
import {ViewportRectangleRenderer} from './overlay-rectangle-renderer'
import {FlamechartColorPassRenderer} from './flamechart-color-pass-renderer'
type FrameCallback = () => void
interface SetViewportScopeProps {
physicalBounds: Rect
}
export class CanvasContext {
public gl: regl.Instance
private rectangleBatchRenderer: RectangleBatchRenderer
private viewportRectangleRenderer: ViewportRectangleRenderer
private textureRenderer: TextureRenderer
private flamechartColorPassRenderer: FlamechartColorPassRenderer
private setViewportScope: regl.Command<{physicalBounds: Rect}>
private setScissor: regl.Command<{}>
public readonly gl: WebGL.Context
public readonly rectangleBatchRenderer: RectangleBatchRenderer
public readonly textureRenderer: TextureRenderer
public readonly viewportRectangleRenderer: ViewportRectangleRenderer
public readonly flamechartColorPassRenderer: FlamechartColorPassRenderer
constructor(canvas: HTMLCanvasElement) {
this.gl = regl({
canvas: canvas,
attributes: {
antialias: false,
},
extensions: ['ANGLE_instanced_arrays', 'WEBGL_depth_texture'],
optionalExtensions: ['EXT_disjoint_timer_query'],
profile: true,
})
console.log(
`WebGL initialized. renderer: ${this.gl.limits.renderer}, vendor: ${
this.gl.limits.vendor
}, version: ${this.gl.limits.version}`,
)
;(window as any)['CanvasContext'] = this
this.gl = new WebGL.Context(canvas)
this.rectangleBatchRenderer = new RectangleBatchRenderer(this.gl)
this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl)
this.textureRenderer = new TextureRenderer(this.gl)
this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl)
this.flamechartColorPassRenderer = new FlamechartColorPassRenderer(this.gl)
this.setScissor = this.gl({scissor: {enable: true}})
this.setViewportScope = this.gl<SetViewportScopeProps>({
context: {
viewportX: (context: regl.Context, props: SetViewportScopeProps) => {
return props.physicalBounds.left()
},
viewportY: (context: regl.Context, props: SetViewportScopeProps) => {
return props.physicalBounds.top()
},
},
viewport: (context, props) => {
const {physicalBounds} = props
return {
x: physicalBounds.left(),
y:
window.devicePixelRatio * window.innerHeight -
physicalBounds.top() -
physicalBounds.height(),
width: physicalBounds.width(),
height: physicalBounds.height(),
}
},
scissor: (context, props) => {
const {physicalBounds} = props
return {
enable: true,
box: {
x: physicalBounds.left(),
y:
window.devicePixelRatio * window.innerHeight -
physicalBounds.top() -
physicalBounds.height(),
width: physicalBounds.width(),
height: physicalBounds.height(),
},
}
},
})
const webGLInfo = this.gl.getWebGLInfo()
if (webGLInfo) {
console.log(
`WebGL initialized. renderer: ${webGLInfo.renderer}, vendor: ${
webGLInfo.vendor
}, version: ${webGLInfo.version}`,
)
}
;(window as any)['testContextLoss'] = () => {
this.gl.testContextLoss()
}
}
private tick: regl.Tick | null = null
private tickNeeded: boolean = false
private animationFrameRequest: number | null = null
private beforeFrameHandlers = new Set<FrameCallback>()
addBeforeFrameHandler(callback: FrameCallback) {
this.beforeFrameHandlers.add(callback)
@@ -108,86 +43,38 @@ export class CanvasContext {
this.beforeFrameHandlers.delete(callback)
}
requestFrame() {
this.tickNeeded = true
if (!this.tick) {
this.tick = this.gl.frame(this.onBeforeFrame)
if (!this.animationFrameRequest) {
this.animationFrameRequest = requestAnimationFrame(this.onBeforeFrame)
}
}
private onBeforeFrame = () => {
this.animationFrameRequest = null
this.gl.setViewport(0, 0, this.gl.renderTargetWidthInPixels, this.gl.renderTargetHeightInPixels)
this.gl.clear(new Graphics.Color(1, 1, 1, 1))
private perfDebug = window.location.href.indexOf('perf-debug=1') !== -1
private statsPanel: StatsPanel | null = this.perfDebug ? new StatsPanel() : null
private onBeforeFrame = (context: regl.Context) => {
this.setScissor(() => {
this.gl.clear({color: [0, 0, 0, 0]})
})
this.tickNeeded = false
this.statsPanel && this.statsPanel.begin()
for (const handler of this.beforeFrameHandlers) {
handler()
}
this.statsPanel && this.statsPanel.end()
if (this.tick && !this.tickNeeded) {
if (!this.perfDebug) {
this.tick.cancel()
this.tick = null
}
}
// TODO(jlfwong): It would be really nice to have GPU
// stats here, but I can't figure out how to interpret
// the gpuTime. I suspect this is caused by executing the same
// program multiple times without flushing.
// I'll investigate this at some point and report a bug to regl.
}
drawRectangleBatch(props: RectangleBatchRendererProps) {
this.rectangleBatchRenderer.render(props)
setViewport(physicalBounds: Rect, cb: () => void): void {
const {origin, size} = physicalBounds
let viewportBefore = this.gl.viewport
this.gl.setViewport(origin.x, origin.y, size.x, size.y)
cb()
let {x, y, width, height} = viewportBefore
this.gl.setViewport(x, y, width, height)
}
drawTexture(props: TextureRendererProps) {
this.textureRenderer.render(props)
}
drawFlamechartColorPass(props: FlamechartColorPassRenderProps) {
this.flamechartColorPassRenderer.render(props)
}
createRectangleBatch(): RectangleBatch {
return new RectangleBatch(this.gl)
}
createTextureCachedRenderer<T>(options: {
render(t: T): void
shouldUpdate(oldProps: T, newProps: T): boolean
}): TextureCachedRenderer<T> {
return new TextureCachedRenderer(this.gl, {
...options,
textureRenderer: this.textureRenderer,
})
}
drawViewportRectangle(props: ViewportRectangleRendererProps) {
this.viewportRectangleRenderer.render(props)
}
renderInto(el: Element, cb: (context: regl.Context) => void) {
renderBehind(el: Element, cb: () => void) {
const bounds = el.getBoundingClientRect()
const physicalBounds = new Rect(
new Vec2(bounds.left * window.devicePixelRatio, bounds.top * window.devicePixelRatio),
new Vec2(bounds.width * window.devicePixelRatio, bounds.height * window.devicePixelRatio),
)
this.setViewportScope({physicalBounds}, cb)
}
setViewport(physicalBounds: Rect, cb: (context: regl.Context) => void) {
this.setViewportScope({physicalBounds}, cb)
}
getMaxTextureSize() {
return this.gl.limits.maxTextureSize
this.setViewport(physicalBounds, cb)
}
}
+150 -150
View File
@@ -1,165 +1,165 @@
import regl from 'regl'
import {Vec2, Rect, AffineTransform} from '../lib/math'
import {Graphics} from './graphics'
import {setUniformAffineTransform} from './utils'
const vertexFormat = new Graphics.VertexFormat()
vertexFormat.add('position', Graphics.AttributeType.FLOAT, 2)
vertexFormat.add('uv', Graphics.AttributeType.FLOAT, 2)
const vert = `
uniform mat3 uvTransform;
uniform mat3 positionTransform;
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = (uvTransform * vec3(uv, 1)).xy;
gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1);
}
`
const frag = `
precision mediump float;
uniform vec2 uvSpacePixelSize;
uniform float renderOutlines;
varying vec2 vUv;
uniform sampler2D colorTexture;
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue
vec3 hcl2rgb(float H, float C, float L) {
float hPrime = H / 60.0;
float X = C * (1.0 - abs(mod(hPrime, 2.0) - 1.0));
vec3 RGB =
hPrime < 1.0 ? vec3(C, X, 0) :
hPrime < 2.0 ? vec3(X, C, 0) :
hPrime < 3.0 ? vec3(0, C, X) :
hPrime < 4.0 ? vec3(0, X, C) :
hPrime < 5.0 ? vec3(X, 0, C) :
vec3(C, 0, X);
float m = L - dot(RGB, vec3(0.30, 0.59, 0.11));
return RGB + vec3(m, m, m);
}
float triangle(float x) {
return 2.0 * abs(fract(x) - 0.5) - 1.0;
}
vec3 colorForBucket(float t) {
float x = triangle(30.0 * t);
float H = 360.0 * (0.9 * t);
float C = 0.25 + 0.2 * x;
float L = 0.80 - 0.15 * x;
return hcl2rgb(H, C, L);
}
void main() {
vec4 here = texture2D(colorTexture, vUv);
if (here.z == 0.0) {
// Background color
gl_FragColor = vec4(0, 0, 0, 0);
return;
}
// Sample the 4 surrounding pixels in the depth texture to determine
// if we should draw a boundary here or not.
vec4 N = texture2D(colorTexture, vUv + vec2(0, uvSpacePixelSize.y));
vec4 E = texture2D(colorTexture, vUv + vec2(uvSpacePixelSize.x, 0));
vec4 S = texture2D(colorTexture, vUv + vec2(0, -uvSpacePixelSize.y));
vec4 W = texture2D(colorTexture, vUv + vec2(-uvSpacePixelSize.x, 0));
// NOTE: For outline checks, we intentionally check both the right
// and the left to determine if we're an edge. If a rectangle is a single
// pixel wide, we don't want to render it as an outline, so this method
// of checking ensures that we don't outline single physical-space
// pixel width rectangles.
if (
renderOutlines > 0.0 &&
(
here.y == N.y && here.y != S.y || // Top edge
here.y == S.y && here.y != N.y || // Bottom edge
here.x == E.x && here.x != W.x || // Left edge
here.x == W.x && here.x != E.x
)
) {
// We're on an edge! Draw transparent.
gl_FragColor = vec4(0, 0, 0, 0);
} else {
// Not on an edge. Draw the appropriate color.
gl_FragColor = vec4(colorForBucket(here.z), here.a);
}
}
`
export interface FlamechartColorPassRenderProps {
rectInfoTexture: regl.Texture
rectInfoTexture: Graphics.Texture
renderOutlines: boolean
srcRect: Rect
dstRect: Rect
}
export class FlamechartColorPassRenderer {
private command: regl.Command<FlamechartColorPassRenderProps>
constructor(gl: regl.Instance) {
this.command = gl({
vert: `
uniform mat3 uvTransform;
uniform mat3 positionTransform;
private material: Graphics.Material
private buffer: Graphics.VertexBuffer
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
constructor(private gl: Graphics.Context) {
const vertices = [
{pos: [-1, 1], uv: [0, 1]},
{pos: [1, 1], uv: [1, 1]},
{pos: [-1, -1], uv: [0, 0]},
{pos: [1, -1], uv: [1, 0]},
]
const floats: number[] = []
for (let v of vertices) {
floats.push(v.pos[0])
floats.push(v.pos[1])
floats.push(v.uv[0])
floats.push(v.uv[1])
}
void main() {
vUv = (uvTransform * vec3(uv, 1)).xy;
gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1);
}
`,
frag: `
precision mediump float;
uniform vec2 uvSpacePixelSize;
uniform float renderOutlines;
varying vec2 vUv;
uniform sampler2D colorTexture;
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue
vec3 hcl2rgb(float H, float C, float L) {
float hPrime = H / 60.0;
float X = C * (1.0 - abs(mod(hPrime, 2.0) - 1.0));
vec3 RGB =
hPrime < 1.0 ? vec3(C, X, 0) :
hPrime < 2.0 ? vec3(X, C, 0) :
hPrime < 3.0 ? vec3(0, C, X) :
hPrime < 4.0 ? vec3(0, X, C) :
hPrime < 5.0 ? vec3(X, 0, C) :
vec3(C, 0, X);
float m = L - dot(RGB, vec3(0.30, 0.59, 0.11));
return RGB + vec3(m, m, m);
}
float triangle(float x) {
return 2.0 * abs(fract(x) - 0.5) - 1.0;
}
vec3 colorForBucket(float t) {
float x = triangle(30.0 * t);
float H = 360.0 * (0.9 * t);
float C = 0.25 + 0.2 * x;
float L = 0.80 - 0.15 * x;
return hcl2rgb(H, C, L);
}
void main() {
vec4 here = texture2D(colorTexture, vUv);
if (here.z == 0.0) {
// Background color
gl_FragColor = vec4(0, 0, 0, 0);
return;
}
// Sample the 4 surrounding pixels in the depth texture to determine
// if we should draw a boundary here or not.
vec4 N = texture2D(colorTexture, vUv + vec2(0, uvSpacePixelSize.y));
vec4 E = texture2D(colorTexture, vUv + vec2(uvSpacePixelSize.x, 0));
vec4 S = texture2D(colorTexture, vUv + vec2(0, -uvSpacePixelSize.y));
vec4 W = texture2D(colorTexture, vUv + vec2(-uvSpacePixelSize.x, 0));
// NOTE: For outline checks, we intentionally check both the right
// and the left to determine if we're an edge. If a rectangle is a single
// pixel wide, we don't want to render it as an outline, so this method
// of checking ensures that we don't outline single physical-space
// pixel width rectangles.
if (
renderOutlines > 0.0 &&
(
here.y == N.y && here.y != S.y || // Top edge
here.y == S.y && here.y != N.y || // Bottom edge
here.x == E.x && here.x != W.x || // Left edge
here.x == W.x && here.x != E.x
)
) {
// We're on an edge! Draw transparent.
gl_FragColor = vec4(0, 0, 0, 0);
} else {
// Not on an edge. Draw the appropriate color;
gl_FragColor = vec4(colorForBucket(here.z), here.a);
}
}
`,
depth: {
enable: false,
},
attributes: {
// Cover full canvas with a rectangle
// with 2 triangles using a triangle
// strip.
//
// 0 +--+ 1
// | /|
// |/ |
// 2 +--+ 3
position: gl.buffer([[-1, 1], [1, 1], [-1, -1], [1, -1]]),
uv: gl.buffer([[0, 1], [1, 1], [0, 0], [1, 0]]),
},
count: 4,
primitive: 'triangle strip',
uniforms: {
colorTexture: (context, props) => props.rectInfoTexture,
uvTransform: (context, props) => {
const {srcRect, rectInfoTexture} = props
const physicalToUV = AffineTransform.withTranslation(new Vec2(0, 1))
.times(AffineTransform.withScale(new Vec2(1, -1)))
.times(
AffineTransform.betweenRects(
new Rect(Vec2.zero, new Vec2(rectInfoTexture.width, rectInfoTexture.height)),
Rect.unit,
),
)
const uvRect = physicalToUV.transformRect(srcRect)
return AffineTransform.betweenRects(Rect.unit, uvRect).flatten()
},
renderOutlines: (context, props) => {
return props.renderOutlines ? 1.0 : 0.0
},
uvSpacePixelSize: (context, props) => {
return Vec2.unit
.dividedByPointwise(new Vec2(props.rectInfoTexture.width, props.rectInfoTexture.height))
.flatten()
},
positionTransform: (context, props) => {
const {dstRect} = props
const viewportSize = new Vec2(context.viewportWidth, context.viewportHeight)
const physicalToNDC = AffineTransform.withScale(new Vec2(1, -1)).times(
AffineTransform.betweenRects(new Rect(Vec2.zero, viewportSize), Rect.NDC),
)
const ndcRect = physicalToNDC.transformRect(dstRect)
return AffineTransform.betweenRects(Rect.NDC, ndcRect).flatten()
},
},
})
this.buffer = gl.createVertexBuffer(vertexFormat.stride * vertices.length)
this.buffer.uploadFloats(floats)
this.material = gl.createMaterial(vertexFormat, vert, frag)
}
render(props: FlamechartColorPassRenderProps) {
this.command(props)
const {srcRect, rectInfoTexture} = props
const physicalToUV = AffineTransform.withTranslation(new Vec2(0, 1))
.times(AffineTransform.withScale(new Vec2(1, -1)))
.times(
AffineTransform.betweenRects(
new Rect(Vec2.zero, new Vec2(rectInfoTexture.width, rectInfoTexture.height)),
Rect.unit,
),
)
const uvRect = physicalToUV.transformRect(srcRect)
const uvTransform = AffineTransform.betweenRects(Rect.unit, uvRect)
const {dstRect} = props
const viewportSize = new Vec2(this.gl.viewport.width, this.gl.viewport.height)
const physicalToNDC = AffineTransform.withScale(new Vec2(1, -1)).times(
AffineTransform.betweenRects(new Rect(Vec2.zero, viewportSize), Rect.NDC),
)
const ndcRect = physicalToNDC.transformRect(dstRect)
const positionTransform = AffineTransform.betweenRects(Rect.NDC, ndcRect)
const uvSpacePixelSize = Vec2.unit.dividedByPointwise(
new Vec2(props.rectInfoTexture.width, props.rectInfoTexture.height),
)
this.material.setUniformSampler('colorTexture', props.rectInfoTexture, 0)
setUniformAffineTransform(this.material, 'uvTransform', uvTransform)
this.material.setUniformFloat('renderOutlines', props.renderOutlines ? 1.0 : 0.0)
this.material.setUniformVec2('uvSpacePixelSize', uvSpacePixelSize.x, uvSpacePixelSize.y)
setUniformAffineTransform(this.material, 'positionTransform', positionTransform)
this.gl.setUnpremultipliedBlendState()
this.gl.draw(Graphics.Primitive.TRIANGLE_STRIP, this.material, this.buffer)
}
}
+76 -42
View File
@@ -1,11 +1,12 @@
import regl from 'regl'
import {Flamechart} from '../lib/flamechart'
import {RectangleBatch} from './rectangle-batch-renderer'
import {CanvasContext} from './canvas-context'
import {RectangleBatch, RectangleBatchRenderer} from './rectangle-batch-renderer'
import {Vec2, Rect, AffineTransform} from '../lib/math'
import {Color} from '../lib/color'
import {KeyedSet} from '../lib/utils'
import {RowAtlas} from './row-atlas'
import {Graphics} from './graphics'
import {FlamechartColorPassRenderer} from './flamechart-color-pass-renderer'
import {renderInto} from './utils'
const MAX_BATCH_SIZE = 10000
@@ -23,9 +24,7 @@ class RangeTreeLeafNode implements RangeTreeNode {
private batch: RectangleBatch,
private bounds: Rect,
private numPrecedingRectanglesInRow: number,
) {
batch.uploadToGPU()
}
) {}
getBatch() {
return this.batch
@@ -127,13 +126,13 @@ export interface FlamechartRendererOptions {
export class FlamechartRenderer {
private layers: RangeTreeNode[] = []
private rectInfoTexture: regl.Texture
private framebuffer: regl.Framebuffer
constructor(
private canvasContext: CanvasContext,
private gl: Graphics.Context,
private rowAtlas: RowAtlas<FlamechartRowAtlasKey>,
private flamechart: Flamechart,
private rectangleBatchRenderer: RectangleBatchRenderer,
private colorPassRenderer: FlamechartColorPassRenderer,
private options: FlamechartRendererOptions = {inverted: false},
) {
const nLayers = flamechart.getLayers().length
@@ -143,7 +142,7 @@ export class FlamechartRenderer {
let minLeft = Infinity
let maxRight = -Infinity
let batch = canvasContext.createRectangleBatch()
let batch = new RectangleBatch(this.gl)
let rectCount = 0
@@ -161,7 +160,7 @@ export class FlamechartRenderer {
)
minLeft = Infinity
maxRight = -Infinity
batch = canvasContext.createRectangleBatch()
batch = new RectangleBatch(this.gl)
}
const configSpaceBounds = new Rect(
new Vec2(frame.start, y),
@@ -198,10 +197,47 @@ export class FlamechartRenderer {
// range than a tree of always-height-two might make this run faster
this.layers.push(new RangeTreeInteriorNode(leafNodes))
}
this.rectInfoTexture = this.canvasContext.gl.texture({width: 1, height: 1})
this.framebuffer = this.canvasContext.gl.framebuffer({
color: [this.rectInfoTexture],
})
}
private rectInfoTexture: Graphics.Texture | null = null
getRectInfoTexture(width: number, height: number): Graphics.Texture {
if (this.rectInfoTexture) {
const texture = this.rectInfoTexture
if (texture.width != width || texture.height != height) {
texture.resize(width, height)
}
} else {
this.rectInfoTexture = this.gl.createTexture(
Graphics.TextureFormat.NEAREST_CLAMP,
width,
height,
)
}
return this.rectInfoTexture
}
private rectInfoRenderTarget: Graphics.RenderTarget | null = null
getRectInfoRenderTarget(width: number, height: number): Graphics.RenderTarget {
const texture = this.getRectInfoTexture(width, height)
if (this.rectInfoRenderTarget) {
if (this.rectInfoRenderTarget.texture != texture) {
this.rectInfoRenderTarget.texture.free()
this.rectInfoRenderTarget.setColor(texture)
}
}
if (!this.rectInfoRenderTarget) {
this.rectInfoRenderTarget = this.gl.createRenderTarget(texture)
}
return this.rectInfoRenderTarget
}
free() {
if (this.rectInfoRenderTarget) {
this.rectInfoRenderTarget.free()
}
if (this.rectInfoTexture) {
this.rectInfoTexture.free()
}
}
private atlasKeys = new KeyedSet<FlamechartRowAtlasKey>()
@@ -274,6 +310,11 @@ export class FlamechartRenderer {
}
}
// TODO(jlfwong): When I switched the GL backend from regl to the port from
// evanw/sky, rendering uncached even for massive documents seemed fast
// enough. It's possible that the row cache is now unnecessary, but I'll
// leave it around for now since it's not causing issues.
const cacheCapacity = this.rowAtlas.getCapacity()
const keysToRenderCached = atlasKeysToRender.slice(0, cacheCapacity)
const keysToRenderUncached = atlasKeysToRender.slice(cacheCapacity)
@@ -282,24 +323,26 @@ export class FlamechartRenderer {
this.rowAtlas.writeToAtlasIfNeeded(keysToRenderCached, (textureDstRect, key) => {
const configSpaceBounds = this.configSpaceBoundsForKey(key)
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, leaf => {
this.canvasContext.drawRectangleBatch({
this.rectangleBatchRenderer.render({
batch: leaf.getBatch(),
configSpaceSrcRect: configSpaceBounds,
physicalSpaceDstRect: textureDstRect,
parityMin: key.stackDepth % 2 == 0 ? 2 : 0,
parityOffset: leaf.getParity(),
})
})
})
this.framebuffer.resize(physicalSpaceDstRect.width(), physicalSpaceDstRect.height())
this.framebuffer.use(context => {
this.canvasContext.gl.clear({color: [0, 0, 0, 0]})
const renderTarget = this.getRectInfoRenderTarget(
physicalSpaceDstRect.width(),
physicalSpaceDstRect.height(),
)
renderInto(this.gl, renderTarget, () => {
this.gl.clear(new Graphics.Color(0, 0, 0, 0))
const viewportRect = new Rect(
Vec2.zero,
new Vec2(context.viewportWidth, context.viewportHeight),
new Vec2(this.gl.viewport.width, this.gl.viewport.height),
)
const configToViewport = AffineTransform.betweenRects(configSpaceSrcRect, viewportRect)
// Render from the cache
@@ -313,34 +356,25 @@ export class FlamechartRenderer {
const configSpaceBounds = this.configSpaceBoundsForKey(key)
const physicalBounds = configToViewport.transformRect(configSpaceBounds)
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, leaf => {
this.canvasContext.drawRectangleBatch({
this.rectangleBatchRenderer.render({
batch: leaf.getBatch(),
configSpaceSrcRect,
configSpaceSrcRect: configSpaceBounds,
physicalSpaceDstRect: physicalBounds,
parityMin: key.stackDepth % 2 == 0 ? 2 : 0,
parityOffset: leaf.getParity(),
})
})
}
})
this.canvasContext.drawFlamechartColorPass({
rectInfoTexture: this.rectInfoTexture,
srcRect: new Rect(
Vec2.zero,
new Vec2(this.rectInfoTexture.width, this.rectInfoTexture.height),
),
const rectInfoTexture = this.getRectInfoTexture(
physicalSpaceDstRect.width(),
physicalSpaceDstRect.height(),
)
this.colorPassRenderer.render({
rectInfoTexture,
srcRect: new Rect(Vec2.zero, new Vec2(rectInfoTexture.width, rectInfoTexture.height)),
dstRect: physicalSpaceDstRect,
renderOutlines: props.renderOutlines,
})
// Overlay the atlas on top of the canvas for debugging
/*
this.canvasContext.drawTexture({
texture: this.rowAtlas.texture,
srcRect: new Rect(Vec2.zero, new Vec2(this.rowAtlas.texture.width, this.rowAtlas.texture.height)),
dstRect: new Rect(Vec2.zero, new Vec2(800, 800))
})
*/
}
}
+1373
View File
File diff suppressed because it is too large Load Diff
+88 -111
View File
@@ -1,126 +1,103 @@
import regl from 'regl'
import {AffineTransform, Rect} from '../lib/math'
import {Graphics} from './graphics'
import {setUniformAffineTransform, setUniformVec2} from './utils'
export interface ViewportRectangleRendererProps {
configSpaceToPhysicalViewSpace: AffineTransform
configSpaceViewportRect: Rect
}
const vertexFormat = new Graphics.VertexFormat()
vertexFormat.add('position', Graphics.AttributeType.FLOAT, 2)
const vert = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0, 1);
}
`
const frag = `
precision mediump float;
uniform mat3 configSpaceToPhysicalViewSpace;
uniform vec2 physicalSize;
uniform vec2 physicalOrigin;
uniform vec2 configSpaceViewportOrigin;
uniform vec2 configSpaceViewportSize;
uniform float framebufferHeight;
void main() {
vec2 origin = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportOrigin, 1.0)).xy;
vec2 size = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportSize, 0.0)).xy;
vec2 halfSize = physicalSize / 2.0;
float borderWidth = 2.0;
origin = floor(origin * halfSize) / halfSize + borderWidth * vec2(1.0, 1.0);
size = floor(size * halfSize) / halfSize - 2.0 * borderWidth * vec2(1.0, 1.0);
vec2 coord = gl_FragCoord.xy;
coord.x = coord.x - physicalOrigin.x;
coord.y = framebufferHeight - coord.y - physicalOrigin.y;
vec2 clamped = clamp(coord, origin, origin + size);
vec2 gap = clamped - coord;
float maxdist = max(abs(gap.x), abs(gap.y));
// TOOD(jlfwong): Could probably optimize this to use mix somehow.
if (maxdist == 0.0) {
// Inside viewport rectangle
gl_FragColor = vec4(0, 0, 0, 0);
} else if (maxdist < borderWidth) {
// Inside viewport rectangle at border
gl_FragColor = vec4(0.7, 0.7, 0.7, 0.8);
} else {
// Outside viewport rectangle
gl_FragColor = vec4(0.7, 0.7, 0.7, 0.5);
}
}
`
export class ViewportRectangleRenderer {
private command: regl.Command<ViewportRectangleRendererProps>
constructor(regl: regl.Instance) {
this.command = regl<ViewportRectangleRendererProps>({
vert: `
attribute vec2 position;
private material: Graphics.Material
private buffer: Graphics.VertexBuffer
void main() {
gl_Position = vec4(position, 0, 1);
}
`,
frag: `
precision mediump float;
uniform mat3 configSpaceToPhysicalViewSpace;
uniform vec2 physicalSize;
uniform vec2 physicalOrigin;
uniform vec2 configSpaceViewportOrigin;
uniform vec2 configSpaceViewportSize;
uniform float framebufferHeight;
void main() {
vec2 origin = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportOrigin, 1.0)).xy;
vec2 size = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportSize, 0.0)).xy;
vec2 halfSize = physicalSize / 2.0;
float borderWidth = 2.0;
origin = floor(origin * halfSize) / halfSize + borderWidth * vec2(1.0, 1.0);
size = floor(size * halfSize) / halfSize - 2.0 * borderWidth * vec2(1.0, 1.0);
vec2 coord = gl_FragCoord.xy;
coord.x = coord.x - physicalOrigin.x;
coord.y = framebufferHeight - coord.y - physicalOrigin.y;
vec2 clamped = clamp(coord, origin, origin + size);
vec2 gap = clamped - coord;
float maxdist = max(abs(gap.x), abs(gap.y));
// TOOD(jlfwong): Could probably optimize this to use mix somehow.
if (maxdist == 0.0) {
// Inside viewport rectangle
gl_FragColor = vec4(0, 0, 0, 0);
} else if (maxdist < borderWidth) {
// Inside viewport rectangle at border
gl_FragColor = vec4(0.7, 0.7, 0.7, 0.8);
} else {
// Outside viewport rectangle
gl_FragColor = vec4(0.7, 0.7, 0.7, 0.5);
}
}
`,
blend: {
enable: true,
func: {
srcRGB: 'src alpha',
srcAlpha: 'one',
dstRGB: 'one minus src alpha',
dstAlpha: 'one',
},
},
depth: {
enable: false,
},
attributes: {
// Cover full canvas with a rectangle
// with 2 triangles using a triangle
// strip.
//
// 0 +--+ 1
// | /|
// |/ |
// 2 +--+ 3
position: [[-1, 1], [1, 1], [-1, -1], [1, -1]],
},
uniforms: {
configSpaceToPhysicalViewSpace: (context, props) => {
return props.configSpaceToPhysicalViewSpace.flatten()
},
configSpaceViewportOrigin: (context, props) => {
return props.configSpaceViewportRect.origin.flatten()
},
configSpaceViewportSize: (context, props) => {
return props.configSpaceViewportRect.size.flatten()
},
physicalSize: (context, props) => {
return [context.viewportWidth, context.viewportHeight]
},
physicalOrigin: (context, props) => {
return [context.viewportX, context.viewportY]
},
framebufferHeight: (context, props) => {
return context.framebufferHeight
},
},
primitive: 'triangle strip',
count: 4,
})
constructor(private gl: Graphics.Context) {
const vertices = [[-1, 1], [1, 1], [-1, -1], [1, -1]]
const floats: number[] = []
for (let v of vertices) {
floats.push(v[0])
floats.push(v[1])
}
this.buffer = gl.createVertexBuffer(vertexFormat.stride * vertices.length)
this.buffer.upload(new Uint8Array(new Float32Array(floats).buffer))
this.material = gl.createMaterial(vertexFormat, vert, frag)
}
render(props: ViewportRectangleRendererProps) {
this.command(props)
}
setUniformAffineTransform(
this.material,
'configSpaceToPhysicalViewSpace',
props.configSpaceToPhysicalViewSpace,
)
resetStats() {
return Object.assign(this.command.stats, {cpuTime: 0, gpuTime: 0, count: 0})
}
stats() {
return this.command.stats
// TODO(jlfwong): Pack these into a Vec4 instead
setUniformVec2(this.material, 'configSpaceViewportOrigin', props.configSpaceViewportRect.origin)
setUniformVec2(this.material, 'configSpaceViewportSize', props.configSpaceViewportRect.size)
// TODO(jlfwong): Pack these into a Vec4 instead
const viewport = this.gl.viewport
this.material.setUniformVec2('physicalOrigin', viewport.x, viewport.y)
this.material.setUniformVec2('physicalSize', viewport.width, viewport.height)
this.material.setUniformFloat('framebufferHeight', this.gl.renderTargetHeightInPixels)
this.gl.setBlendState(
Graphics.BlendOperation.SOURCE_ALPHA,
Graphics.BlendOperation.INVERSE_SOURCE_ALPHA,
)
this.gl.draw(Graphics.Primitive.TRIANGLE_STRIP, this.material, this.buffer)
}
}
+102 -172
View File
@@ -1,73 +1,99 @@
import regl from 'regl'
import {Rect, Vec2, AffineTransform} from '../lib/math'
import {Color} from '../lib/color'
import {Graphics} from './graphics'
import {setUniformAffineTransform} from './utils'
const vertexFormat = new Graphics.VertexFormat()
vertexFormat.add('configSpacePos', Graphics.AttributeType.FLOAT, 2)
vertexFormat.add('color', Graphics.AttributeType.FLOAT, 3)
const vert = `
uniform mat3 configSpaceToNDC;
attribute vec2 configSpacePos;
attribute vec3 color;
varying vec3 vColor;
void main() {
vColor = color;
vec2 position = (configSpaceToNDC * vec3(configSpacePos, 1)).xy;
gl_Position = vec4(position, 1, 1);
}
`
const frag = `
precision mediump float;
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor.rgb, 1);
}
`
export class RectangleBatch {
private rectCapacity = 1000
private rectCount = 0
private configSpaceOffsets = new Float32Array(this.rectCapacity * 2)
private configSpaceSizes = new Float32Array(this.rectCapacity * 2)
private colors = new Float32Array(this.rectCapacity * 3)
constructor(private gl: regl.Instance) {}
private rects: Rect[] = []
private colors: Color[] = []
constructor(private gl: Graphics.Context) {}
getRectCount() {
return this.rectCount
return this.rects.length
}
private configSpaceOffsetBuffer: regl.Buffer | null = null
getConfigSpaceOffsetBuffer() {
if (!this.configSpaceOffsetBuffer)
this.configSpaceOffsetBuffer = this.gl.buffer(this.configSpaceOffsets)
return this.configSpaceOffsetBuffer
}
private buffer: Graphics.VertexBuffer | null = null
getBuffer(): Graphics.VertexBuffer {
if (this.buffer) {
return this.buffer
}
private configSpaceSizeBuffer: regl.Buffer | null = null
getConfigSpaceSizeBuffer() {
if (!this.configSpaceSizeBuffer)
this.configSpaceSizeBuffer = this.gl.buffer(this.configSpaceSizes)
return this.configSpaceSizeBuffer
}
const corners = [[0, 0], [1, 0], [0, 1], [1, 0], [0, 1], [1, 1]]
private colorBuffer: regl.Buffer | null = null
getColorBuffer() {
if (!this.colorBuffer) this.colorBuffer = this.gl.buffer(this.colors)
return this.colorBuffer
}
const bytes = new Uint8Array(vertexFormat.stride * corners.length * this.rects.length)
const floats = new Float32Array(bytes.buffer)
let idx = 0
uploadToGPU() {
this.getConfigSpaceOffsetBuffer()
this.getConfigSpaceSizeBuffer()
this.getColorBuffer()
for (let i = 0; i < this.rects.length; i++) {
const rect = this.rects[i]
const color = this.colors[i]
// TODO(jlfwong): In the conversion from regl to graphics.ts, I lost the
// ability to do instanced drawing. This is a pretty significant hit to
// the performance here since I need 6x the memory to allocate these
// things. Adding instanced drawing to graphics.ts is non-trivial, so I'm
// just going to try this for now.
for (let corner of corners) {
floats[idx++] = rect.origin.x + corner[0] * rect.size.x
floats[idx++] = rect.origin.y + corner[1] * rect.size.y
floats[idx++] = color.r
floats[idx++] = color.g
floats[idx++] = color.b
}
}
if (idx !== floats.length) {
throw new Error("Buffer expected to be full but wasn't")
}
this.buffer = this.gl.createVertexBuffer(bytes.length)
this.buffer.upload(bytes)
return this.buffer
}
addRect(rect: Rect, color: Color) {
const index = this.rectCount++
if (index >= this.rectCapacity) {
// Not enough capacity, time to resize! We'll double the capacity each time.
this.rectCapacity *= 2
const configSpaceOffsets = new Float32Array(this.rectCapacity * 2)
const configSpaceSizes = new Float32Array(this.rectCapacity * 2)
const colors = new Float32Array(this.rectCapacity * 3)
this.rects.push(rect)
this.colors.push(color)
configSpaceOffsets.set(this.configSpaceOffsets)
configSpaceSizes.set(this.configSpaceSizes)
colors.set(this.colors)
this.configSpaceOffsets = configSpaceOffsets
this.configSpaceSizes = configSpaceSizes
this.colors = colors
if (this.buffer) {
this.buffer.free()
this.buffer = null
}
this.configSpaceOffsets[index * 2] = rect.origin.x
this.configSpaceOffsets[index * 2 + 1] = rect.origin.y
}
this.configSpaceSizes[index * 2] = rect.size.x
this.configSpaceSizes[index * 2 + 1] = rect.size.y
this.colors[index * 3] = color.r
this.colors[index * 3 + 1] = color.g
this.colors[index * 3 + 2] = color.b
free() {
if (this.buffer) {
this.buffer.free()
this.buffer = null
}
}
}
@@ -75,131 +101,35 @@ export interface RectangleBatchRendererProps {
batch: RectangleBatch
configSpaceSrcRect: Rect
physicalSpaceDstRect: Rect
parityMin?: number
parityOffset?: number
}
export class RectangleBatchRenderer {
private command: regl.Command<RectangleBatchRendererProps>
constructor(gl: regl.Instance) {
// We draw the parity / 5 into the depth channel so it can be used in a
// post-processing step to draw boundaries between rectangles. We use 5
// different values (2 per row) + one for the background, so we can
// distingish both between adjacent rectangles on a row and between rows!
this.command = gl({
vert: `
uniform mat3 configSpaceToNDC;
// Non-instanced
attribute vec2 corner;
// Instanced
attribute vec2 configSpaceOffset;
attribute vec2 configSpaceSize;
attribute vec3 color;
attribute float index;
varying vec3 vColor;
void main() {
vColor = color;
vec2 configSpacePos = configSpaceOffset + corner * configSpaceSize;
vec2 position = (configSpaceToNDC * vec3(configSpacePos, 1)).xy;
gl_Position = vec4(position, 1, 1);
}
`,
depth: {
enable: false,
},
frag: `
precision mediump float;
varying vec3 vColor;
varying float vParity;
void main() {
gl_FragColor = vec4(vColor.rgb, 1);
}
`,
attributes: {
// Non-instanced attributes
corner: gl.buffer([[0, 0], [1, 0], [0, 1], [1, 1]]),
// Instanced attributes
configSpaceOffset: (context, props) => {
return {
buffer: props.batch.getConfigSpaceOffsetBuffer(),
offset: 0,
stride: 2 * 4,
size: 2,
divisor: 1,
}
},
configSpaceSize: (context, props) => {
return {
buffer: props.batch.getConfigSpaceSizeBuffer(),
offset: 0,
stride: 2 * 4,
size: 2,
divisor: 1,
}
},
color: (context, props) => {
return {
buffer: props.batch.getColorBuffer(),
offset: 0,
stride: 3 * 4,
size: 3,
divisor: 1,
}
},
},
uniforms: {
configSpaceToNDC: (context, props) => {
const configToPhysical = AffineTransform.betweenRects(
props.configSpaceSrcRect,
props.physicalSpaceDstRect,
)
const viewportSize = new Vec2(context.viewportWidth, context.viewportHeight)
const physicalToNDC = AffineTransform.withTranslation(new Vec2(-1, 1)).times(
AffineTransform.withScale(new Vec2(2, -2).dividedByPointwise(viewportSize)),
)
return physicalToNDC.times(configToPhysical).flatten()
},
parityOffset: (context, props) => {
return props.parityOffset == null ? 0 : props.parityOffset
},
parityMin: (context, props) => {
return props.parityMin == null ? 0 : 1 + props.parityMin
},
},
instances: (context, props) => {
return props.batch.getRectCount()
},
count: 4,
primitive: 'triangle strip',
})
material: Graphics.Material
constructor(private gl: Graphics.Context) {
this.material = gl.createMaterial(vertexFormat, vert, frag)
}
render(props: RectangleBatchRendererProps) {
this.command(props)
}
setUniformAffineTransform(
this.material,
'configSpaceToNDC',
(() => {
const configToPhysical = AffineTransform.betweenRects(
props.configSpaceSrcRect,
props.physicalSpaceDstRect,
)
resetStats() {
return Object.assign(this.command.stats, {cpuTime: 0, gpuTime: 0, count: 0})
}
stats() {
return this.command.stats
const viewportSize = new Vec2(this.gl.viewport.width, this.gl.viewport.height)
const physicalToNDC = AffineTransform.withTranslation(new Vec2(-1, 1)).times(
AffineTransform.withScale(new Vec2(2, -2).dividedByPointwise(viewportSize)),
)
return physicalToNDC.times(configToPhysical)
})(),
)
this.gl.setUnpremultipliedBlendState()
this.gl.draw(Graphics.Primitive.TRIANGLES, this.material, props.batch.getBuffer())
}
}
+24 -21
View File
@@ -1,31 +1,34 @@
import regl from 'regl'
import {LRUCache} from '../lib/lru-cache'
import {RectangleBatch} from './rectangle-batch-renderer'
import {CanvasContext} from './canvas-context'
import {RectangleBatch, RectangleBatchRenderer} from './rectangle-batch-renderer'
import {Rect, Vec2} from '../lib/math'
import {Color} from '../lib/color'
import {Graphics} from './graphics'
import {TextureRenderer} from './texture-renderer'
import {renderInto} from './utils'
export class RowAtlas<K> {
texture: regl.Texture
private framebuffer: regl.Framebuffer
private renderToFramebuffer: regl.Command<{}>
private texture: Graphics.Texture
private renderTarget: Graphics.RenderTarget
private rowCache: LRUCache<K, number>
private clearLineBatch: RectangleBatch
constructor(private canvasContext: CanvasContext) {
this.texture = canvasContext.gl.texture({
width: Math.min(canvasContext.getMaxTextureSize(), 4096),
height: Math.min(canvasContext.getMaxTextureSize(), 4096),
wrapS: 'clamp',
wrapT: 'clamp',
})
this.framebuffer = canvasContext.gl.framebuffer({color: [this.texture]})
constructor(
private gl: Graphics.Context,
private rectangleBatchRenderer: RectangleBatchRenderer,
private textureRenderer: TextureRenderer,
) {
this.texture = gl.createTexture(Graphics.TextureFormat.NEAREST_CLAMP, 4096, 4096)
this.renderTarget = gl.createRenderTarget(this.texture)
this.rowCache = new LRUCache(this.texture.height)
this.renderToFramebuffer = canvasContext.gl({
framebuffer: this.framebuffer,
})
this.clearLineBatch = canvasContext.createRectangleBatch()
this.clearLineBatch = new RectangleBatch(gl)
this.clearLineBatch.addRect(Rect.unit, new Color(0, 0, 0, 0))
// All of the cached data is stored GPU-side, and we don't retain a copy of
// it client-side. This means when we get a context loss event, the data is
// totally gone. So let's clear our CPU-side cache to reflect that fact.
gl.addContextResetHandler(() => {
this.rowCache.clear()
})
}
has(key: K) {
@@ -53,7 +56,7 @@ export class RowAtlas<K> {
}
writeToAtlasIfNeeded(keys: K[], render: (textureDstRect: Rect, key: K) => void) {
this.renderToFramebuffer((context: regl.Context) => {
renderInto(this.gl, this.renderTarget, () => {
for (let key of keys) {
let row = this.rowCache.get(key)
if (row != null) {
@@ -64,7 +67,7 @@ export class RowAtlas<K> {
row = this.allocateLine(key)
const textureRect = new Rect(new Vec2(0, row), new Vec2(this.texture.width, 1))
this.canvasContext.drawRectangleBatch({
this.rectangleBatchRenderer.render({
batch: this.clearLineBatch,
configSpaceSrcRect: Rect.unit,
physicalSpaceDstRect: textureRect,
@@ -84,7 +87,7 @@ export class RowAtlas<K> {
// At this point, we have the row in cache, and we can
// paint directly from it into the framebuffer.
this.canvasContext.drawTexture({
this.textureRenderer.render({
texture: this.texture,
srcRect: textureRect,
dstRect: dstRect,
-183
View File
@@ -1,183 +0,0 @@
import regl from 'regl'
import {Vec2, Rect, AffineTransform} from '../lib/math'
export interface TextureRendererProps {
texture: regl.Texture
srcRect: Rect
dstRect: Rect
}
export class TextureRenderer {
private command: regl.Command<TextureRendererProps>
constructor(gl: regl.Instance) {
this.command = gl({
vert: `
uniform mat3 uvTransform;
uniform mat3 positionTransform;
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = (uvTransform * vec3(uv, 1)).xy;
gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1);
}
`,
frag: `
precision mediump float;
varying vec2 vUv;
uniform sampler2D texture;
void main() {
gl_FragColor = texture2D(texture, vUv);
}
`,
depth: {
enable: false,
},
attributes: {
// Cover full canvas with a rectangle
// with 2 triangles using a triangle
// strip.
//
// 0 +--+ 1
// | /|
// |/ |
// 2 +--+ 3
position: gl.buffer([[-1, 1], [1, 1], [-1, -1], [1, -1]]),
uv: gl.buffer([[0, 1], [1, 1], [0, 0], [1, 0]]),
},
uniforms: {
texture: (context, props) => props.texture,
uvTransform: (context, props) => {
const {srcRect, texture} = props
const physicalToUV = AffineTransform.withTranslation(new Vec2(0, 1))
.times(AffineTransform.withScale(new Vec2(1, -1)))
.times(
AffineTransform.betweenRects(
new Rect(Vec2.zero, new Vec2(texture.width, texture.height)),
Rect.unit,
),
)
const uvRect = physicalToUV.transformRect(srcRect)
return AffineTransform.betweenRects(Rect.unit, uvRect).flatten()
},
positionTransform: (context, props) => {
const {dstRect} = props
const viewportSize = new Vec2(context.viewportWidth, context.viewportHeight)
const physicalToNDC = AffineTransform.withScale(new Vec2(1, -1)).times(
AffineTransform.betweenRects(new Rect(Vec2.zero, viewportSize), Rect.NDC),
)
const ndcRect = physicalToNDC.transformRect(dstRect)
return AffineTransform.betweenRects(Rect.NDC, ndcRect).flatten()
},
},
primitive: 'triangle strip',
count: 4,
})
}
render(props: TextureRendererProps) {
this.command(props)
}
resetStats() {
return Object.assign(this.command.stats, {cpuTime: 0, gpuTime: 0, count: 0})
}
stats() {
return this.command.stats
}
}
export interface TextureCachedRendererOptions<T> {
textureRenderer: TextureRenderer
render(t: T): void
shouldUpdate(oldProps: T, newProps: T): boolean
}
export class TextureCachedRenderer<T> {
private renderUncached: (t: T) => void
private shouldUpdate: (oldProps: T, newProps: T) => boolean
private texture: regl.Texture
private framebuffer: regl.Framebuffer
private textureRenderer: TextureRenderer
private withContext: regl.Command<{}>
constructor(private gl: regl.Instance, options: TextureCachedRendererOptions<T>) {
this.renderUncached = options.render
this.shouldUpdate = options.shouldUpdate
this.textureRenderer = options.textureRenderer
this.texture = gl.texture(1, 1)
this.framebuffer = gl.framebuffer({color: [this.texture]})
this.withContext = gl({})
}
private lastRenderProps: T | null = null
private dirty: boolean = false
setDirty() {
this.dirty = true
}
render(props: T) {
this.withContext((context: regl.Context) => {
let needsRender = false
if (
this.texture.width !== context.viewportWidth ||
this.texture.height !== context.viewportHeight
) {
// TODO(jlfwong): Can probably just use this.framebuffer.resize
this.texture({width: context.viewportWidth, height: context.viewportHeight})
this.framebuffer({color: [this.texture]})
needsRender = true
} else if (this.lastRenderProps == null) {
needsRender = true
} else if (this.shouldUpdate(this.lastRenderProps, props)) {
needsRender = true
} else if (this.dirty) {
needsRender = true
}
if (needsRender) {
this.gl({
viewport: (context, props) => {
return {
x: 0,
y: 0,
width: context.viewportWidth,
height: context.viewportHeight,
}
},
framebuffer: this.framebuffer,
})(() => {
this.gl.clear({color: [0, 0, 0, 0]})
this.renderUncached(props)
})
}
const glViewportRect = new Rect(
Vec2.zero,
new Vec2(context.viewportWidth, context.viewportHeight),
)
// Render from texture
this.textureRenderer.render({
texture: this.texture,
srcRect: glViewportRect,
dstRect: glViewportRect,
})
this.lastRenderProps = props
this.dirty = false
})
}
}
+109
View File
@@ -0,0 +1,109 @@
import {Vec2, Rect, AffineTransform} from '../lib/math'
import {Graphics} from './graphics'
import {setUniformAffineTransform} from './utils'
export interface TextureRendererProps {
texture: Graphics.Texture
srcRect: Rect
dstRect: Rect
}
const vert = `
uniform mat3 uvTransform;
uniform mat3 positionTransform;
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = (uvTransform * vec3(uv, 1)).xy;
gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1);
}
`
const frag = `
precision mediump float;
varying vec2 vUv;
uniform sampler2D texture;
void main() {
gl_FragColor = texture2D(texture, vUv);
}
`
export interface TextureRendererProps {
texture: Graphics.Texture
srcRect: Rect
dstRect: Rect
}
export class TextureRenderer {
private buffer: Graphics.VertexBuffer
private material: Graphics.Material
constructor(private gl: Graphics.Context) {
const vertexFormat = new Graphics.VertexFormat()
vertexFormat.add('position', Graphics.AttributeType.FLOAT, 2)
vertexFormat.add('uv', Graphics.AttributeType.FLOAT, 2)
const vertices = [
{pos: [-1, 1], uv: [0, 1]},
{pos: [1, 1], uv: [1, 1]},
{pos: [-1, -1], uv: [0, 0]},
{pos: [1, -1], uv: [1, 0]},
]
const floats: number[] = []
for (let v of vertices) {
floats.push(v.pos[0])
floats.push(v.pos[1])
floats.push(v.uv[0])
floats.push(v.uv[1])
}
this.buffer = gl.createVertexBuffer(vertexFormat.stride * vertices.length)
this.buffer.upload(new Uint8Array(new Float32Array(floats).buffer))
this.material = gl.createMaterial(vertexFormat, vert, frag)
}
render(props: TextureRendererProps) {
this.material.setUniformSampler('texture', props.texture, 0)
setUniformAffineTransform(
this.material,
'uvTransform',
(() => {
const {srcRect, texture} = props
const physicalToUV = AffineTransform.withTranslation(new Vec2(0, 1))
.times(AffineTransform.withScale(new Vec2(1, -1)))
.times(
AffineTransform.betweenRects(
new Rect(Vec2.zero, new Vec2(texture.width, texture.height)),
Rect.unit,
),
)
const uvRect = physicalToUV.transformRect(srcRect)
return AffineTransform.betweenRects(Rect.unit, uvRect)
})(),
)
setUniformAffineTransform(
this.material,
'positionTransform',
(() => {
const {dstRect} = props
const {viewport} = this.gl
const viewportSize = new Vec2(viewport.width, viewport.height)
const physicalToNDC = AffineTransform.withScale(new Vec2(1, -1)).times(
AffineTransform.betweenRects(new Rect(Vec2.zero, viewportSize), Rect.NDC),
)
const ndcRect = physicalToNDC.transformRect(dstRect)
return AffineTransform.betweenRects(Rect.NDC, ndcRect)
})(),
)
this.gl.setUnpremultipliedBlendState()
this.gl.draw(Graphics.Primitive.TRIANGLE_STRIP, this.material, this.buffer)
}
}
+21
View File
@@ -0,0 +1,21 @@
import {Graphics} from './graphics'
import {AffineTransform, Vec2} from '../lib/math'
export function setUniformAffineTransform(
material: Graphics.Material,
name: string,
transform: AffineTransform,
) {
let {m00, m01, m02, m10, m11, m12} = transform
material.setUniformMat3(name, m00, m01, m02, m10, m11, m12, 0, 0, 1)
}
export function setUniformVec2(material: Graphics.Material, name: string, vec: Vec2) {
material.setUniformVec2(name, vec.x, vec.y)
}
export function renderInto(gl: Graphics.Context, target: Graphics.RenderTarget, cb: () => void) {
gl.setRenderTarget(target)
gl.setViewport(0, 0, target.texture.width, target.texture.height)
cb()
gl.setRenderTarget(null)
}
@@ -50,6 +50,60 @@ Object {
}
`;
exports[`importFromBGFlameGraph with CRLF 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "a",
"line": undefined,
"name": "a",
"selfWeight": 0,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "b",
"line": undefined,
"name": "b",
"selfWeight": 5,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "c",
"line": undefined,
"name": "c",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "d",
"line": undefined,
"name": "d",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "simple-crlf.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
"a;b;c 3",
"a;b 5",
],
}
`;
exports[`importFromBGFlameGraph with CRLF: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with CRLF: profileGroup.name 1`] = `"simple-crlf.txt"`;
exports[`importFromBGFlameGraph: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph: profileGroup.name 1`] = `"simple.txt"`;
+407 -154
View File
@@ -9,8 +9,8 @@ Object {
"key": "(anonymous)::0:0",
"line": 0,
"name": "(anonymous)",
"selfWeight": 11575,
"totalWeight": 45920,
"selfWeight": 1060,
"totalWeight": 34345,
},
Frame {
"col": 10,
@@ -19,7 +19,7 @@ Object {
"line": 0,
"name": "a",
"selfWeight": 0,
"totalWeight": 34345,
"totalWeight": 33285,
},
Frame {
"col": 10,
@@ -28,7 +28,7 @@ Object {
"line": 5,
"name": "b",
"selfWeight": 0,
"totalWeight": 16788,
"totalWeight": 17002,
},
Frame {
"col": 10,
@@ -36,8 +36,8 @@ Object {
"key": "d::13:10",
"line": 13,
"name": "d",
"selfWeight": 34345,
"totalWeight": 34345,
"selfWeight": 33285,
"totalWeight": 33285,
},
Frame {
"col": 10,
@@ -46,14 +46,15 @@ Object {
"line": 9,
"name": "c",
"selfWeight": 0,
"totalWeight": 17557,
"totalWeight": 16283,
},
],
"name": "simple.cpuprofile",
"stacks": Array [
"(anonymous) 11.57ms",
"(anonymous);a;b;d 16.79ms",
"(anonymous);a;c;d 17.56ms",
" 11.57ms",
"(anonymous) 1.06ms",
"(anonymous);a;b;d 17.00ms",
"(anonymous);a;c;d 16.28ms",
],
}
`;
@@ -71,8 +72,8 @@ Object {
"key": "(program)::-1:-1",
"line": -1,
"name": "(program)",
"selfWeight": 12244,
"totalWeight": 12244,
"selfWeight": 2299,
"totalWeight": 2299,
},
Frame {
"col": 10,
@@ -81,7 +82,7 @@ Object {
"line": 0,
"name": "a",
"selfWeight": 0,
"totalWeight": 33184,
"totalWeight": 33186,
},
Frame {
"col": 10,
@@ -89,8 +90,8 @@ Object {
"key": "c:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:13:10",
"line": 13,
"name": "c",
"selfWeight": 16197,
"totalWeight": 16197,
"selfWeight": 16275,
"totalWeight": 16275,
},
Frame {
"col": 10,
@@ -98,170 +99,422 @@ Object {
"key": "b:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:7:10",
"line": 7,
"name": "b",
"selfWeight": 16987,
"totalWeight": 16987,
"selfWeight": 16911,
"totalWeight": 16911,
},
],
"name": "simple-timeline.json",
"stacks": Array [
"(program) 10.16ms",
" 1.99ms",
"(program) 926.00µs",
" 85.95ms",
"(program) 133.00µs",
" 10.06ms",
"(program) 202.00µs",
" 1.97ms",
"(program) 1.04ms",
" 85.89ms",
"(program) 132.00µs",
" 8.40ms",
"(program) 134.00µs",
"a;c 257.00µs",
"a;b 534.00µs",
"a;c 513.00µs",
"a;c 273.00µs",
"a;b 513.00µs",
"a;c 257.00µs",
"a;b 518.00µs",
"a;c 514.00µs",
"a;b 261.00µs",
"a;c 512.00µs",
"a;b 513.00µs",
"a;c 262.00µs",
"a;b 514.00µs",
"a;c 517.00µs",
"a;b 258.00µs",
"a;b 261.00µs",
"a;c 515.00µs",
"a;b 273.00µs",
"a;c 134.00µs",
"a;b 146.00µs",
"a;c 291.00µs",
"a;b 256.00µs",
"a;c 145.00µs",
"a;b 134.00µs",
"a;c 292.00µs",
"a;b 272.00µs",
"a;c 129.00µs",
"a;b 145.00µs",
"a;c 265.00µs",
"a;b 142.00µs",
"a;c 141.00µs",
"a;b 139.00µs",
"a;c 397.00µs",
"a;b 126.00µs",
"a;c 131.00µs",
"a;b 126.00µs",
"a;c 521.00µs",
"a;b 128.00µs",
"a;c 261.00µs",
"a;b 127.00µs",
"a;c 130.00µs",
"a;b 128.00µs",
"a;c 136.00µs",
"a;b 272.00µs",
"a;c 146.00µs",
"a;b 661.00µs",
"a;c 113.00µs",
"a;b 135.00µs",
"a;c 585.00µs",
"a;b 294.00µs",
"a;c 274.00µs",
"a;b 129.00µs",
"a;c 127.00µs",
"a;b 128.00µs",
"a;c 247.00µs",
"a;b 526.00µs",
"a;c 125.00µs",
"a;b 129.00µs",
"a;c 131.00µs",
"a;b 253.00µs",
"a;c 135.00µs",
"a;b 123.00µs",
"a;c 128.00µs",
"a;b 257.00µs",
"a;c 137.00µs",
"a;b 256.00µs",
"a;c 411.00µs",
"a;b 892.00µs",
"a;c 146.00µs",
"a;b 148.00µs",
"a;c 309.00µs",
"a;b 140.00µs",
"a;c 151.00µs",
"a;b 458.00µs",
"a;c 156.00µs",
"a;b 170.00µs",
"a;c 273.00µs",
"a;b 141.00µs",
"a;c 139.00µs",
"a;b 290.00µs",
"a;c 115.00µs",
"a;b 281.00µs",
"a;c 270.00µs",
"a;b 283.00µs",
"a;c 144.00µs",
"a;b 146.00µs",
"a;c 410.00µs",
"a;b 146.00µs",
"a;c 147.00µs",
"a;b 312.00µs",
"a;c 146.00µs",
"a;b 907.00µs",
"a;c 231.00µs",
"a;b 138.00µs",
"a;c 280.00µs",
"a;b 255.00µs",
"a;c 257.00µs",
"a;b 130.00µs",
"a;c 255.00µs",
"a;b 128.00µs",
"a;c 526.00µs",
"a;b 128.00µs",
"a;c 384.00µs",
"a;b 398.00µs",
"a;c 260.00µs",
"a;b 664.00µs",
"a;c 393.00µs",
"a;b 131.00µs",
"a;c 126.00µs",
"a;b 129.00µs",
"a;c 520.00µs",
"a;b 129.00µs",
"a;c 259.00µs",
"a;b 130.00µs",
"a;c 128.00µs",
"a;b 136.00µs",
"a;c 137.00µs",
"a;b 281.00µs",
"a;c 150.00µs",
"a;b 624.00µs",
"a;c 135.00µs",
"a;b 129.00µs",
"a;c 604.00µs",
"a;b 292.00µs",
"a;c 257.00µs",
"a;b 127.00µs",
"a;c 128.00µs",
"a;b 118.00µs",
"a;c 256.00µs",
"a;b 524.00µs",
"a;c 129.00µs",
"a;b 131.00µs",
"a;c 125.00µs",
"a;b 263.00µs",
"a;c 123.00µs",
"a;b 128.00µs",
"a;c 129.00µs",
"a;b 265.00µs",
"a;c 128.00µs",
"a;b 280.00µs",
"a;c 366.00µs",
"a;b 931.00µs",
"a;c 148.00µs",
"a;b 147.00µs",
"a;c 156.00µs",
"a;b 137.00µs",
"a;c 302.00µs",
"a;b 151.00µs",
"a;c 158.00µs",
"a;b 456.00µs",
"a;c 170.00µs",
"a;b 139.00µs",
"a;c 145.00µs",
"a;b 260.00µs",
"a;c 141.00µs",
"a;b 280.00µs",
"a;c 268.00µs",
"a;b 289.00µs",
"a;c 146.00µs",
"a;b 120.00µs",
"a;c 436.00µs",
"a;b 147.00µs",
"a;c 164.00µs",
"a;b 294.00µs",
"a;c 147.00µs",
"a;b 856.00µs",
"a;c 273.00µs",
"a;b 139.00µs",
"a;c 269.00µs",
"a;b 256.00µs",
"a;c 258.00µs",
"a;b 128.00µs",
"a;c 255.00µs",
"a;b 140.00µs",
"a;c 141.00µs",
"a;c 514.00µs",
"a;b 129.00µs",
"a;c 395.00µs",
"a;b 389.00µs",
"a;c 257.00µs",
"a;b 686.00µs",
"a;c 147.00µs",
"a;b 156.00µs",
"a;c 137.00µs",
"a;b 141.00µs",
"a;c 140.00µs",
"a;b 140.00µs",
"a;c 139.00µs",
"a;b 129.00µs",
"a;c 269.00µs",
"a;b 293.00µs",
"a;c 148.00µs",
"a;b 293.00µs",
"a;c 292.00µs",
"a;b 296.00µs",
"a;c 147.00µs",
"a;b 140.00µs",
"a;c 139.00µs",
"a;b 139.00µs",
"a;c 511.00µs",
"a;b 515.00µs",
"a;c 386.00µs",
"a;b 514.00µs",
"a;c 272.00µs",
"a;b 242.00µs",
"a;c 128.00µs",
"a;b 134.00µs",
"a;c 112.00µs",
"a;b 288.00µs",
"a;c 451.00µs",
"a;b 138.00µs",
"a;c 292.00µs",
"a;b 145.00µs",
"a;c 259.00µs",
"a;b 148.00µs",
"a;c 293.00µs",
"a;b 141.00µs",
"a;c 141.00µs",
"a;b 140.00µs",
"a;c 140.00µs",
" 61.61ms",
"(program) 118.00µs",
" 134.00µs",
"(program) 128.00µs",
" 16.40ms",
"(program) 125.00µs",
" 16.64ms",
"(program) 129.00µs",
" 16.72ms",
"(program) 130.00µs",
" 294.15ms",
"(program) 129.00µs",
" 7.68ms",
"a;b 139.00µs",
"a;c 129.00µs",
"a;b 149.00µs",
"a;c 267.00µs",
"a;b 294.00µs",
"a;c 145.00µs",
"a;b 294.00µs",
"a;c 294.00µs",
"a;b 295.00µs",
"a;c 140.00µs",
"a;b 139.00µs",
"a;c 139.00µs",
"a;b 127.00µs",
"a;c 513.00µs",
"a;b 516.00µs",
"a;c 385.00µs",
"a;b 520.00µs",
"a;c 252.00µs",
"a;b 255.00µs",
"a;c 134.00µs",
"a;b 112.00µs",
"a;c 140.00µs",
"a;b 296.00µs",
"a;c 441.00µs",
"a;b 145.00µs",
"a;c 292.00µs",
"a;b 130.00µs",
"a;c 277.00µs",
"a;b 147.00µs",
"a;c 286.00µs",
"a;b 140.00µs",
"a;c 136.00µs",
" 61.59ms",
"(program) 134.00µs",
" 128.00µs",
"(program) 141.00µs",
" 16.39ms",
"(program) 145.00µs",
" 16.63ms",
"(program) 131.00µs",
" 16.71ms",
"(program) 131.00µs",
" 294.15ms",
"(program) 109.00µs",
],
}
`;
exports[`importFromChromeTimeline Chrome 69 1`] = `
Object {
"frames": Array [
Frame {
"col": 0,
"file": "extensions::SafeBuiltins",
"key": "(anonymous):extensions::SafeBuiltins:0:0",
"line": 0,
"name": "(anonymous)",
"selfWeight": 0,
"totalWeight": 3188,
},
Frame {
"col": 9,
"file": "extensions::SafeBuiltins",
"key": "(anonymous):extensions::SafeBuiltins:0:9",
"line": 0,
"name": "(anonymous)",
"selfWeight": 0,
"totalWeight": 3188,
},
Frame {
"col": 20,
"file": "extensions::SafeBuiltins",
"key": "saveBuiltin:extensions::SafeBuiltins:24:20",
"line": 24,
"name": "saveBuiltin",
"selfWeight": 302,
"totalWeight": 3188,
},
Frame {
"col": undefined,
"file": undefined,
"key": "(program):undefined:undefined:undefined",
"line": undefined,
"name": "(program)",
"selfWeight": 6862,
"totalWeight": 6862,
},
Frame {
"col": 0,
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "(anonymous):file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:0:0",
"line": 0,
"name": "(anonymous)",
"selfWeight": 136,
"totalWeight": 37217,
},
Frame {
"col": 14,
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "alpha:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:0:14",
"line": 0,
"name": "alpha",
"selfWeight": 873,
"totalWeight": 37081,
},
Frame {
"col": 13,
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "beta:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:7:13",
"line": 7,
"name": "beta",
"selfWeight": 7552,
"totalWeight": 17562,
},
Frame {
"col": 14,
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "gamma:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:19:14",
"line": 19,
"name": "gamma",
"selfWeight": 20018,
"totalWeight": 20018,
},
Frame {
"col": 14,
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "delta:file:///Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:13:14",
"line": 13,
"name": "delta",
"selfWeight": 7179,
"totalWeight": 18646,
},
Frame {
"col": undefined,
"file": undefined,
"key": "(garbage collector):undefined:undefined:undefined",
"line": undefined,
"name": "(garbage collector)",
"selfWeight": 646,
"totalWeight": 778,
},
],
"name": "simple.json",
"stacks": Array [
" 6.85ms",
"(anonymous);(anonymous);saveBuiltin 302.00µs",
"(anonymous);(anonymous);saveBuiltin;(program) 2.89ms",
" 230.00µs",
"(program) 828.00µs",
" 110.00µs",
"(program) 118.00µs",
" 323.00µs",
"(program) 123.00µs",
" 139.00µs",
"(program) 400.00µs",
"(anonymous) 136.00µs",
"(anonymous);alpha;beta 108.00µs",
"(anonymous);alpha;beta;gamma 268.00µs",
"(anonymous);alpha 137.00µs",
"(anonymous);alpha;delta 139.00µs",
"(anonymous);alpha;delta;gamma 261.00µs",
"(anonymous);alpha;beta;gamma 964.00µs",
"(anonymous);alpha;delta;gamma 268.00µs",
"(anonymous);alpha;beta;gamma 265.00µs",
"(anonymous);alpha;delta;gamma 582.00µs",
"(anonymous);alpha;beta;gamma 135.00µs",
"(anonymous);alpha;delta;gamma 147.00µs",
"(anonymous);alpha;beta;gamma 163.00µs",
"(anonymous);alpha;delta;gamma 133.00µs",
"(anonymous);alpha;beta;gamma 144.00µs",
"(anonymous);alpha;delta;gamma 276.00µs",
"(anonymous);alpha;delta 136.00µs",
"(anonymous);alpha;beta;gamma 144.00µs",
"(anonymous);alpha;delta;gamma 431.00µs",
"(anonymous);alpha;beta;gamma 419.00µs",
"(anonymous);alpha;delta;gamma 134.00µs",
"(anonymous);alpha;beta;gamma 136.00µs",
"(anonymous);alpha;delta;gamma 144.00µs",
"(anonymous);alpha;beta;gamma 269.00µs",
"(anonymous);alpha;delta;gamma 160.00µs",
"(anonymous);alpha;beta;gamma 141.00µs",
"(anonymous);alpha;delta;gamma 292.00µs",
"(anonymous);alpha;beta;gamma 583.00µs",
"(anonymous);alpha;delta;gamma 149.00µs",
"(anonymous);alpha;beta;gamma 139.00µs",
"(anonymous);alpha;delta;gamma 133.00µs",
"(anonymous);alpha;beta;gamma 386.00µs",
"(anonymous);alpha;delta;gamma 385.00µs",
"(anonymous);alpha;beta;gamma 391.00µs",
"(anonymous);alpha;delta;gamma 261.00µs",
"(anonymous);alpha;beta;gamma 131.00µs",
"(anonymous);alpha;delta;gamma 132.00µs",
"(anonymous);alpha 144.00µs",
"(anonymous);alpha;delta;gamma 134.00µs",
"(anonymous);alpha;beta;gamma 146.00µs",
"(anonymous);alpha;delta;gamma 119.00µs",
"(anonymous);alpha;beta;gamma 120.00µs",
"(anonymous);alpha;delta;gamma 134.00µs",
"(anonymous);alpha;beta;gamma 1.47ms",
"(anonymous);alpha;delta;gamma 148.00µs",
"(anonymous);alpha;beta;gamma 147.00µs",
"(anonymous);alpha;delta;gamma 146.00µs",
"(anonymous);alpha;beta;gamma 269.00µs",
"(anonymous);alpha;delta;gamma 138.00µs",
"(anonymous);alpha;beta;gamma 509.00µs",
"(anonymous);alpha;delta;gamma 1.63ms",
"(anonymous);alpha;beta;gamma 432.00µs",
"(anonymous);alpha;delta;gamma 129.00µs",
"(anonymous);alpha;beta;gamma 139.00µs",
"(anonymous);alpha;delta;gamma 389.00µs",
"(anonymous);alpha;beta 133.00µs",
"(anonymous);alpha;beta;gamma 133.00µs",
"(anonymous);alpha;delta;gamma 803.00µs",
"(anonymous);alpha;beta;gamma 283.00µs",
"(anonymous);alpha;delta;gamma 152.00µs",
"(anonymous);alpha;beta;gamma 135.00µs",
"(anonymous);alpha;delta;gamma 132.00µs",
"(anonymous);alpha;beta;gamma 134.00µs",
"(anonymous);alpha;delta;gamma 154.00µs",
"(anonymous);alpha;beta;gamma 145.00µs",
"(anonymous);alpha;delta;gamma 516.00µs",
"(anonymous);alpha;delta 128.00µs",
"(anonymous);alpha;delta;gamma 138.00µs",
"(anonymous);alpha;beta;gamma 139.00µs",
"(anonymous);alpha;delta;gamma 253.00µs",
"(anonymous);alpha;beta;gamma 145.00µs",
"(anonymous);alpha;delta;gamma 450.00µs",
"(anonymous);alpha;beta;gamma 142.00µs",
"(anonymous);alpha;delta;gamma 125.00µs",
"(anonymous);alpha;beta;gamma 289.00µs",
"(anonymous);alpha;delta;gamma 293.00µs",
"(anonymous);alpha;beta;gamma 147.00µs",
"(anonymous);alpha 443.00µs",
"(anonymous);alpha;delta;gamma 133.00µs",
"(anonymous);alpha;beta;gamma 409.00µs",
"(anonymous);alpha;beta 385.00µs",
"(anonymous);alpha;delta 659.00µs",
"(anonymous);alpha;beta 253.00µs",
"(anonymous);alpha;delta 257.00µs",
"(anonymous);alpha;beta 256.00µs",
"(anonymous);alpha;delta 129.00µs",
"(anonymous);alpha;beta 260.00µs",
"(anonymous);alpha;delta 255.00µs",
"(anonymous);alpha;beta 129.00µs",
"(anonymous);alpha;delta 140.00µs",
"(anonymous);alpha;beta 136.00µs",
"(anonymous);alpha;delta 134.00µs",
"(anonymous);alpha;beta 283.00µs",
"(anonymous);alpha;delta 253.00µs",
"(anonymous);alpha;beta 397.00µs",
"(anonymous);alpha;delta 401.00µs",
"(anonymous);alpha;beta 267.00µs",
"(anonymous);alpha;delta 283.00µs",
"(anonymous);alpha;beta 137.00µs",
"(anonymous);alpha;delta 134.00µs",
"(anonymous);alpha;beta 403.00µs",
"(anonymous);alpha;delta 135.00µs",
"(anonymous);alpha;beta 658.00µs",
"(anonymous);alpha;delta 117.00µs",
"(anonymous);alpha;beta 257.00µs",
"(anonymous);alpha;delta 134.00µs",
"(anonymous);alpha;beta 143.00µs",
"(anonymous);alpha;delta 260.00µs",
"(anonymous);alpha 149.00µs",
"(anonymous);alpha;delta 146.00µs",
"(anonymous);alpha;beta 129.00µs",
"(anonymous);alpha;delta 131.00µs",
"(anonymous);alpha;beta 130.00µs",
"(anonymous);alpha;delta 665.00µs",
"(anonymous);alpha;beta 415.00µs",
"(anonymous);alpha;delta 264.00µs",
"(anonymous);alpha;beta 527.00µs",
"(anonymous);alpha;delta 140.00µs",
"(anonymous);alpha;beta 269.00µs",
"(anonymous);alpha;delta 135.00µs",
"(anonymous);alpha;beta 386.00µs",
"(anonymous);alpha;delta 559.00µs",
"(anonymous);alpha;beta 131.00µs",
"(anonymous);alpha;delta 140.00µs",
"(anonymous);alpha;beta 129.00µs",
"(anonymous);alpha;delta 400.00µs",
"(anonymous);alpha;beta 134.00µs",
"(anonymous);alpha;delta 634.00µs",
"(anonymous);alpha;beta 1.10ms",
"(anonymous);alpha;delta 271.00µs",
"(anonymous);alpha;delta;(program) 1.46ms",
" 430.00µs",
"(program) 781.00µs",
" 273.00µs",
"(garbage collector) 646.00µs",
"(garbage collector);(program) 132.00µs",
" 26.36ms",
"(program) 135.00µs",
],
}
`;
exports[`importFromChromeTimeline Chrome 69: indexToView 1`] = `0`;
exports[`importFromChromeTimeline Chrome 69: profileGroup.name 1`] = `"simple.json"`;
exports[`importFromChromeTimeline: indexToView 1`] = `0`;
exports[`importFromChromeTimeline: profileGroup.name 1`] = `"simple-timeline.json"`;
@@ -42,6 +42,7 @@ Object {
],
"name": "simple-firefox.json",
"stacks": Array [
" 2.04ms",
"a;b;d 989.53µs",
"a;c;d 1.02ms",
"a;b;d 982.04µs",
@@ -59,6 +60,71 @@ Object {
}
`;
exports[`importFromFirefox ignore self-hosted 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:1",
"key": "alpha (http://localhost:8000/simple.js:1:14)",
"line": 14,
"name": "alpha",
"selfWeight": 0,
"totalWeight": 26.983816999942064,
},
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:14",
"key": "delta (http://localhost:8000/simple.js:14:14)",
"line": 14,
"name": "delta",
"selfWeight": 0,
"totalWeight": 11.946324001066387,
},
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:20",
"key": "gamma (http://localhost:8000/simple.js:20:14)",
"line": 14,
"name": "gamma",
"selfWeight": 26.983816999942064,
"totalWeight": 26.983816999942064,
},
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:8",
"key": "beta (http://localhost:8000/simple.js:8:13)",
"line": 13,
"name": "beta",
"selfWeight": 0,
"totalWeight": 15.037492998875678,
},
],
"name": "simple-firefox.json",
"stacks": Array [
" 4.61ms",
"alpha;delta;gamma 999.57µs",
"alpha;beta;gamma 2.01ms",
"alpha;delta;gamma 1.00ms",
"alpha;beta;gamma 995.68µs",
"alpha;delta;gamma 996.27µs",
"alpha;beta;gamma 4.01ms",
"alpha;delta;gamma 2.02ms",
"alpha;beta;gamma 959.44µs",
"alpha;delta;gamma 2.01ms",
"alpha;beta;gamma 4.01ms",
"alpha;delta;gamma 959.28µs",
"alpha;beta;gamma 2.03ms",
"alpha;delta;gamma 3.96ms",
"alpha;beta;gamma 1.02ms",
],
}
`;
exports[`importFromFirefox ignore self-hosted: indexToView 1`] = `0`;
exports[`importFromFirefox ignore self-hosted: profileGroup.name 1`] = `"simple-firefox.json"`;
exports[`importFromFirefox recursion 1`] = `
Object {
"frames": Array [
@@ -110,6 +176,7 @@ Object {
],
"name": "recursion.json",
"stacks": Array [
" 1.71ms",
"main;alpha;beta;alpha;beta;alpha;beta;alpha;delta;gamma 998.89µs",
"main 1.19ms",
"main;alpha;beta;alpha;delta;gamma 1.83ms",
@@ -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"`;
+249
View File
@@ -0,0 +1,249 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importAsPprofProfile 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
"key": "runtime.main:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
"line": 0,
"name": "runtime.main",
"selfWeight": 0,
"totalWeight": 136,
},
Frame {
"col": undefined,
"file": "/Users/jlfwong/code/speedscope/sample/programs/go/simple.go",
"key": "main.main:/Users/jlfwong/code/speedscope/sample/programs/go/simple.go:0",
"line": 0,
"name": "main.main",
"selfWeight": 0,
"totalWeight": 136,
},
Frame {
"col": undefined,
"file": "/Users/jlfwong/code/speedscope/sample/programs/go/simple.go",
"key": "main.delta:/Users/jlfwong/code/speedscope/sample/programs/go/simple.go:0",
"line": 0,
"name": "main.delta",
"selfWeight": 22,
"totalWeight": 58,
},
Frame {
"col": undefined,
"file": "/Users/jlfwong/code/speedscope/sample/programs/go/simple.go",
"key": "main.beta:/Users/jlfwong/code/speedscope/sample/programs/go/simple.go:0",
"line": 0,
"name": "main.beta",
"selfWeight": 39,
"totalWeight": 39,
},
Frame {
"col": undefined,
"file": "/Users/jlfwong/code/speedscope/sample/programs/go/simple.go",
"key": "main.alpha:/Users/jlfwong/code/speedscope/sample/programs/go/simple.go:0",
"line": 0,
"name": "main.alpha",
"selfWeight": 48,
"totalWeight": 48,
},
Frame {
"col": undefined,
"file": "/Users/jlfwong/code/speedscope/sample/programs/go/simple.go",
"key": "main.gamma:/Users/jlfwong/code/speedscope/sample/programs/go/simple.go:0",
"line": 0,
"name": "main.gamma",
"selfWeight": 27,
"totalWeight": 27,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/asm_amd64.s",
"key": "runtime.morestack:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/asm_amd64.s:0",
"line": 0,
"name": "runtime.morestack",
"selfWeight": 0,
"totalWeight": 11,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/stack.go",
"key": "runtime.newstack:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/stack.go:0",
"line": 0,
"name": "runtime.newstack",
"selfWeight": 0,
"totalWeight": 11,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/duff_amd64.s",
"key": "runtime.duffcopy:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/duff_amd64.s:0",
"line": 0,
"name": "runtime.duffcopy",
"selfWeight": 11,
"totalWeight": 11,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/time.go",
"key": "runtime.timerproc:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/time.go:0",
"line": 0,
"name": "runtime.timerproc",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/time.go",
"key": "runtime.goroutineReady:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/time.go:0",
"line": 0,
"name": "runtime.goroutineReady",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
"key": "runtime.goready:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
"line": 0,
"name": "runtime.goready",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/asm_amd64.s",
"key": "runtime.systemstack:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/asm_amd64.s:0",
"line": 0,
"name": "runtime.systemstack",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
"key": "runtime.goready.func1:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
"line": 0,
"name": "runtime.goready.func1",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
"key": "runtime.ready:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
"line": 0,
"name": "runtime.ready",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
"key": "runtime.wakep:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
"line": 0,
"name": "runtime.wakep",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
"key": "runtime.startm:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
"line": 0,
"name": "runtime.startm",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/lock_sema.go",
"key": "runtime.notewakeup:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/lock_sema.go:0",
"line": 0,
"name": "runtime.notewakeup",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/os_darwin.go",
"key": "runtime.semawakeup:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/os_darwin.go:0",
"line": 0,
"name": "runtime.semawakeup",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/os_darwin.go",
"key": "runtime.mach_semrelease:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/os_darwin.go:0",
"line": 0,
"name": "runtime.mach_semrelease",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/sys_darwin_amd64.s",
"key": "runtime.mach_semaphore_signal:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/sys_darwin_amd64.s:0",
"line": 0,
"name": "runtime.mach_semaphore_signal",
"selfWeight": 1,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
"key": "runtime.mstart:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
"line": 0,
"name": "runtime.mstart",
"selfWeight": 0,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
"key": "runtime.mstart1:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
"line": 0,
"name": "runtime.mstart1",
"selfWeight": 0,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go",
"key": "runtime.sysmon:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/proc.go:0",
"line": 0,
"name": "runtime.sysmon",
"selfWeight": 0,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": "/usr/local/Cellar/go/1.10.1/libexec/src/runtime/sys_darwin_amd64.s",
"key": "runtime.usleep:/usr/local/Cellar/go/1.10.1/libexec/src/runtime/sys_darwin_amd64.s:0",
"line": 0,
"name": "runtime.usleep",
"selfWeight": 2,
"totalWeight": 2,
},
],
"name": "simple.prof",
"stacks": Array [
"runtime.main;main.main;main.delta;main.beta 14",
"runtime.main;main.main;main.alpha 26",
"runtime.main;main.main;main.gamma 27",
"runtime.morestack;runtime.newstack;runtime.duffcopy 11",
"runtime.main;main.main;main.beta 25",
"runtime.main;main.main;main.delta 22",
"runtime.main;main.main;main.delta;main.alpha 22",
"runtime.timerproc;runtime.goroutineReady;runtime.goready;runtime.systemstack;runtime.goready.func1;runtime.ready;runtime.wakep;runtime.startm;runtime.notewakeup;runtime.semawakeup;runtime.mach_semrelease;runtime.mach_semaphore_signal 1",
"runtime.mstart;runtime.mstart1;runtime.sysmon;runtime.usleep 2",
],
}
`;
exports[`importAsPprofProfile: indexToView 1`] = `0`;
exports[`importAsPprofProfile: profileGroup.name 1`] = `"simple.prof"`;
File diff suppressed because it is too large Load Diff
+4
View File
@@ -3,3 +3,7 @@ import {checkProfileSnapshot} from '../lib/test-utils'
test('importFromBGFlameGraph', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple.txt')
})
test('importFromBGFlameGraph with CRLF', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-crlf.txt')
})
+4
View File
@@ -7,3 +7,7 @@ test('importFromChromeCPUProfile', async () => {
test('importFromChromeTimeline', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/65/simple-timeline.json')
})
test('importFromChromeTimeline Chrome 69', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/69/simple.json')
})
+102 -25
View File
@@ -1,7 +1,9 @@
import {Profile, FrameInfo, CallTreeProfileBuilder} from '../lib/profile'
import {getOrInsert, lastOf} from '../lib/utils'
import {getOrInsert, lastOf, sortBy} from '../lib/utils'
import {TimeFormatter} from '../lib/value-formatters'
// See: https://github.com/v8/v8/blob/master/src/inspector/js_protocol.json
interface TimelineEvent {
pid: number
tid: number
@@ -45,16 +47,77 @@ interface CPUProfile {
timeDeltas: number[]
}
export function isChromeTimeline(rawProfile: any): boolean {
if (!Array.isArray(rawProfile)) return false
if (rawProfile.length < 1) return false
const first = rawProfile[0]
if (!('pid' in first && 'tid' in first && 'ph' in first && 'cat' in first)) return false
if (
!rawProfile.find(
e => e.name === 'CpuProfile' || e.name === 'Profile' || e.name === 'ProfileChunk',
)
)
return false
return true
}
export function importFromChromeTimeline(events: TimelineEvent[]): Profile {
// It seems like sometimes Chrome timeline files contain multiple CpuProfiles?
// For now, choose the first one in the list.
let cpuProfile: CPUProfile | null = null
// The events aren't necessarily recorded in chronological order. Sort them so
// that they are.
sortBy(events, e => e.ts)
for (let event of events) {
if (event.name == 'CpuProfile') {
const chromeProfile = event.args.data.cpuProfile as CPUProfile
return importFromChromeCPUProfile(chromeProfile)
cpuProfile = event.args.data.cpuProfile as CPUProfile
}
if (event.name == 'Profile') {
cpuProfile = {
startTime: 0,
endTime: 0,
nodes: [],
samples: [],
timeDeltas: [],
...event.args.data,
}
}
if (event.name == 'ProfileChunk') {
if (cpuProfile) {
const chunk = event.args.data
if (chunk.cpuProfile) {
if (chunk.cpuProfile.nodes) {
cpuProfile.nodes = cpuProfile.nodes.concat(chunk.cpuProfile.nodes)
}
if (chunk.cpuProfile.samples) {
cpuProfile.samples = cpuProfile.samples.concat(chunk.cpuProfile.samples)
}
}
if (chunk.timeDeltas) {
cpuProfile.timeDeltas = cpuProfile.timeDeltas.concat(chunk.timeDeltas)
}
if (chunk.startTime != null) {
cpuProfile.startTime = chunk.startTime
}
if (chunk.endTime != null) {
cpuProfile.endTime = chunk.endTime
}
} else {
console.log('Ignoring ProfileChunk when no Profile is active')
}
}
}
throw new Error('Could not find CPU profile in Timeline')
if (cpuProfile) {
return importFromChromeCPUProfile(cpuProfile as CPUProfile)
} else {
throw new Error('Could not find CPU profile in Timeline')
}
}
const callFrameToFrameInfo = new Map<CPUProfileCallFrame, FrameInfo>()
@@ -74,7 +137,14 @@ function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
})
}
function shouldIgnoreFunction(functionName: string) {
function shouldIgnoreFunction(callFrame: CPUProfileCallFrame) {
const {functionName, url} = callFrame
if (url === 'native dummy.js') {
// I'm not really sure what this is about, but this seems to be used
// as a way of avoiding edge cases in V8's implementation.
// See: https://github.com/v8/v8/blob/b8626ca4/tools/js2c.py#L419-L424
return true
}
return functionName === '(root)' || functionName === '(idle)'
}
@@ -90,6 +160,10 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
nodeById.set(node.id, node)
}
for (let node of chromeProfile.nodes) {
if (typeof node.parent === 'number') {
node.parent = nodeById.get(node.parent)
}
if (!node.children) continue
for (let childId of node.children) {
const child = nodeById.get(childId)
@@ -99,9 +173,12 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
}
const samples: number[] = []
const timeDeltas: number[] = []
const sampleTimes: number[] = []
// The first delta is relative to the profile startTime.
// Ref: https://github.com/v8/v8/blob/44bd8fd7/src/inspector/js_protocol.json#L1485
let elapsed = chromeProfile.timeDeltas[0]
let elapsed = 0
let lastNodeId = NaN
// The chrome CPU profile format doesn't collapse identical samples. We'll do that
@@ -110,29 +187,30 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
const nodeId = chromeProfile.samples[i]
if (nodeId != lastNodeId) {
samples.push(nodeId)
timeDeltas.push(elapsed)
elapsed = 0
sampleTimes.push(elapsed)
}
let timeDelta = chromeProfile.timeDeltas[i]
if (timeDelta < 0) {
console.warn('Substituting zero for unexpected time delta:', timeDelta, 'at index', i)
timeDelta = 0
}
if (i === chromeProfile.samples.length - 1) {
if (!isNaN(lastNodeId)) {
samples.push(lastNodeId)
sampleTimes.push(elapsed)
}
} else {
let timeDelta = chromeProfile.timeDeltas[i + 1]
if (timeDelta < 0) {
console.warn('Substituting zero for unexpected time delta:', timeDelta, 'at index', i)
timeDelta = 0
}
elapsed += timeDelta
lastNodeId = nodeId
}
if (!isNaN(lastNodeId)) {
samples.push(lastNodeId)
timeDeltas.push(elapsed)
elapsed += timeDelta
lastNodeId = nodeId
}
}
let prevStack: CPUProfileNode[] = []
let value = 0
for (let i = 0; i < samples.length; i++) {
const timeDelta = timeDeltas[i + 1] || 0
const value = sampleTimes[i]
const nodeId = samples[i]
let stackTop = nodeById.get(nodeId)
if (!stackTop) continue
@@ -161,7 +239,7 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
const toOpen: CPUProfileNode[] = []
for (
let node: CPUProfileNode | null = stackTop;
node && node != lca && !shouldIgnoreFunction(node.callFrame.functionName);
node && node != lca && !shouldIgnoreFunction(node.callFrame);
// Place Chrome internal functions on top of the previous call stack
node = shouldPlaceOnTopOfPreviousStack(node.callFrame.functionName)
? lastOf(prevStack)
@@ -176,12 +254,11 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
}
prevStack = prevStack.concat(toOpen)
value += timeDelta
}
// Close frames that are open at the end of the trace
for (let i = prevStack.length - 1; i >= 0; i--) {
profile.leaveFrame(frameInfoForCallFrame(prevStack[i].callFrame), value)
profile.leaveFrame(frameInfoForCallFrame(prevStack[i].callFrame), lastOf(sampleTimes)!)
}
profile.setValueFormatter(new TimeFormatter('microseconds'))
+4
View File
@@ -7,3 +7,7 @@ test('importFromFirefox', async () => {
test('importFromFirefox recursion', async () => {
await checkProfileSnapshot('./sample/profiles/Firefox/61/recursion.json')
})
test('importFromFirefox ignore self-hosted', async () => {
await checkProfileSnapshot('./sample/profiles/Firefox/63/simple-firefox.json')
})
+5 -1
View File
@@ -180,7 +180,11 @@ export function importFromFirefox(firefoxProfile: FirefoxProfile): Profile {
if (!match) return null
if (match[2].startsWith('resource:') || match[2] === 'self-hosted') {
if (
match[2].startsWith('resource:') ||
match[2] === 'self-hosted' ||
match[2].startsWith('self-hosted:')
) {
// Ignore Firefox-internals stuff
return null
}
+5
View File
@@ -0,0 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importFromHaskell', async () => {
await checkProfileSnapshot('./sample/profiles/haskell/simple.prof')
})
+98
View File
@@ -0,0 +1,98 @@
import {ProfileGroup, FrameInfo, CallTreeProfileBuilder} from '../lib/profile'
import {TimeFormatter, ByteFormatter} from '../lib/value-formatters'
// See https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/profiling.html#json-profile-format
// for information on the GHC profiler JSON output format.
interface CostCentre {
id: number
label: string
module: string
src_loc: string
is_caf: boolean
}
interface ProfileTree {
id: number
entries: number
alloc: number
ticks: number
children: ProfileTree[]
}
interface HaskellProfile {
program: string
arguments: string[]
rts_arguments: string[]
end_time: string
initial_capabilities: number
total_time: number
total_ticks: number
tick_interval: number
total_alloc: number
cost_centres: CostCentre[]
profile: ProfileTree
}
// The profiler already collapses recursion before output so using the JS stack here should be fine
function addToProfile(
tree: ProfileTree,
startVal: number,
profile: CallTreeProfileBuilder,
infos: Map<number, FrameInfo>,
attribute: (tree: ProfileTree) => number,
): number {
// If the expression never did anything we don't care about it
if (tree.ticks === 0 && tree.entries === 0 && tree.alloc === 0 && tree.children.length === 0)
return startVal
let curVal = startVal
let frameInfo = infos.get(tree.id)!
profile.enterFrame(frameInfo, curVal)
for (let child of tree.children) {
curVal = addToProfile(child, curVal, profile, infos, attribute)
}
curVal += attribute(tree)
profile.leaveFrame(frameInfo, curVal)
return curVal
}
export function importFromHaskell(haskellProfile: HaskellProfile): ProfileGroup {
const idToFrameInfo = new Map<number, FrameInfo>()
for (let centre of haskellProfile.cost_centres) {
const frameInfo: FrameInfo = {
key: centre.id,
name: `${centre.module}.${centre.label}`,
}
// Ignore things like <entire-module> and <no location info>
if (!centre.src_loc.startsWith('<')) {
// This also contains line and column information, but sometimes it contains ranges,
// and in varying formats, so it's a better experience just to leave it as is
frameInfo.file = centre.src_loc
}
idToFrameInfo.set(centre.id, frameInfo)
}
const timeProfile = new CallTreeProfileBuilder(haskellProfile.total_ticks)
addToProfile(haskellProfile.profile, 0, timeProfile, idToFrameInfo, tree => tree.ticks)
timeProfile.setValueFormatter(new TimeFormatter('milliseconds'))
timeProfile.setName(`${haskellProfile.program} time`)
const allocProfile = new CallTreeProfileBuilder(haskellProfile.total_ticks)
addToProfile(haskellProfile.profile, 0, allocProfile, idToFrameInfo, tree => tree.alloc)
allocProfile.setValueFormatter(new ByteFormatter())
allocProfile.setName(`${haskellProfile.program} allocation`)
return {
name: haskellProfile.program,
indexToView: 0,
profiles: [timeProfile.build(), allocProfile.build()],
}
}
+4 -4
View File
@@ -1,8 +1,8 @@
import {importProfileGroup} from '.'
import {importProfileGroupFromText} from '.'
test('importProfileGroup', async () => {
// Importing garbage should return null
expect(await importProfileGroup('unknown', '')).toBe(null)
expect(await importProfileGroup('unknown', 'Hello world')).toBe(null)
expect(await importProfileGroup('unknown', 'Hello\n\nWorld')).toBe(null)
expect(await importProfileGroupFromText('unknown', '')).toBe(null)
expect(await importProfileGroupFromText('unknown', 'Hello world')).toBe(null)
expect(await importProfileGroupFromText('unknown', 'Hello\n\nWorld')).toBe(null)
})
+60 -9
View File
@@ -1,7 +1,7 @@
import {Profile, ProfileGroup} from '../lib/profile'
import {FileSystemDirectoryEntry} from './file-system-entry'
import {importFromChromeCPUProfile, importFromChromeTimeline} from './chrome'
import {importFromChromeCPUProfile, importFromChromeTimeline, isChromeTimeline} from './chrome'
import {importFromStackprof} from './stackprof'
import {importFromInstrumentsDeepCopy, importFromInstrumentsTrace} from './instruments'
import {importFromBGFlameGraph} from './bg-flamegraph'
@@ -9,12 +9,43 @@ import {importFromFirefox} from './firefox'
import {importSpeedscopeProfiles} from '../lib/file-format'
import {importFromV8ProfLog} from './v8proflog'
import {importFromLinuxPerf} from './linux-tools-perf'
import {importFromHaskell} from './haskell'
import {ProfileDataSource, TextProfileDataSource, MaybeCompressedDataReader} from './utils'
import {importAsPprofProfile} from './pprof'
import {decodeBase64} from '../lib/utils'
import {importFromChromeHeapProfile} from './v8heapalloc'
export async function importProfileGroup(
export async function importProfileGroupFromText(
fileName: string,
contents: string,
): Promise<ProfileGroup | null> {
const profileGroup = await _importProfileGroup(fileName, contents)
return await importProfileGroup(new TextProfileDataSource(fileName, contents))
}
export async function importProfileGroupFromBase64(
fileName: string,
b64contents: string,
): Promise<ProfileGroup | null> {
return await importProfileGroup(
MaybeCompressedDataReader.fromArrayBuffer(fileName, decodeBase64(b64contents).buffer),
)
}
export async function importProfilesFromFile(file: File): Promise<ProfileGroup | null> {
return importProfileGroup(MaybeCompressedDataReader.fromFile(file))
}
export async function importProfilesFromArrayBuffer(
fileName: string,
buffer: ArrayBuffer,
): Promise<ProfileGroup | null> {
return importProfileGroup(MaybeCompressedDataReader.fromArrayBuffer(fileName, buffer))
}
async function importProfileGroup(dataSource: ProfileDataSource): Promise<ProfileGroup | null> {
const fileName = await dataSource.name()
const profileGroup = await _importProfileGroup(dataSource)
if (profileGroup) {
if (!profileGroup.name) {
profileGroup.name = fileName
@@ -34,10 +65,21 @@ function toGroup(profile: Profile | null): ProfileGroup | null {
return {name: profile.getName(), indexToView: 0, profiles: [profile]}
}
async function _importProfileGroup(
fileName: string,
contents: string,
): Promise<ProfileGroup | null> {
async function _importProfileGroup(dataSource: ProfileDataSource): Promise<ProfileGroup | null> {
const fileName = await dataSource.name()
const buffer = await dataSource.readAsArrayBuffer()
{
const profile = importAsPprofProfile(buffer)
if (profile) {
console.log('Importing as protobuf encoded pprof file')
return toGroup(profile)
}
}
const contents = await dataSource.readAsText()
// First pass: Check known file format names to infer the file type
if (fileName.endsWith('.speedscope.json')) {
console.log('Importing as speedscope json file')
@@ -63,6 +105,9 @@ async function _importProfileGroup(
} else if (fileName.endsWith('.v8log.json')) {
console.log('Importing as --prof-process v8 log')
return toGroup(importFromV8ProfLog(JSON.parse(contents)))
} else if (fileName.endsWith('.heapprofile')) {
console.log('Importing as Chrome Heap Profile')
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
}
// Second pass: Try to guess what file format it is based on structure
@@ -77,7 +122,7 @@ async function _importProfileGroup(
} else if (parsed['systemHost'] && parsed['systemHost']['name'] == 'Firefox') {
console.log('Importing as Firefox profile')
return toGroup(importFromFirefox(parsed))
} else if (Array.isArray(parsed) && parsed[parsed.length - 1].name === 'CpuProfile') {
} else if (isChromeTimeline(parsed)) {
console.log('Importing as Chrome CPU Profile')
return toGroup(importFromChromeTimeline(parsed))
} else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) {
@@ -89,6 +134,12 @@ async function _importProfileGroup(
} else if ('code' in parsed && 'functions' in parsed && 'ticks' in parsed) {
console.log('Importing as --prof-process v8 log')
return toGroup(importFromV8ProfLog(parsed))
} else if ('head' in parsed && 'selfSize' in parsed['head']) {
console.log('Importing as Chrome Heap Profile')
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
} else if ('rts_arguments' in parsed && 'initial_capabilities' in parsed) {
console.log('Importing as Haskell GHC JSON Profile')
return importFromHaskell(parsed)
}
} else {
// Format is not JSON
@@ -103,7 +154,7 @@ async function _importProfileGroup(
// If every line ends with a space followed by a number, it's probably
// the collapsed stack format.
const lineCount = contents.split(/\n/).length
if (lineCount >= 1 && lineCount === contents.split(/ \d+\n/).length) {
if (lineCount >= 1 && lineCount === contents.split(/ \d+\r?\n/).length) {
console.log('Importing as collapsed stack format')
return toGroup(importFromBGFlameGraph(contents))
}
+3 -47
View File
@@ -9,9 +9,9 @@ import {
ProfileGroup,
} from '../lib/profile'
import {sortBy, getOrThrow, getOrInsert, lastOf, getOrElse, zeroPad} from '../lib/utils'
import * as pako from 'pako'
import {ByteFormatter, TimeFormatter} from '../lib/value-formatters'
import {FileSystemDirectoryEntry, FileSystemEntry, FileSystemFileEntry} from './file-system-entry'
import {MaybeCompressedDataReader} from './utils'
function parseTSV<T>(contents: string): T[] {
const lines = contents.split('\n').map(l => l.split('\t'))
@@ -183,56 +183,12 @@ async function extractDirectoryTree(entry: FileSystemDirectoryEntry): Promise<Tr
return node
}
class MaybeCompressedFileReader {
private fileData: Promise<ArrayBuffer>
constructor(file: File) {
this.fileData = new Promise(resolve => {
const reader = new FileReader()
reader.addEventListener('loadend', () => {
if (!(reader.result instanceof ArrayBuffer)) {
throw new Error('Expected reader.result to be an instance of ArrayBuffer')
}
resolve(reader.result)
})
reader.readAsArrayBuffer(file)
})
}
private async getUncompressed(): Promise<ArrayBuffer> {
const fileData = await this.fileData
try {
const result = pako.inflate(new Uint8Array(fileData)).buffer
return result
} catch (e) {
return fileData
}
}
async readAsArrayBuffer(): Promise<ArrayBuffer> {
return await this.getUncompressed()
}
async readAsText(): Promise<string> {
const buffer = await this.getUncompressed()
let ret: string = ''
// JavaScript strings are UTF-16 encoded, but this data is coming
// from a UTF-8 encoded file.
const array = new Uint8Array(buffer)
for (let i = 0; i < array.length; i++) {
ret += String.fromCharCode(array[i])
}
return ret
}
}
function readAsArrayBuffer(file: File): Promise<ArrayBuffer> {
return new MaybeCompressedFileReader(file).readAsArrayBuffer()
return MaybeCompressedDataReader.fromFile(file).readAsArrayBuffer()
}
function readAsText(file: File): Promise<string> {
return new MaybeCompressedFileReader(file).readAsText()
return MaybeCompressedDataReader.fromFile(file).readAsText()
}
function getCoreDirForRun(tree: TraceDirectoryTree, selectedRun: number): TraceDirectoryTree {
+5
View File
@@ -0,0 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importAsPprofProfile', async () => {
await checkProfileSnapshot('./sample/profiles/pprof/simple.prof')
})
+134
View File
@@ -0,0 +1,134 @@
import {perftools} from './profile.proto.js'
import {FrameInfo, StackListProfileBuilder, Profile} from '../lib/profile'
import {lastOf} from '../lib/utils'
import {TimeFormatter, ByteFormatter} from '../lib/value-formatters'
interface SampleType {
type: string
unit: string
}
export function importAsPprofProfile(rawProfile: ArrayBuffer): Profile | null {
if (rawProfile.byteLength === 0) return null
let protoProfile: perftools.profiles.Profile
try {
protoProfile = perftools.profiles.Profile.decode(new Uint8Array(rawProfile))
} catch (e) {
return null
}
function i32(n: number | Long): number {
return typeof n === 'number' ? n : (n as Long).low
}
function stringVal(key: number | Long): string | null {
return protoProfile.stringTable[i32(key)] || null
}
const frameInfoByFunctionID = new Map<number, FrameInfo>()
function frameInfoForFunction(f: perftools.profiles.IFunction): FrameInfo | null {
const {name, filename, startLine} = f
const nameString = (name != null && stringVal(name)) || '(unknown)'
const fileNameString = filename != null ? stringVal(filename) : null
const line = startLine != null ? +startLine : null
const key = `${nameString}:${fileNameString}:${line}`
const frameInfo: FrameInfo = {
key,
name: nameString,
}
if (fileNameString != null) {
frameInfo.file = fileNameString
}
if (line != null) {
frameInfo.line = line
}
return frameInfo
}
for (let f of protoProfile.function) {
if (f.id) {
const frameInfo = frameInfoForFunction(f)
if (frameInfo != null) {
frameInfoByFunctionID.set(i32(f.id), frameInfo)
}
}
}
function frameInfoForLocation(location: perftools.profiles.ILocation): FrameInfo | null {
const {line} = location
if (line == null) return null
// From a comment on profile.proto:
//
// Multiple line indicates this location has inlined functions,
// where the last entry represents the caller into which the
// preceding entries were inlined.
//
// E.g., if memcpy() is inlined into printf:
// line[0].function_name == "memcpy"
// line[1].function_name == "printf"
//
// Let's just take the last line then
const lastLine = lastOf(line)
if (lastLine == null) return null
if (lastLine.functionId) {
return frameInfoByFunctionID.get(i32(lastLine.functionId)) || null
} else {
return null
}
}
const frameByLocationID = new Map<number, FrameInfo>()
for (let l of protoProfile.location) {
if (l.id != null) {
const frameInfo = frameInfoForLocation(l)
if (frameInfo) {
frameByLocationID.set(i32(l.id), frameInfo)
}
}
}
const sampleTypes: SampleType[] = protoProfile.sampleType.map(type => ({
type: (type.type && stringVal(type.type)) || 'samples',
unit: (type.unit && stringVal(type.unit)) || 'count',
}))
const sampleTypeIndex = protoProfile.defaultSampleType
? +protoProfile.defaultSampleType
: sampleTypes.length - 1
const sampleType = sampleTypes[sampleTypeIndex]
const profileBuilder = new StackListProfileBuilder()
switch (sampleType.unit) {
case 'nanoseconds':
case 'microseconds':
case 'milliseconds':
case 'seconds':
profileBuilder.setValueFormatter(new TimeFormatter(sampleType.unit))
break
case 'bytes':
profileBuilder.setValueFormatter(new ByteFormatter())
break
}
for (let s of protoProfile.sample) {
const stack = s.locationId ? s.locationId.map(l => frameByLocationID.get(i32(l))) : []
stack.reverse()
const value = s.value![sampleTypeIndex]
profileBuilder.appendSampleWithWeight(stack.filter(f => f != null) as FrameInfo[], +value)
}
return profileBuilder.build()
}
+206
View File
@@ -0,0 +1,206 @@
// THIS FILE WAS IMPORTED FROM AN EXTERNAL SOURCE. DO NOT MODIFY THIS FILE
// MANUALLY.
//
// Original from: https://github.com/google/pprof/blob/e027b50/proto/profile.proto
//
// This file is licensed under the Apache License 2.0
// (https://github.com/google/pprof/blob/e027b5/LICENSE)
// Profile is a common stacktrace profile format.
//
// Measurements represented with this format should follow the
// following conventions:
//
// - Consumers should treat unset optional fields as if they had been
// set with their default value.
//
// - When possible, measurements should be stored in "unsampled" form
// that is most useful to humans. There should be enough
// information present to determine the original sampled values.
//
// - On-disk, the serialized proto must be gzip-compressed.
//
// - The profile is represented as a set of samples, where each sample
// references a sequence of locations, and where each location belongs
// to a mapping.
// - There is a N->1 relationship from sample.location_id entries to
// locations. For every sample.location_id entry there must be a
// unique Location with that id.
// - There is an optional N->1 relationship from locations to
// mappings. For every nonzero Location.mapping_id there must be a
// unique Mapping with that id.
syntax = "proto3";
package perftools.profiles;
option java_package = "com.google.perftools.profiles";
option java_outer_classname = "ProfileProto";
message Profile {
// A description of the samples associated with each Sample.value.
// For a cpu profile this might be:
// [["cpu","nanoseconds"]] or [["wall","seconds"]] or [["syscall","count"]]
// For a heap profile, this might be:
// [["allocations","count"], ["space","bytes"]],
// If one of the values represents the number of events represented
// by the sample, by convention it should be at index 0 and use
// sample_type.unit == "count".
repeated ValueType sample_type = 1;
// The set of samples recorded in this profile.
repeated Sample sample = 2;
// Mapping from address ranges to the image/binary/library mapped
// into that address range. mapping[0] will be the main binary.
repeated Mapping mapping = 3;
// Useful program location
repeated Location location = 4;
// Functions referenced by locations
repeated Function function = 5;
// A common table for strings referenced by various messages.
// string_table[0] must always be "".
repeated string string_table = 6;
// frames with Function.function_name fully matching the following
// regexp will be dropped from the samples, along with their successors.
int64 drop_frames = 7; // Index into string table.
// frames with Function.function_name fully matching the following
// regexp will be kept, even if it matches drop_functions.
int64 keep_frames = 8; // Index into string table.
// The following fields are informational, do not affect
// interpretation of results.
// Time of collection (UTC) represented as nanoseconds past the epoch.
int64 time_nanos = 9;
// Duration of the profile, if a duration makes sense.
int64 duration_nanos = 10;
// The kind of events between sampled ocurrences.
// e.g [ "cpu","cycles" ] or [ "heap","bytes" ]
ValueType period_type = 11;
// The number of events between sampled occurrences.
int64 period = 12;
// Freeform text associated to the profile.
repeated int64 comment = 13; // Indices into string table.
// Index into the string table of the type of the preferred sample
// value. If unset, clients should default to the last sample value.
int64 default_sample_type = 14;
}
// ValueType describes the semantics and measurement units of a value.
message ValueType {
int64 type = 1; // Index into string table.
int64 unit = 2; // Index into string table.
}
// Each Sample records values encountered in some program
// context. The program context is typically a stack trace, perhaps
// augmented with auxiliary information like the thread-id, some
// indicator of a higher level request being handled etc.
message Sample {
// The ids recorded here correspond to a Profile.location.id.
// The leaf is at location_id[0].
repeated uint64 location_id = 1;
// The type and unit of each value is defined by the corresponding
// entry in Profile.sample_type. All samples must have the same
// number of values, the same as the length of Profile.sample_type.
// When aggregating multiple samples into a single sample, the
// result has a list of values that is the elemntwise sum of the
// lists of the originals.
repeated int64 value = 2;
// label includes additional context for this sample. It can include
// things like a thread id, allocation size, etc
repeated Label label = 3;
}
message Label {
int64 key = 1; // Index into string table
// At most one of the following must be present
int64 str = 2; // Index into string table
int64 num = 3;
// Should only be present when num is present.
// Specifies the units of num.
// Use arbitrary string (for example, "requests") as a custom count unit.
// If no unit is specified, consumer may apply heuristic to deduce the unit.
// Consumers may also interpret units like "bytes" and "kilobytes" as memory
// units and units like "seconds" and "nanoseconds" as time units,
// and apply appropriate unit conversions to these.
int64 num_unit = 4; // Index into string table
}
message Mapping {
// Unique nonzero id for the mapping.
uint64 id = 1;
// Address at which the binary (or DLL) is loaded into memory.
uint64 memory_start = 2;
// The limit of the address range occupied by this mapping.
uint64 memory_limit = 3;
// Offset in the binary that corresponds to the first mapped address.
uint64 file_offset = 4;
// The object this entry is loaded from. This can be a filename on
// disk for the main binary and shared libraries, or virtual
// abstractions like "[vdso]".
int64 filename = 5; // Index into string table
// A string that uniquely identifies a particular program version
// with high probability. E.g., for binaries generated by GNU tools,
// it could be the contents of the .note.gnu.build-id field.
int64 build_id = 6; // Index into string table
// The following fields indicate the resolution of symbolic info.
bool has_functions = 7;
bool has_filenames = 8;
bool has_line_numbers = 9;
bool has_inline_frames = 10;
}
// Describes function and line table debug information.
message Location {
// Unique nonzero id for the location. A profile could use
// instruction addresses or any integer sequence as ids.
uint64 id = 1;
// The id of the corresponding profile.Mapping for this location.
// It can be unset if the mapping is unknown or not applicable for
// this profile type.
uint64 mapping_id = 2;
// The instruction address for this location, if available. It
// should be within [Mapping.memory_start...Mapping.memory_limit]
// for the corresponding mapping. A non-leaf address may be in the
// middle of a call instruction. It is up to display tools to find
// the beginning of the instruction if necessary.
uint64 address = 3;
// Multiple line indicates this location has inlined functions,
// where the last entry represents the caller into which the
// preceding entries were inlined.
//
// E.g., if memcpy() is inlined into printf:
// line[0].function_name == "memcpy"
// line[1].function_name == "printf"
repeated Line line = 4;
// Provides an indication that multiple symbols map to this location's
// address, for example due to identical code folding by the linker. In that
// case the line information above represents one of the multiple
// symbols. This field must be recomputed when the symbolization state of the
// profile changes.
bool is_folded = 5;
}
message Line {
// The id of the corresponding profile.Function for this line.
uint64 function_id = 1;
// Line number in source code.
int64 line = 2;
}
message Function {
// Unique nonzero id for the function.
uint64 id = 1;
// Name of the function, in human-readable form if available.
int64 name = 2; // Index into string table
// Name of the function, as identified by the system.
// For instance, it can be a C++ mangled name.
int64 system_name = 3; // Index into string table
// Source file containing the function.
int64 filename = 4; // Index into string table
// Line number in source file.
int64 start_line = 5;
}
+1047
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+88
View File
@@ -0,0 +1,88 @@
import * as pako from 'pako'
export interface ProfileDataSource {
name(): Promise<string>
readAsArrayBuffer(): Promise<ArrayBuffer>
readAsText(): Promise<string>
}
export class TextProfileDataSource implements ProfileDataSource {
constructor(private fileName: string, private contents: string) {}
async name() {
return this.fileName
}
async readAsArrayBuffer() {
// JavaScript strings are UTF-16 encoded, but if this string is
// constructed based on
// TODO(jlfwong): Might want to make this construct an array
// buffer based on the text
return new ArrayBuffer(0)
}
async readAsText() {
return this.contents
}
}
export class MaybeCompressedDataReader implements ProfileDataSource {
private uncompressedData: Promise<ArrayBuffer>
constructor(
private namePromise: Promise<string>,
maybeCompressedDataPromise: Promise<ArrayBuffer>,
) {
this.uncompressedData = maybeCompressedDataPromise.then(async (fileData: ArrayBuffer) => {
try {
const result = pako.inflate(new Uint8Array(fileData)).buffer
return result
} catch (e) {
return fileData
}
})
}
async name(): Promise<string> {
return await this.namePromise
}
async readAsArrayBuffer(): Promise<ArrayBuffer> {
return await this.uncompressedData
}
async readAsText(): Promise<string> {
const buffer = await this.readAsArrayBuffer()
let ret: string = ''
if (typeof TextDecoder !== 'undefined') {
const decoder = new TextDecoder()
return decoder.decode(buffer)
} else {
// JavaScript strings are UTF-16 encoded, but we're reading data
// from disk that we're going to asusme is UTF-8 encoded.
const array = new Uint8Array(buffer)
for (let i = 0; i < array.length; i++) {
ret += String.fromCharCode(array[i])
}
return ret
}
}
static fromFile(file: File): MaybeCompressedDataReader {
const maybeCompressedDataPromise: Promise<ArrayBuffer> = new Promise(resolve => {
const reader = new FileReader()
reader.addEventListener('loadend', () => {
if (!(reader.result instanceof ArrayBuffer)) {
throw new Error('Expected reader.result to be an instance of ArrayBuffer')
}
resolve(reader.result)
})
reader.readAsArrayBuffer(file)
})
return new MaybeCompressedDataReader(Promise.resolve(file.name), maybeCompressedDataPromise)
}
static fromArrayBuffer(name: string, buffer: ArrayBuffer): MaybeCompressedDataReader {
return new MaybeCompressedDataReader(Promise.resolve(name), Promise.resolve(buffer))
}
}
+9
View File
@@ -0,0 +1,9 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importV8HeapAlloc from Chrome', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/69/Heap-20181005T144546.heapprofile')
})
test('importV8HeapAlloc from NodeJS', async () => {
await checkProfileSnapshot('./sample/profiles/node/10.11.0/Heap-20181003T105432.heapprofile')
})
+109
View File
@@ -0,0 +1,109 @@
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
import {getOrInsert} from '../lib/utils'
import {ByteFormatter} from '../lib/value-formatters'
/**
* The V8 Heap Allocation profile is a way to represent heap allocation for each
* javascript function. The format is a simple tree where the weight of each node
* represent the memory allocated by the function and all its callee.
* You can find more information on how to get a profile there :
* https://developers.google.com/web/tools/chrome-devtools/memory-problems/#allocation-profile
* You need to scroll down to "Investigate memory allocation by function"
*
* Note that Node.JS can retrieve this kind of profile via the Inspector protocol.
*/
interface HeapProfileCallFrame {
columnNumber: number
functionName: string
lineNumber: number
scriptId: string
url: string
}
interface HeapProfileNode {
callFrame: HeapProfileCallFrame
selfSize: number
children: HeapProfileNode[]
id: number
parent?: number
totalSize: number
}
interface HeapProfile {
head: HeapProfileNode
}
const callFrameToFrameInfo = new Map<HeapProfileCallFrame, FrameInfo>()
function frameInfoForCallFrame(callFrame: HeapProfileCallFrame) {
return getOrInsert(callFrameToFrameInfo, callFrame, callFrame => {
const name = callFrame.functionName || '(anonymous)'
const file = callFrame.url
const line = callFrame.lineNumber
const col = callFrame.columnNumber
return {
key: `${name}:${file}:${line}:${col}`,
name,
file,
line,
col,
}
})
}
export function importFromChromeHeapProfile(chromeProfile: HeapProfile): Profile {
const nodeById = new Map<number, HeapProfileNode>()
let currentId = 0
const computeId = (node: HeapProfileNode, parent?: HeapProfileNode) => {
node.id = currentId++
nodeById.set(node.id, node)
if (parent) {
node.parent = parent.id
}
node.children.forEach(children => computeId(children, node))
}
computeId(chromeProfile.head)
// Compute the total size
const computeTotalSize = (node: HeapProfileNode): number => {
if (node.children.length === 0) return node.selfSize || 0
const totalChild = node.children.reduce((total: number, children) => {
total += computeTotalSize(children)
return total
}, node.selfSize)
node.totalSize = totalChild
return totalChild
}
const total = computeTotalSize(chromeProfile.head)
// Compute all stacks by taking each last node and going upward
const stacks: HeapProfileNode[][] = []
for (let currentNode of nodeById.values()) {
let stack: HeapProfileNode[] = []
stack.push(currentNode)
// While we found a parent
while (true) {
if (currentNode.parent === undefined) break
const parent = nodeById.get(currentNode.parent)
if (parent === undefined) break
// Push the parent at the beginning of the stack
stack.unshift(parent)
currentNode = parent
}
stacks.push(stack)
}
const profile = new StackListProfileBuilder(total)
for (let stack of stacks) {
const lastFrame = stack[stack.length - 1]
profile.appendSampleWithWeight(
stack.map(frame => frameInfoForCallFrame(frame.callFrame)),
lastFrame.selfSize,
)
}
profile.setValueFormatter(new ByteFormatter())
return profile.build()
}
+5
View File
@@ -159,4 +159,9 @@ export class LRUCache<K, V> {
this.map.delete(key)
return [key, value]
}
clear() {
this.list = new List<K>()
this.map = new Map<K, LRUCacheNode<K, V>>()
}
}
+8 -8
View File
@@ -521,10 +521,9 @@ export class CallTreeProfileBuilder extends Profile {
private framesInStack = new Map<Frame, number>()
private stack: Frame[] = []
private lastValue: number | null = null
private lastValue: number = 0
private addWeightsToFrames(value: number) {
if (this.lastValue == null) this.lastValue = value
const delta = value - this.lastValue!
const delta = value - this.lastValue
for (let frame of this.framesInStack.keys()) {
frame.addToTotalWeight(delta)
}
@@ -534,7 +533,7 @@ export class CallTreeProfileBuilder extends Profile {
}
}
private addWeightsToNodes(value: number, stack: CallTreeNode[]) {
const delta = value - this.lastValue!
const delta = value - this.lastValue
for (let node of stack) {
node.addToTotalWeight(delta)
}
@@ -552,14 +551,15 @@ export class CallTreeProfileBuilder extends Profile {
if (prevTop) {
if (useAppendOrder) {
const delta = value - this.lastValue!
const delta = value - this.lastValue
if (delta > 0) {
this.samples.push(prevTop)
this.weights.push(value - this.lastValue!)
this.weights.push(value - this.lastValue)
} else if (delta < 0) {
throw new Error(
`Samples must be provided in increasing order of cumulative value. Last sample was ${this
.lastValue!}, this sample was ${value}`,
`Samples must be provided in increasing order of cumulative value. Last sample was ${
this.lastValue
}, this sample was ${value}`,
)
}
}
+5 -4
View File
@@ -1,8 +1,8 @@
import * as fs from 'fs'
import * as path from 'path'
import {Profile, CallTreeNode, Frame} from './profile'
import {importProfileGroup} from '../import'
import {exportProfileGroup, importSpeedscopeProfiles} from './file-format'
import {importProfilesFromArrayBuffer} from '../import'
interface DumpedProfile {
name: string
@@ -47,9 +47,10 @@ export function dumpProfile(profile: Profile): any {
}
export async function checkProfileSnapshot(filepath: string) {
const input = fs.readFileSync(filepath, 'utf8')
const buffer = fs.readFileSync(filepath)
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
const profileGroup = await importProfileGroup(path.basename(filepath), input)
const profileGroup = await importProfilesFromArrayBuffer(path.basename(filepath), arrayBuffer)
if (profileGroup) {
expect(profileGroup.name).toMatchSnapshot('profileGroup.name')
expect(profileGroup.indexToView).toMatchSnapshot('indexToView')
@@ -62,7 +63,7 @@ export async function checkProfileSnapshot(filepath: string) {
return
}
const profilesWithoutFilename = await importProfileGroup('unknown', input)
const profilesWithoutFilename = await importProfilesFromArrayBuffer('unknown', arrayBuffer)
if (profilesWithoutFilename) {
expect(profilesWithoutFilename.profiles.length).toEqual(profileGroup.profiles.length)
profilesWithoutFilename.name = profileGroup.name
+19
View File
@@ -13,8 +13,11 @@ import {
memoizeByReference,
memoizeByShallowEquality,
objectsHaveShallowEquality,
decodeBase64,
} from './utils'
import * as jsc from 'jsverify'
test('sortBy', () => {
const ls = ['a3', 'b2', 'c1', 'd4']
sortBy(ls, k => k.charCodeAt(1))
@@ -173,3 +176,19 @@ test('objectsHaveShallowEquality', () => {
expect(objectsHaveShallowEquality([1], [1, 2])).toBe(false)
expect(objectsHaveShallowEquality([1, 2], [1])).toBe(false)
})
test('decodeBase64', () => {
jsc.assertForall(jsc.array(jsc.uint8), byteArray => {
let binaryString = ''
for (let byte of byteArray) {
binaryString += String.fromCharCode(byte)
}
const b64string = btoa(binaryString)
const decoded = decodeBase64(b64string)
expect(Uint8Array.from(byteArray)).toEqual(decoded)
// If the above expect(...) assertion fails, we won't reach here.
return true
})
})
+128
View File
@@ -167,3 +167,131 @@ export function memoizeByReference<T, U>(cb: (t: T) => U): (t: T) => U {
}
}
}
export function lazyStatic<T>(cb: () => T): () => T {
let last: {result: T} | null = null
return () => {
if (last == null) {
last = {result: cb()}
}
return last.result
}
}
const base64lookupTable = lazyStatic((): Map<string, number> => {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
const ret = new Map<string, number>()
for (let i = 0; i < alphabet.length; i++) {
ret.set(alphabet.charAt(i), i)
}
ret.set('=', -1)
return ret
})
// NOTE: There are probably simpler solutions to this problem, but I have this written already, so
// until we run into problems with this, let's just use this.
//
// See: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem#The_Unicode_Problem
export function decodeBase64(encoded: string): Uint8Array {
// Reference: https://www.rfc-editor.org/rfc/rfc4648.txt
const lookupTable = base64lookupTable()
// 3 byte groups are represented as sequneces of 4 characters.
//
// "The encoding process represents 24-bit groups of input bits as output
// strings of 4 encoded characters."
//
// "Special processing is performed if fewer than 24 bits are available
// at the end of the data being encoded. A full encoding quantum is
// always completed at the end of a quantity. When fewer than 24 input
// bits are available in an input group bits with value zero are added
// (on the right) to form an integral number of 6-bit groups."
if (encoded.length % 4 !== 0) {
throw new Error(
`Invalid length for base64 encoded string. Expected length % 4 = 0, got length = ${
encoded.length
}`,
)
}
const quartetCount = encoded.length / 4
let byteCount: number
// Special processing is performed if fewer than 24 bits are available
// at the end of the data being encoded. A full encoding quantum is
// always completed at the end of a quantity. When fewer than 24 input
// bits are available in an input group, bits with value zero are added
// (on the right) to form an integral number of 6-bit groups. Padding
// at the end of the data is performed using the '=' character. Since
// all base 64 input is an integral number of octets, only the following
// cases can arise:
//
// (1) The final quantum of encoding input is an integral multiple of 24
// bits; here, the final unit of encoded output will be an integral
// multiple of 4 characters with no "=" padding.
//
// (2) The final quantum of encoding input is exactly 8 bits; here, the
// final unit of encoded output will be two characters followed by
// two "=" padding characters.
//
// (3) The final quantum of encoding input is exactly 16 bits; here, the
// final unit of encoded output will be three characters followed by
// one "=" padding character.
if (encoded.length >= 4) {
if (encoded.charAt(encoded.length - 1) === '=') {
if (encoded.charAt(encoded.length - 2) === '=') {
// Case (2)
byteCount = quartetCount * 3 - 2
} else {
// Case (3)
byteCount = quartetCount * 3 - 1
}
} else {
// Case (1)
byteCount = quartetCount * 3
}
} else {
// Case (1)
byteCount = quartetCount * 3
}
const bytes = new Uint8Array(byteCount)
let offset = 0
for (let i = 0; i < quartetCount; i++) {
const enc1 = encoded.charAt(i * 4 + 0)
const enc2 = encoded.charAt(i * 4 + 1)
const enc3 = encoded.charAt(i * 4 + 2)
const enc4 = encoded.charAt(i * 4 + 3)
const sextet1 = lookupTable.get(enc1)
const sextet2 = lookupTable.get(enc2)
const sextet3 = lookupTable.get(enc3)
const sextet4 = lookupTable.get(enc4)
if (sextet1 == null || sextet2 == null || sextet3 == null || sextet4 == null) {
throw new Error(
`Invalid quartet at indices ${i * 4} .. ${i * 4 + 3}: ${encoded.substring(
i * 4,
i * 4 + 3,
)}`,
)
}
bytes[offset++] = (sextet1 << 2) | (sextet2 >> 4)
if (enc3 !== '=') {
bytes[offset++] = ((sextet2 & 15) << 4) | (sextet3 >> 2)
}
if (enc4 !== '=') {
bytes[offset++] = ((sextet3 & 7) << 6) | sextet4
}
}
if (offset !== byteCount) {
throw new Error(`Expected to decode ${byteCount} bytes, but only decoded ${offset})`)
}
return bytes
}
+4 -2
View File
@@ -7,7 +7,8 @@ describe('TimeFormatter', () => {
expect(f.format(0.04)).toEqual('40.00µs')
expect(f.format(3)).toEqual('3.00ms')
expect(f.format(2070)).toEqual('2.07s')
expect(f.format(1203123)).toEqual('20.05min')
expect(f.format(150000)).toEqual('2:30')
expect(f.format(1203123)).toEqual('20:03')
})
test('input units seconds', () => {
@@ -15,7 +16,8 @@ describe('TimeFormatter', () => {
expect(f.format(0.00004)).toEqual('40.00µs')
expect(f.format(0.003)).toEqual('3.00ms')
expect(f.format(2.07)).toEqual('2.07s')
expect(f.format(1203.123)).toEqual('20.05min')
expect(f.format(150)).toEqual('2:30')
expect(f.format(1203.123)).toEqual('20:03')
})
})
+6 -1
View File
@@ -1,4 +1,5 @@
import {FileFormat} from './file-format-spec'
import {zeroPad} from './utils'
export interface ValueFormatter {
unit: FileFormat.ValueUnit
@@ -25,7 +26,11 @@ export class TimeFormatter implements ValueFormatter {
formatUnsigned(v: number) {
const s = v * this.multiplier
if (s / 60 >= 1) return `${(s / 60).toFixed(2)}min`
if (s / 60 >= 1) {
const minutes = Math.floor(s / 60)
const seconds = Math.floor(s - minutes * 60).toString()
return `${minutes}:${zeroPad(seconds, 2)}`
}
if (s / 1 >= 1) return `${s.toFixed(2)}s`
if (s / 1e-3 >= 1) return `${(s / 1e-3).toFixed(2)}ms`
if (s / 1e-6 >= 1) return `${(s / 1e-6).toFixed(2)}µs`
+5 -1
View File
@@ -33,7 +33,11 @@ export const getCanvasContext = memoizeByReference((canvas: HTMLCanvasElement) =
})
export const getRowAtlas = memoizeByReference((canvasContext: CanvasContext) => {
return new RowAtlas<FlamechartRowAtlasKey>(canvasContext)
return new RowAtlas<FlamechartRowAtlasKey>(
canvasContext.gl,
canvasContext.rectangleBatchRenderer,
canvasContext.textureRenderer,
)
})
export const getProfileWithRecursionFlattened = memoizeByReference((profile: Profile) =>
+1 -1
View File
@@ -1,6 +1,6 @@
import * as fs from 'fs'
import {Store, AnyAction} from '../../node_modules/redux'
import {Store, AnyAction} from 'redux'
import {ApplicationState, createApplicationStore} from '.'
import {importSpeedscopeProfiles} from '../lib/file-format'
-589
View File
@@ -1,589 +0,0 @@
declare module 'regl' {
interface InitializationOptions {
/** A reference to a WebGL rendering context. (Default created from canvas) */
gl?: WebGLRenderingContext
/** A reference to an HTML canvas element. (Default created and appending to container) */
canvas?: HTMLCanvasElement | string
/** A container element which regl inserts a canvas into. (Default document.body) */
container?: HTMLElement | string
/** The context creation attributes passed to the WebGL context constructor. See below for defaults. */
attributes?: {
/** Boolean that indicates if the canvas contains an alpha buffer. */
alpha?: boolean
/** Boolean that indicates that the drawing buffer has a depth buffer of at least 16 bits. */
depth?: boolean
/** Boolean that indicates that the drawing buffer has a stencil buffer of at least 8 bits. */
stencil?: boolean
/** Boolean that indicates whether or not to perform anti-aliasing. */
antialias?: boolean
/** Boolean that indicates that the page compositor will assume the drawing buffer contains colors with pre-multiplied alpha. */
premultipliedAlpha?: boolean
/** If the value is true the buffers will not be cleared and will preserve their values until cleared or overwritten by the author. */
preserveDrawingBuffer?: boolean
/** Boolean that indicates if a context will be created if the system performance is low. */
failIfMajorPerformanceCavet?: boolean
}
/** A multiplier which is used to scale the canvas size relative to the container. (Default window.devicePixelRatio) */
pixelRatio?: number
/** A list of extensions that must be supported by WebGL context. Default [] */
extensions?: string[] | string
/** A list of extensions which are loaded opportunistically. Default [] */
optionalExtensions?: string[] | string
/** If set, turns on profiling for all commands by default. (Default false) */
profile?: boolean
/**
* An optional callback which accepts a pair of arguments, (err, regl) that
* is called after the application loads. If not specified, context creation
* errors throw.
*/
onDone?: (err: Error, regl: regl.Instance) => void
}
function regl(options: InitializationOptions): regl.Instance
/** Create fullscreen canvas */
function regl(): regl.Instance
/** Build from an existing canvas */
function regl(canvas: HTMLCanvasElement): regl.Instance
/** Build from an existing div container */
function regl(container: HTMLElement): regl.Instance
/** Build from an existing WebGL context */
function regl(gl: WebGLRenderingContext): regl.Instance
namespace regl {
export type vec2 = [number, number]
export type vec3 = [number, number, number]
export type vec4 = [number, number, number, number]
export type mat3 = [number, number, number, number, number, number, number, number, number]
export type mat4 = [
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number
]
type GlslPrimitive = number | vec2 | vec3 | vec4 | mat3 | mat4
interface Tick {
cancel(): void
}
interface Instance {
<P>(params: CommandOptions<P>): Command<P>
clear(args: {
color?: [number, number, number, number]
depth?: number
stencil?: number
}): void
// TODO(jlfwong): read()
buffer(args: BufferArgs): Buffer
texture(width: number, height: number): Texture
texture(args: TextureArgs): Texture
framebuffer(args: FramebufferOptions): Framebuffer
renderbuffer(args: RenderBufferOptions): RenderBuffer
limits: {
colorBits: [number, number, number, number]
depthBits: number
stencilBits: number
subpixelBits: number
extensions: string[]
maxAnistropic: number
maxDrawbuffers: number
maxColorAttachments: number
pointSizeDims: number
lineWidthDims: number
maxViewportDims: number
maxCombinedTextureUnits: number
maxCubeMapSize: number
maxTextureUnits: number
maxTextureSize: number
maxAttributes: number
maxVertexUniforms: number
maxVertexTextureUnits: number
maxFragmentUniforms: number
glsl: string
renderer: string
vendor: string
version: string
}
stats: {
bufferCount: number
elementsCount: number
framebufferCount: number
shaderCount: number
textureCount: number
cubeCount: number
renderbufferCount: number
getTotalTextureSize(): number
getTotalBufferSize(): number
getTotalRenderbufferSize(): number
getMaxUniformsCount(): number
maxTextureUnits(): number
}
destroy(): void
frame(callback: (context: Context) => void): Tick
}
type TypedArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
type DrawMode =
| 'points'
| 'lines'
| 'line strip'
| 'line loop'
| 'triangles'
| 'triangle strip'
| 'triangle fan'
interface Context {
tick: number
time: number
viewportWidth: number
viewportHeight: number
framebufferWidth: number
framebufferHeight: number
drawingBufferWidth: number
drawingBufferHeight: number
pixelRatio: number
[key: string]: any
}
interface BufferOptions {
data?: TypedArray | GlslPrimitive[]
/** If data is null or not present reserves space for the buffer */
length?: number
/** Sets array buffer usage hint */
usage?: 'static' | 'dynamic' | 'stream'
}
type BufferArgs = number | number[] | vec2[] | vec3[] | mat3[] | TypedArray | BufferOptions
interface Buffer {
(args: BufferArgs): void
stats: {
size: number
}
destroy(): void
}
interface ElementsOptions {
data?: TypedArray
usage?: 'static' | 'dynamic' | 'stream'
length?: number
primitive?: GlslPrimitive
count?: number
}
type ElementsArgs = vec3[] | ElementsOptions
interface Elements {
(args: ElementsArgs): void
destroy(): void
}
type MagFilter = 'nearest' | 'linear'
type MinFilter =
| 'nearest'
| 'linear'
| 'mipmap'
| 'linear mipmap linear'
| 'nearest mipmap linear'
| 'nearest mipmap nearest'
type WrapMode = 'repeat' | 'clamp' | 'mirror'
type TextureFormat =
| 'alpha'
| 'luminance'
| 'luminance alpha'
| 'rgb'
| 'rgba'
| 'rgba4'
| 'rgb5 a1'
| 'rgb565'
| 'srgb'
| 'srgba'
| 'depth'
| 'depth stencil'
| 'rgb s3tc dxt1'
| 'rgb s3tc dxt5'
| 'rgb atc'
| 'rgba atc explicit alpha'
| 'rgba atc interpolated alpha'
| 'rgb pvrtc 4bppv1'
| 'rgb pvrtc 2bppv1'
| 'rgba pvrtc 4bppv1'
| 'rgba pvrtc 2bppv1'
| 'rgb etc1'
type TextureType = 'uint8' | 'uint16' | 'float' | 'float32' | 'half float' | 'float16'
type ColorSpace = 'none' | 'browser'
type MipmapHint = "don't care" | 'dont care' | 'nice' | 'fast'
type TextureData =
| number[]
| number[][]
| TypedArray
| HTMLImageElement
| HTMLVideoElement
| HTMLCanvasElement
| CanvasRenderingContext2D
interface TextureOptions {
width?: number
height?: number
shape?: [number, number] | [number, number, number]
radius?: number
/** Sets magnification filter */
mag?: MagFilter
/** Sets minification filter */
min?: MinFilter
/** Sets wrap mode on S axis */
wrapS?: WrapMode
/** Sets wrap mode on T axis */
wrapT?: WrapMode
/** Sets number of anisotropic samples, requires EXT_texture_filter_anisotropic */
aniso?: number
format?: TextureFormat
type?: TextureType
mipmap?: MipmapHint | boolean
/** Flips textures vertically when uploading */
flipY?: boolean
/** Sets unpack alignment per pixel */
alignment?: number
/** Premultiply alpha when unpacking */
premultiplyAlpha?: boolean
colorSpace?: ColorSpace
copy?: boolean
data?: TextureData
}
type TextureArgs = TextureData | TextureOptions
interface Texture {
width: number
height: number
(args: TextureArgs): void
resize(width: number, height: number): void
destroy(): void
stats: {
size: number
}
}
// TODO(jlfwong): Cube maps
// TODO(jlfwong): Cubic frame buffers
interface RenderBufferOptions {
format?:
| 'rgba4'
| 'rgb565'
| 'rgb5 a1'
| 'depth'
| 'stencil'
| 'depth stencil'
| 'srgba'
| 'rgba16f'
| 'rgb16f'
| 'rgba32f'
width?: number
height?: number
shape?: [number, number]
radius?: number
}
interface RenderBuffer {
(options: RenderBufferOptions): void
resize(width: number, height: number): void
destroy(): void
stats: {
size: number
}
}
interface FramebufferOptions {
width?: number
height?: number
shape?: [number, number]
color?: RenderBuffer[] | Texture[]
depth?: boolean | RenderBuffer | Texture
stencil?: boolean | RenderBuffer | Texture
depthStencil?: boolean | RenderBuffer | Texture
colorFormat?:
| 'rgba'
| 'rgba4'
| 'rgb565'
| 'rgb5 a1'
| 'rgb16f'
| 'rgba16f'
| 'rgba32f'
| 'srgba'
colorType?: 'uint8' | 'half float' | 'float'
}
interface Framebuffer {
width: number
height: number
color?: RenderBuffer[]
depth?: RenderBuffer
(options: FramebufferOptions): void
use(cb: (context: Context) => void): void
resize(width: number, height: number): void
destroy(): void
}
type Uniform = number | vec2 | vec3 | mat3 | Texture
interface AttributeOptions {
buffer?: Buffer | BufferArgs
offset?: number
stride?: number
normalized?: boolean
size?: number
divisor?: number
}
type Attribute =
| AttributeOptions
| Buffer
| BufferArgs
| {constant: number | vec2 | vec3 | vec4 | mat3 | mat4}
interface Computed<P, T> {
(context: Context, props: P, batchId: number): T
}
type MaybeComputed<P, T> = Computed<P, T> | T
type DepthFunction =
| 'never'
| 'always'
| '<'
| 'less'
| '<='
| 'lequal'
| '>'
| 'greater'
| '>='
| 'gequal'
| '='
| 'equal'
| '!='
| 'notequal'
type BlendFunction =
| 0
| 'zero'
| 1
| 'one'
| 'src color'
| 'one minus src color'
| 'src alpha'
| 'one minus src alpha'
| 'dst color'
| 'one minus dst color'
| 'dst alpha'
| 'one minus dst alpha'
| 'constant color'
| 'one minus constant color'
| 'one minus constant alpha'
| 'src alpha saturate'
type BlendEquation = 'add' | 'subtract' | 'reverse subtract' | 'min' | 'max'
type StencilFunction = DepthFunction
type StencilOp =
| 'zero'
| 'keep'
| 'replace'
| 'invert'
| 'increment'
| 'decrement'
| 'increment wrap'
| 'decrement wrap'
interface CommandOptions<P> {
/** Source code of vertex shader */
vert?: string
/** Source code of fragment shader */
frag?: string
context?: {[contextName: string]: MaybeComputed<P, any>}
uniforms?: {[uniformName: string]: MaybeComputed<P, Uniform>}
attributes?: {[attributeName: string]: MaybeComputed<P, Attribute>}
primitive?: DrawMode
/** Number of vertices to draw */
count?: MaybeComputed<P, number>
/** Offset of primitives to draw */
offset?: MaybeComputed<P, number>
/** Number of instances to render */
instances?: MaybeComputed<P, number>
/** Element array buffer */
elements?: MaybeComputed<P, Elements | ElementsArgs>
framebuffer?: MaybeComputed<P, Framebuffer>
profile?: MaybeComputed<P, boolean>
depth?: MaybeComputed<
P,
{
enable?: boolean
mask?: boolean
func?: DepthFunction
range?: [number, number]
}
>
blend?: MaybeComputed<
P,
{
enable?: boolean
func?:
| {
src: BlendFunction
dst: BlendFunction
}
| {
srcRGB: BlendFunction
srcAlpha: BlendFunction
dstRGB: BlendFunction
dstAlpha: BlendFunction
}
equation?:
| BlendEquation
| {
rgb: BlendEquation
alpha: BlendEquation
}
color?: vec4
}
>
stencil?: MaybeComputed<
P,
{
enable?: boolean
mask?: number
func?: StencilFunction
opFront?: {fail: StencilOp; zfail: StencilOp; pass: StencilOp}
opBack?: {fail: StencilOp; zfail: StencilOp; pass: StencilOp}
}
>
polygonOffset?: MaybeComputed<
P,
{
enable?: boolean
offset?: {
factor: number
units: number
}
}
>
cull?: MaybeComputed<
P,
{
enable?: boolean
face?: 'front' | 'back'
}
>
frontFace?: MaybeComputed<P, 'cw' | 'ccw'>
dither?: MaybeComputed<P, boolean>
lineWidth?: MaybeComputed<P, number>
colorMask?: MaybeComputed<P, [boolean, boolean, boolean, boolean]>
sample?: MaybeComputed<
P,
{
enable?: boolean
alpha?: boolean
coverage?: {
value: number
invert: boolean
}
}
>
scissor?: MaybeComputed<
P,
{
enable?: boolean
box?: {
x: number
y: number
width: number
height: number
}
}
>
viewport?: MaybeComputed<
P,
{
x: number
y: number
width: number
height: number
}
>
}
function prop<P>(name: keyof P): (context: Context, props: P, batchId: number) => P[keyof P]
function context<P>(
name: keyof Context,
): (context: Context, props: P, batchId: number) => Context[keyof Context]
interface Command<P> {
/** One shot rendering */
(): void
(p: P): void
/** Render a batch */
(ps: P[]): void
/** Scoped commands */
(cb: (context: Context) => void): void
(p: P, cb: (context: Context) => void): void
stats: {
gpuTime: number
cpuTime: number
hitCount: number
}
}
}
export default regl
}
+17 -1
View File
@@ -1,8 +1,9 @@
import {createContainer, Dispatch, bindActionCreator, ActionCreator} from '../lib/typed-redux'
import {Application, ActiveProfileState} from './application'
import {ApplicationState} from '../store'
import {getProfileToView} from '../store/getters'
import {getProfileToView, getCanvasContext} from '../store/getters'
import {actions} from '../store/actions'
import {Graphics} from '../gl/graphics'
export const ApplicationContainer = createContainer(
Application,
@@ -26,6 +27,8 @@ export const ApplicationContainer = createContainer(
return bindActionCreator(dispatch, actionCreator)
}
// TODO(jlfwong): Cache this and resizeCanvas below to prevent re-renders
// due to changing props.
const setters = {
setGLCanvas: wrapActionCreator(actions.setGLCanvas),
setLoading: wrapActionCreator(actions.setLoading),
@@ -40,6 +43,19 @@ export const ApplicationContainer = createContainer(
return {
activeProfileState,
dispatch,
canvasContext: state.glCanvas ? getCanvasContext(state.glCanvas) : null,
resizeCanvas: (
widthInPixels: number,
heightInPixels: number,
widthInAppUnits: number,
heightInAppUnits: number,
) => {
if (state.glCanvas) {
const gl = getCanvasContext(state.glCanvas).gl
gl.resize(widthInPixels, heightInPixels, widthInAppUnits, heightInAppUnits)
gl.clear(new Graphics.Color(1, 1, 1, 1))
}
},
...setters,
...state,
}
+94 -37
View File
@@ -12,12 +12,36 @@ import {StatelessComponent} from '../lib/typed-redux'
import {LeftHeavyFlamechartView, ChronoFlamechartView} from './flamechart-view-container'
import {SandwichViewState} from '../store/sandwich-view-state'
import {FlamechartViewState} from '../store/flamechart-view-state'
import {CanvasContext} from '../gl/canvas-context'
import {Graphics} from '../gl/graphics'
const importModule = import('../import')
// Force eager loading of the module
importModule.then(() => {})
async function importProfiles(fileName: string, contents: string): Promise<ProfileGroup | null> {
return (await importModule).importProfileGroup(fileName, contents)
async function importProfilesFromText(
fileName: string,
contents: string,
): Promise<ProfileGroup | null> {
return (await importModule).importProfileGroupFromText(fileName, contents)
}
async function importProfilesFromBase64(
fileName: string,
contents: string,
): Promise<ProfileGroup | null> {
return (await importModule).importProfileGroupFromBase64(fileName, contents)
}
async function importProfilesFromArrayBuffer(
fileName: string,
contents: ArrayBuffer,
): Promise<ProfileGroup | null> {
return (await importModule).importProfilesFromArrayBuffer(fileName, contents)
}
async function importProfilesFromFile(file: File): Promise<ProfileGroup | null> {
return (await importModule).importProfilesFromFile(file)
}
async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntry) {
return (await importModule).importFromFileSystemDirectoryEntry(entry)
@@ -168,6 +192,7 @@ export class Toolbar extends StatelessComponent<ToolbarProps> {
}
interface GLCanvasProps {
canvasContext: CanvasContext | null
setGLCanvas: (canvas: HTMLCanvasElement | null) => void
}
export class GLCanvas extends Component<GLCanvasProps, void> {
@@ -183,37 +208,66 @@ export class GLCanvas extends Component<GLCanvasProps, void> {
this.props.setGLCanvas(this.canvas)
}
private maybeResize() {
if (!this.canvas) return
let {width, height} = this.canvas.getBoundingClientRect()
width = Math.floor(width) * window.devicePixelRatio
height = Math.floor(height) * window.devicePixelRatio
private container: HTMLElement | null = null
private containerRef = (container?: Element) => {
if (container instanceof HTMLElement) {
this.container = container
} else {
this.container = null
}
}
// Still initializing: don't resize yet
if (width < 4 || height < 4) return
const oldWidth = this.canvas.width
const oldHeight = this.canvas.height
private maybeResize = () => {
if (!this.container) return
if (!this.props.canvasContext) return
// Already at the right size
if (width === oldWidth && height === oldHeight) return
let {width, height} = this.container.getBoundingClientRect()
this.canvas.width = width
this.canvas.height = height
const widthInAppUnits = width
const heightInAppUnits = height
const widthInPixels = width * window.devicePixelRatio
const heightInPixels = height * window.devicePixelRatio
this.props.canvasContext.gl.resize(
widthInPixels,
heightInPixels,
widthInAppUnits,
heightInAppUnits,
)
this.props.canvasContext.gl.clear(new Graphics.Color(1, 1, 1, 1))
}
onWindowResize = () => {
this.maybeResize()
window.addEventListener('resize', this.onWindowResize)
if (this.props.canvasContext) {
this.props.canvasContext.requestFrame()
}
}
componentWillReceiveProps(nextProps: GLCanvasProps) {
if (this.props.canvasContext !== nextProps.canvasContext) {
if (this.props.canvasContext) {
this.props.canvasContext.removeBeforeFrameHandler(this.maybeResize)
}
if (nextProps.canvasContext) {
nextProps.canvasContext.addBeforeFrameHandler(this.maybeResize)
nextProps.canvasContext.requestFrame()
}
}
}
componentDidMount() {
window.addEventListener('resize', this.onWindowResize)
requestAnimationFrame(() => this.maybeResize())
}
componentWillUnmount() {
if (this.props.canvasContext) {
this.props.canvasContext.removeBeforeFrameHandler(this.maybeResize)
}
window.removeEventListener('resize', this.onWindowResize)
}
render() {
return <canvas className={css(style.glCanvasView)} ref={this.ref} width={1} height={1} />
return (
<div ref={this.containerRef} className={css(style.glCanvasView)}>
<canvas ref={this.ref} width={1} height={1} />
</div>
)
}
}
@@ -235,6 +289,7 @@ export type ApplicationProps = ApplicationState & {
setFlattenRecursion: (flattenRecursion: boolean) => void
setProfileIndexToView: (profileIndex: number) => void
activeProfileState: ActiveProfileState | null
canvasContext: CanvasContext | null
}
export class Application extends StatelessComponent<ApplicationProps> {
@@ -287,16 +342,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
loadFromFile(file: File) {
this.loadProfile(async () => {
const reader = new FileReader()
const loadPromise = new Promise(resolve => reader.addEventListener('loadend', resolve))
reader.readAsText(file)
await loadPromise
if (typeof reader.result !== 'string') {
throw new Error('Expected ArrayBuffer')
}
const profiles = await importProfiles(file.name, reader.result)
const profiles = await importProfilesFromFile(file)
if (profiles) {
for (let profile of profiles.profiles) {
if (!profile.getName()) {
@@ -311,7 +357,19 @@ export class Application extends StatelessComponent<ApplicationProps> {
// a symbol map. If that's the case, we want to parse it, and apply the symbol
// mapping to the already loaded profile. This can be use to take an opaque
// profile and make it readable.
const map = importEmscriptenSymbolMap(reader.result)
const reader = new FileReader()
const fileContentsPromise = new Promise<string>(resolve => {
reader.addEventListener('loadend', () => {
if (typeof reader.result !== 'string') {
throw new Error('Expected reader.result to be a string')
}
resolve(reader.result)
})
})
reader.readAsText(file)
const fileContents = await fileContentsPromise
const map = importEmscriptenSymbolMap(fileContents)
if (map) {
const {profile, index} = this.props.activeProfileState
console.log('Importing as emscripten symbol map')
@@ -332,7 +390,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
this.loadProfile(async () => {
const filename = 'perf-vertx-stacks-01-collapsed-all.txt'
const data = await fetch(exampleProfileURL).then(resp => resp.text())
return await importProfiles(filename, data)
return await importProfilesFromText(filename, data)
})
}
@@ -430,7 +488,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
const pasted = (ev as ClipboardEvent).clipboardData.getData('text')
this.loadProfile(async () => {
return await importProfiles('From Clipboard', pasted)
return await importProfilesFromText('From Clipboard', pasted)
})
}
@@ -456,12 +514,12 @@ export class Application extends StatelessComponent<ApplicationProps> {
return
}
this.loadProfile(async () => {
const response = await fetch(this.props.hashParams.profileURL!)
const response: Response = await fetch(this.props.hashParams.profileURL!)
let filename = new URL(this.props.hashParams.profileURL!).pathname
if (filename.includes('/')) {
filename = filename.slice(filename.lastIndexOf('/') + 1)
}
return await importProfiles(filename, await response.text())
return await importProfilesFromArrayBuffer(filename, await response.arrayBuffer())
})
} else if (this.props.hashParams.localProfilePath) {
// There isn't good cross-browser support for XHR of local files, even from
@@ -469,8 +527,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
// as a JavaScript file which will invoke a global function.
;(window as any)['speedscope'] = {
loadFileFromBase64: (filename: string, base64source: string) => {
const source = atob(base64source)
this.loadProfile(() => importProfiles(filename, source))
this.loadProfile(() => importProfilesFromBase64(filename, base64source))
},
}
@@ -608,7 +665,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
onDragLeave={this.onDragLeave}
className={css(style.root, this.props.dragActive && style.dragTargetRoot)}
>
<GLCanvas setGLCanvas={this.props.setGLCanvas} />
<GLCanvas setGLCanvas={this.props.setGLCanvas} canvasContext={this.props.canvasContext} />
<Toolbar
saveFile={this.saveFile}
browseForFile={this.browseForFile}
+4 -39
View File
@@ -6,7 +6,6 @@ import {FlamechartRenderer} from '../gl/flamechart-renderer'
import {style} from './flamechart-style'
import {FontFamily, FontSize, Colors, Sizes, commonStyle} from './style'
import {CanvasContext} from '../gl/canvas-context'
import {TextureCachedRenderer} from '../gl/texture-cached-renderer'
import {cachedMeasureTextWidth} from '../lib/text-utils'
interface FlamechartMinimapViewProps {
@@ -71,45 +70,13 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
return AffineTransform.withTranslation(new Vec2(-bounds.left, -bounds.top))
}
private cachedRenderer: TextureCachedRenderer<{
physicalSize: Vec2
}> | null = null
private renderRects() {
if (!this.container) return
// Hasn't resized yet -- no point in rendering yet
if (this.physicalViewSize().x < 2) return
if (!this.cachedRenderer) {
this.cachedRenderer = this.props.canvasContext.createTextureCachedRenderer({
shouldUpdate(oldProps, newProps) {
if (!oldProps.physicalSize.equals(newProps.physicalSize)) {
return true
}
return false
},
render: props => {
this.props.flamechartRenderer.render({
physicalSpaceDstRect: new Rect(
this.minimapOrigin(),
this.physicalViewSize().minus(this.minimapOrigin()),
),
configSpaceSrcRect: new Rect(new Vec2(0, 0), this.configSpaceSize()),
renderOutlines: false,
})
},
})
}
this.props.canvasContext.renderInto(this.container, context => {
// TODO(jlfwong): Switch back to the texture cached renderer once I figure out
// how to resize a framebuffer while another framebuffer is active. It seems
// to crash regl. I should submit a reduced repro case and hopefully get it fixed?
/*
this.cachedRenderer!.render({
physicalSize: this.physicalViewSize()
})
*/
this.props.canvasContext.renderBehind(this.container, () => {
this.props.flamechartRenderer.render({
configSpaceSrcRect: new Rect(new Vec2(0, 0), this.configSpaceSize()),
physicalSpaceDstRect: new Rect(
@@ -118,7 +85,8 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
),
renderOutlines: false,
})
this.props.canvasContext.drawViewportRectangle({
this.props.canvasContext.viewportRectangleRenderer.render({
configSpaceViewportRect: this.props.configSpaceViewportRect,
configSpaceToPhysicalViewSpace: this.configSpaceToPhysicalViewSpace(),
})
@@ -190,9 +158,6 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
componentWillReceiveProps(nextProps: FlamechartMinimapViewProps) {
if (this.props.flamechart !== nextProps.flamechart) {
if (this.cachedRenderer) {
this.cachedRenderer.setDirty()
}
this.renderCanvas()
} else if (this.props.configSpaceViewportRect != nextProps.configSpaceViewportRect) {
this.renderCanvas()
@@ -409,7 +374,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
}
}
private onMouseLeave = (ev: MouseEvent) => {
private onMouseLeave = () => {
if (this.draggingMode == null) {
document.body.style.cursor = 'default'
}
+27 -20
View File
@@ -227,22 +227,24 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
)
}
const trimmedText = trimTextMid(
ctx,
frame.node.frame.name,
physicalLabelBounds.width() - 2 * LABEL_PADDING_PX,
)
if (physicalLabelBounds.width() > minWidthToRender) {
const trimmedText = trimTextMid(
ctx,
frame.node.frame.name,
physicalLabelBounds.width() - 2 * LABEL_PADDING_PX,
)
// Note that this is specifying the position of the starting text
// baseline.
ctx.fillText(
trimmedText,
physicalLabelBounds.left() + LABEL_PADDING_PX,
Math.round(
physicalLabelBounds.bottom() -
(physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2,
),
)
// Note that this is specifying the position of the starting text
// baseline.
ctx.fillText(
trimmedText,
physicalLabelBounds.left() + LABEL_PADDING_PX,
Math.round(
physicalLabelBounds.bottom() -
(physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2,
),
)
}
}
for (let child of frame.children) {
renderFrameLabelAndChildren(child, depth + 1)
@@ -410,7 +412,7 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
if (this.props.configSpaceViewportRect.isEmpty()) return
this.props.canvasContext.renderInto(this.container, context => {
this.props.canvasContext.renderBehind(this.container, () => {
this.props.flamechartRenderer.render({
physicalSpaceDstRect: this.physicalBounds(),
configSpaceSrcRect: this.props.configSpaceViewportRect,
@@ -658,15 +660,20 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
if (ev.ctrlKey || ev.shiftKey || ev.metaKey) return
// NOTE: We intentionally use ev.code rather than ev.key for
// WASD in order to have the keys retain the same layout even
// if the keyboard layout is not QWERTY.
//
// See: https://github.com/jlfwong/speedscope/pull/184
if (ev.key === '0') {
this.zoom(new Vec2(width / 2, height / 2), 1e9)
} else if (ev.key === 'ArrowRight' || ev.key === 'd') {
} else if (ev.key === 'ArrowRight' || ev.code === 'KeyD') {
this.pan(new Vec2(100, 0))
} else if (ev.key === 'ArrowLeft' || ev.key === 'a') {
} else if (ev.key === 'ArrowLeft' || ev.code === 'KeyA') {
this.pan(new Vec2(-100, 0))
} else if (ev.key === 'ArrowUp' || ev.key === 'w') {
} else if (ev.key === 'ArrowUp' || ev.code === 'KeyW') {
this.pan(new Vec2(0, -100))
} else if (ev.key === 'ArrowDown' || ev.key === 's') {
} else if (ev.key === 'ArrowDown' || ev.code === 'KeyS') {
this.pan(new Vec2(0, 100))
} else if (ev.key === 'Escape') {
this.props.onNodeSelect(null)
+8 -1
View File
@@ -103,7 +103,14 @@ export const createMemoizedFlamechartRenderer = (options?: FlamechartRendererOpt
canvasContext: CanvasContext
flamechart: Flamechart
}): FlamechartRenderer => {
return new FlamechartRenderer(canvasContext, getRowAtlas(canvasContext), flamechart, options)
return new FlamechartRenderer(
canvasContext.gl,
getRowAtlas(canvasContext),
flamechart,
canvasContext.rectangleBatchRenderer,
canvasContext.flamechartColorPassRenderer,
options,
)
},
)