Compare commits

...
46 Commits
Author SHA1 Message Date
Jamie Wong 60f1812e51 1.17.0 2023-12-25 22:40:40 -05:00
Jamie Wong 1717fecafb Upgrade prettier, update prettier & react-hooks eslint plugins (#456)
Re-ran prettier with latest version
2023-12-25 22:37:45 -05:00
Jamie Wong c296f530c7 Upgrade typescript & eslint to latest, fix resulting errors (#455)
Also updated the ci.yml node test versions to 18.x and 20.x, given current support: https://endoflife.date/nodejs
2023-12-25 22:23:33 -05:00
Jamie Wong 8e0fa58d65 Re-enable eslint prettier rule after being accidentally disabled for 3 years (#454)
It looks like in #267 (which was 3 years ago!), I accidentally disabled prettier linting altogether 😱

https://github.com/jlfwong/speedscope/pull/267/files#diff-e2954b558f2aa82baff0e30964490d12942e0e251c1aa56c3294de6ec67b7cf5

There's no comment in that PR about this being an intentional thing, so I have to assume this was a dumb mistake.
2023-12-25 21:22:56 -05:00
Zachary Marion b21480494e Support the chrome JSON trace format (allows viewing of hermes traces) (#453) 2023-12-25 21:11:45 -05:00
Zachary Marion dfd3a0dfb3 Fix bug in selectQueueToTakeFromNext for trace profiles (#450)
I have been taking a lot of profiles using the Hermes profiler, but I noticed that they sometimes to not show up properly. After debugging what exactly was going on, I realized it was because the logic in `selectQueueToTakeFromNext` only checks for name, instead of the key for the event. I had a bunch of events with the name `anonymous` that were getting improperly exited before they should have been due to this logic. 

This fix makes the code more robust if there are added "args" which differentiate an event from another (as is the case in Hermes profiles), however it would still be an issue if they key just defaults to the name.

Example profile before:

<img width="1728" alt="Screenshot 2023-12-15 at 12 54 04 AM" src="https://github.com/jlfwong/speedscope/assets/9957046/345f556e-f944-40f1-b59c-748133acb950">

What it should look like (in Perfetto):
<img width="1051" alt="Screenshot 2023-12-15 at 8 51 38 AM" src="https://github.com/jlfwong/speedscope/assets/9957046/7473cdf8-95f1-49de-a0c7-ef4ac081ff85">

After the fix:

<img width="1728" alt="Screenshot 2023-12-15 at 12 54 29 AM" src="https://github.com/jlfwong/speedscope/assets/9957046/56b0870a-538b-4916-acc8-de2b7dfd78eb">
2023-12-16 00:27:03 -08:00
Jamie Wong ac4a015559 Add bounds checking for sampleTypeIndex (#449)
Wow this was surprising. As reported in #448, the `simple.speedscope.json` file failed import. This was surprising to me because there's a test that covers that file to ensure it imports correctly.

The file provided in #448, however, is from a version of speedscope from 5 years ago before the file had been pretty printed. It turns out that when this *particular* file is not pretty-printed, it's a schematically valid pprof profile.

The fix is to do some bounds checks and return null. After the change, the file imports as you'd expect after realizing its not actually a valid pprof profile.

Fixes #448
2023-12-07 18:43:47 -08:00
byronhe de17f128d0 Update README-zh_CN.md (#442)
fix url
2023-09-04 09:54:35 -07:00
Jamie Wong 304ccf33ab Update publish-and-deploy to remove automated release creation (#440)
Turns out the `--attach` command was hallucinated by GPT!
2023-07-16 15:31:31 -07:00
Jamie Wong 650e505b55 1.16.0 2023-07-16 03:04:29 -07:00
Jamie Wong 8dad28e5e2 Automate more of the release process (#439)
The publish, deploy, and release process is annoying enough at the moment that I avoid doing it frequently. Let's automate most of it to reduce the friction
2023-07-16 03:01:50 -07:00
Nguyễn Văn Đức f62519ab69 Improve profile builder performance (#437)
Follow up on this PR #435. 

Currently, it took roughly 22 seconds to load my 1.3GB file. After inspecting the profiler, there's a large chunk of time spending in Frame.getOrInsert. I figure we can reduce the number of invocations by half. It reduces the load time to roughly 18 seconds.I also tested with a smaller file (~350MB), and it show similar gains, about 15-20%
2023-06-28 22:11:12 -07:00
Nguyễn Văn Đức 984bf1296a Fix crash when importing big linux perf tool files (#435)
Currently, importing files generated by linux perf tool whose some blocks exceed V8 strings limit can crash the application. This issue is similar to the one in #385.

This PR fixes it by changing parseEvents to work directly with lines instead of chunking lines into blocks first.

Fixes #433
2023-06-28 01:30:08 -07:00
Nguyễn Văn Đức 26884c116c Improve splitLines: return iterator instead (#434)
The current behavior of splitLines is to eagerly split all the lines and return an array of strings.

This PR improves this by returning an iterator instead, which will emit lines. This lets callers decide how to best use the splitLines function (i.e. lazily enumerate over lines)

Relates to #433
2023-06-27 23:27:15 -07:00
Jamie Wong bb063e49e3 Fix trimTextMid (#431)
There was a subtle bug in `trimTextMid` caused by calling substring methods with non-integer values. This happens because `findValueBisect` returns non-integer values, and there was no special handling of this.

The bug results in strings cutting off many of the last few characters in a string, rather than always displaying it when possible.

Before:
<img width="368" alt="image" src="https://github.com/jlfwong/speedscope/assets/150329/754a25f1-a6f7-46f1-8e34-059503d9e4cf">

After:
<img width="386" alt="image" src="https://github.com/jlfwong/speedscope/assets/150329/b2688ca1-54af-4d2e-b704-9f3322d2e5b4">

Fixes #411
2023-06-23 13:22:53 -07:00
xieve 693545b77a Added support for Papyrus profiles (#428)
Fixed #427
2023-06-23 13:02:08 -07:00
Jamie Wong b3b4b1492a 1.15.2 2023-06-21 17:28:24 -07:00
Jamie Wong 9cdceede15 Support showing pprof lines from the pprof Line object (take 2) (#430)
The previous behavior was to use the StartLine of a function as the line number to show in speedscope. However, the Line object has more precise line information, and we should only fallback to StartLine if we don't have this more detailed information.

Looking at the [documentation for the pprof proto](https://github.com/google/pprof/tree/main/proto#general-structure-of-a-profile), this is more how it intends to interpret line information:

> location: A unique place in the program, commonly mapped to a single instruction address. It has a unique nonzero id, to be referenced from the samples. It contains source information in the form of lines, and a mapping id that points to a binary.
> function: A program function as defined in the program source. It has a unique nonzero id, referenced from the location lines. It contains a human-readable name for the function (eg a C++ demangled name), a system name (eg a C++ mangled name), the name of the corresponding source file, and other function attributes.

Here is a sample profile that had line-level info on the Line object of the profile:

Before:

<img width="449" alt="Screen Shot 2022-11-03 at 11 17 11 AM" src="https://user-images.githubusercontent.com/618615/199760730-712daa70-6cfb-4e90-b037-b571809c26d9.png">

After:

<img width="449" alt="Screen Shot 2022-11-03 at 11 17 22 AM" src="https://user-images.githubusercontent.com/618615/199760777-1c0d5581-7b29-42b7-b642-6035f7d25405.png">
2023-06-21 15:25:16 -07:00
Manuel Correa 741fdeb427 Stackprof: weight on-cpu samples by period rather than timestamp delta (#425)
This attempts to improve the quality of the on-CPU profiles stackprof provides. Rather than weighing samples by their timestamp deltas, which, in our opinion, are only valid in wall-clock mode, this weighs callchains by:


```
S = number of samples
P = sample period in nanoseconds

W = S * P
```

The difference after this change is quite substantial, specially in profiles that previously were showing up with heavy IO frames:  

* Total profile weight is almost down by 90%, which actually makes sense for an on-CPU profile if the app is relatively idle
* Certain callchains that blocked in syscalls / IO are now much lower weight. This was what I was expecting to find.
Here is an example of the latter point.

In delta mode, we see an io select taking a long time, it is a significant portion of the profile:

<img width="1100" alt="236936508-709bee01-d616-4246-ba74-ab004331dcd3" src="https://github.com/dalehamel/speedscope/assets/4398256/39140f1e-50a9-4f33-8a61-ec98b6273fd4">

But in period scaling mode, it is only a couple of sample periods ultimately:

<img width="206" alt="236936693-9d44304e-a1c2-4906-b3c8-50e19e6f9f27" src="https://github.com/dalehamel/speedscope/assets/4398256/7d19077f-ef25-4d79-980b-cfa1775d928d">
2023-06-17 20:50:19 -07:00
Jake Zimmerman e9133be353 Use frame.name?.startsWith for stackprof (#419)
Sometimes, stackprof frames don't get generated with a `name` in the frame.
I think it's probably worth tracking down why that is, but in the mean
time, speedscope simply crashes with a method call on `undefined`. The
crash is bad because it only shows up in the console--there's no visible
message saying that speedscope failed to parse and load a profile.

For more information, see #378

This fixes the crash by simply skipping the logic in demangle if the name
field isn't present on a frame. That's probably a fine tradeoff? Because in
this case, stackprof is generating ruby frames, which means that C++ name
demangling won't apply.

I have tested this by running the scripts/prepare-test-installation.sh
script and verifying that `bin/cli.js` can now successfully load the
included profile. Before these changes, I verified that speedscope failed
with the behavior mentioned in #378.

I've also included a snapshot test case, but it seems that the Jest test
harness only tests the parsing, not the rendering (correct me if I'm wrong).
So I haven't actually been able to create an automated test that would catch
a regression. Please let me know if there's a better way to have written this
test.

I've staged the commits on this branch so that the second commit (dcb9840)
showcases the minimal diff to a stackprof file that reproduces the bug. That is,
rather than look at the thousands of new lines in the stackprof profile, you can
view the second commit to see the salient part of the file.
2023-06-15 01:30:53 -07:00
Dave Vasilevsky fcc1fa5689 fix pprof defaultSampleType (#424)
Fixes #415

* Interprets pprof's defaultSampleType as an index into the string table [as documented in the proto](https://github.com/jlfwong/speedscope/blob/0414c2f617742e7fb0cf31a66ac0f77c2f5c0540/src/import/profile.proto#L85), not as an index into the sample types repeated-field. This allows parsing to succeed when the string-table index is not a valid sampleTypes index, which is common on allocation profiles.
* Update the pprof snapshot. This is necessarily because we were previously interpreting an empty defaultSampleType as truthy-but-zero when long.js is present, ie: the first sample-type. But Speedscope-in-the-browser doesn't seem to include long.js, so our tests were disagreeing with in-browser behavior. With this PR, that should be fixed.
2023-06-15 01:14:47 -07:00
Jamie Wong 0414c2f617 1.15.1 2023-06-04 04:14:07 -07:00
Jamie Wong f23f65b3af Callgrind: Subposition compression and weight correction (#423)
This fixes a number of bugs with callgrind import. Dealing with this file format is a big pain because the documentation on https://www.valgrind.org/docs/manual/cl-format.html doesn't contain enough examples to disambiguate some of the behaviour, and because there's a fundamental impedance mismatch between call-trees and call-graphs.

In any case, after this PR, the behavior of callgrind file import is much better.
The file provided in #414 now imports correctly and, as far as I can tell, displays the same weights as what I see in KCacheGrind.

Some of the key changes:
- Implementing subposition compression. This was just a TODO in the code that was never implemented
- Fixing a misinterpretation of how `fe` and `fi` were intended to be used. Previously, I was using it to change the filename of a symbol, meaning that an `fi` or an `fe` line in the middle of a block describing costs for an `fn` would split a node in the call-graph into multiple nodes causing all manners of problems
- Fixing a bug where `cfn` was persisting beyond a single call, also resulting in call graph nodes being split when they shouldn't be

Fixes #414
2023-06-04 04:06:22 -07:00
Jamie Wong 8da9088ec1 Fix import from Chrome Devtools performance tab in Chrome >= 114 (#422)
The file format uses by Chrome Devtools performance tab periodically changes. It uses the Chrome trace event format (https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview).

This format, however, has two different types: one is `TraceEvent[]`, the other is `{traceEvents: traceEvent[]}`. The importer for non-Chrome devtools profiles already handled this, but the one for Chrome Devtools didn't because Chrome < 114 never used it. It seems like they changed the file format. This PR addresses that change.

Fixes #420
2023-06-03 22:35:13 -07:00
Jamie Wong 81a6f29ad1 1.15.0 2022-10-22 00:18:24 +08:00
Jamie Wong 263f7d513e Update package.json to use upstream version of uint8array-json-parser (#408)
In #385, I introduced a dependency on the `uint8array-json-parser` npm package, but used a fork because of a typescript error. This was resolved in evanw/uint8array-json-parser#1 and published as part of `uint8array-json-parser@0.0.2`. Let's use the upstream.

This also conveniently fixes a new typechecking error that was preventing deployment. The error looked like this:

```
src/import/utils.ts(2,26): error TS2306: File '\''/Users/jlfwong/code/speedscope/node_modules/uint8array-json-parser/uint8array-json-parser.ts'\'' is not a module.'
```

After updating to the upstream, the problem is fixed.
2022-10-22 00:12:45 +08:00
Jamie Wong 1bce806933 Replace fuzzy matching with exact substring matching for finding matching frames (#407)
In #297, I re-used the fuzzy matching logic I implemented in #282 for profile selection. Based on feedback from several people in #352, this is surprising behavior.

Upon reflection, this should have been obvious -- I hijacked the Ctrl/Cmd+F browser behaviour, so I should try to replicate the expected behaviour there as closely as possible. Given more patience, I also would've done some user research :)

This PR updates this logic to try to more closely match browser behaviour. This means case-insensitive, exact-substring matching.

I've left the fuzzy matching alone for profile selection since that doesn't attempt to mimic browser behaviour.

The non-fuzzy matching feels slightly odd to me given the filtering behaviour on the sandwich view, but I think consistency across this find UI is important.

Here are the before & after results when searching for the string "ca" in the example profile.

|Before|After|
|-|-|
|<img width="1791" alt="image" src="https://user-images.githubusercontent.com/150329/197232741-6d1d7a8a-8b8c-4a4f-98e3-2c043fd7efd5.png">|<img width="1789" alt="image" src="https://user-images.githubusercontent.com/150329/197232694-82697b68-ca15-49e7-887b-2606646ee5e9.png">|

Fixes #352 
Supersedes #403
2022-10-21 23:53:46 +08:00
Jamie Wong 6493c5f66f Update deploy script to python3 2022-07-30 23:20:36 -07:00
Evan Wallace 639dae322b Add support for cycle-based Instruments deep copy (#400)
Unlike the Time Profiler, the CPU Profiler in Instruments use `cycles` for units instead of `ms`:

<img width="872" src="https://user-images.githubusercontent.com/406394/175755999-289cb7c0-f29a-44b1-b00e-b55ef17ee303.png">

Currently Speedscope fails to import the data with the following error in the console:

```
Failed to load format Error: Unrecognized units Gc
```

This PR adds support for `cycles` as a unit to the Instruments deep copy importer as well as `Kc`, `Mc`, and `Gc`, which I'm assuming are increasing in multiples of 1000. Hopefully I've added support for this correctly and this PR is helpful.
2022-07-02 22:01:08 -04:00
Jamie Wong 33a8f3f313 1.4.0
Node.js CI / test (10.x) (push) Has been cancelled
Node.js CI / test (12.x) (push) Has been cancelled
Node.js CI / test (14.x) (push) Has been cancelled
Node.js CI / finish (push) Has been cancelled
2022-05-19 01:37:56 -07:00
Jamie Wong 7ae545a6c3 Improve HoverTip placement logic (#395)
This changes the HoverTip placement logic to use measurements from the actual DOM node rather than basing everything on the maximum sizes.

This avoids some counter-intuitive behaviour, most importantly situations where the label would overflow off the left side of the screen for no obvious reason.

Fixes #394
Fixes #256
2022-05-17 13:42:13 -07:00
David Judd 48d692c2a3 Add a hash param to control view-mode (#362)
e.g. "view=left-heavy", "view=sandwich"

Fixes #355
2022-05-17 00:15:51 -07:00
Alex Coco ca8fcb48cc Support stackprof object mode (#391)
This PR attempts to support stackprof's object mode which tracks the number of allocated objects. This differs from the other modes (cpu and wall) by taking samples every time a Ruby object is allocated using Ruby's [`NEWOBJ` tracepoint](https://github.com/tmm1/stackprof/blob/df24b85953bb45d3abff58d9c82169a3003a60f1/ext/stackprof/stackprof.c#L198-L199).

When importing an object mode profile into speedscope today it still works but what you see is a profile using time units. The profile will only have samples for when an object was allocated which means even if time is reported, the profile is not really meaningful when looking at time.

To address this I've done three things when `mode` is `object`:
+ adjusted the total size of the `StackListProfileBuilder` to use the number of samples (since each sample is one allocation)
+ adjusted the weight of each sample to be `nSamples` (which I believe is always `1` but I'm not positive)
+ do not set the value formatter to a time formatter

Here's what it looks like before and after my changes (note the units and weight of samples):

wall (before) | object (before) | object (after)
-- | -- | --
<img width="1624" alt="Screen Shot 2022-05-11 at 4 51 31 PM" src="https://user-images.githubusercontent.com/898172/167945635-2401ca73-4de7-4559-b884-cf8947ca9738.png"> | <img width="1624" alt="Screen Shot 2022-05-11 at 4 51 34 PM" src="https://user-images.githubusercontent.com/898172/167945641-ef302a60-730b-4afd-8e44-5f02e54b3cb7.png"> | <img width="1624" alt="Screen Shot 2022-05-11 at 4 51 42 PM" src="https://user-images.githubusercontent.com/898172/167945643-5611b267-f8b2-4227-a2bf-7145c4030aa2.png">

<details>
<summary>Test code</summary>

```ruby
require 'stackprof'
require 'json'

def do_test
  5.times do
    make_a_word
  end
end

def make_a_word
  ('a'..'z').to_a.shuffle.map(&:upcase).join
end

StackProf.start(mode: :object, interval: 1, raw: true)
do_test
StackProf.stop

File.write('tmp/object_profile.json', JSON.generate(StackProf.results))

StackProf.start(mode: :wall, interval: 1, raw: true)
do_test
StackProf.stop

File.write('tmp/wall_profile.json', JSON.generate(StackProf.results))
```
</details>
2022-05-17 00:05:49 -07:00
Dan Vanderkam 63f3bc0395 Support relative URLs (#357)
Fixes #312 

This turns out not to be very deep: you have to pass an optional second parameter to the [`URL` constructor](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) to resolve relative URLs.

```
> new URL('/path/to/file#hashcode').pathnaem
VM252:1 Uncaught TypeError: Failed to construct 'URL': Invalid URL
    at <anonymous>:1:1
(anonymous) @ VM252:1
> new URL('/path/to/file#hashcode', 'http://example.com/').pathname
"/path/to/file"
```
2022-05-17 00:01:02 -07:00
Tobias Koppers 1ac88cc09a add file and line information (#365)
* add file and line to tooltips
* add file and line to anonymous methods

## Before:

![image](https://user-images.githubusercontent.com/1365881/134863173-0d5635e8-1884-4276-a2cc-b0b7af5a579b.png)

![image](https://user-images.githubusercontent.com/1365881/134863456-f56aca3b-2742-4194-ba1e-823ff871a316.png)

While you was able to get this info with clicking in "Time Order" and "Left Heavy" view, it was impossible to receive in the sandwich view.

## After:

![image](https://user-images.githubusercontent.com/1365881/134863282-6719db20-e528-4eb5-b876-b35f99c34da4.png)

![image](https://user-images.githubusercontent.com/1365881/134863365-b6a550d4-56d8-4cf0-a17f-f682f1d2fe57.png)
2022-05-16 23:59:17 -07:00
Jamie Wong 9a2c2a270b Add PHP import instructions (fixes #368) 2022-05-16 23:24:14 -07:00
Jamie Wong 229d48eca5 Update README-zh_CN.md to reflect changes in 103db68 2022-05-16 23:18:11 -07:00
Joe Rickerby 103db681d2 Add link to pyinstrument wiki page (#377) 2022-05-16 23:14:25 -07:00
Jamie Wong 21167e69d8 Support importing profiles whose contents exceed V8s maximum string size (#385)
Browsers have a limit on how big you can make strings. In Chrome on a 64 bit machine, this is around 512MB, which explains why in #340, a 600MB file fails to load.

To work around this issue, we avoid making strings this large.

To do so, we need two core changes:
1. Instead of sending large strings as the import mechanism to different file format importers, we introduce a new `TextFileContent` interface which exposes methods to get the lines in the file or the JSON representation. In the case of line splitting, we assume that no single line exceeds the 512MB limit.
2. We introduce a dependency on https://github.com/evanw/uint8array-json-parser to allow us to parse JSON files contained in `Uint8Array` objects

To ensure that this code doesn't code rot without introducing 600MB test files or test file generation into the repository, we also re-run a small set of tests with a mocked maximum string size of 100 bytes. You can see that the chunked string representation code is getting executed via test coverage.

Fixes #340
2022-05-16 23:11:13 -07:00
轩灵 e37f6fa7c3 Add README-zh_CN.md file (#364) 2021-09-22 11:20:25 -07:00
Daniel Giger 6d02bf510f Fix typo in README (#360) 2021-08-09 23:20:20 -07:00
Jamie Wong b71cef5db4 Bump TypeScript to 4.3.2 (#343)
* Bump TypeScript to 4.3.2

* Bump eslint deps

* Fix eslint errors from upgrade
2021-03-28 16:08:15 -07:00
Jamie Wong e6351a3c22 Remove accidentally checked-in vscode settings 2021-03-28 15:32:09 -07:00
Gabriele N. Tornetta 36aebfbda6 Allow collapsed stacks with invalid lines (#336)
Ingest files containing collapsed stacks and tolerate invalid lines,
like FlameGraph does.

Some files might contain lines starting with a # to add comments to the
collected samples. Speedscope should still attempt to parse these files
as collapsed stacks and only keep the samples that it can find. Only
fail if there are no samples reported.
2021-03-28 15:30:36 -07:00
Jamie Wong 246fc3dd5d Remove redux in favor of a small recoil-inspired "atom" library (#341)
This is an experiment in replacing redux entirely with a tiny library I wrote for global application state management.

Redux has been okay, but all of the redux actions in speedscope are setters, which always made me think there must be a simpler way. This is an attempt to find that simpler way.

See `src/lib/atom.ts` for the library.
2021-03-28 02:44:43 -07:00
Gabriele N. Tornetta d6f5efa06c fix(search): allow paste in search box (#338) 2021-03-27 15:05:16 -07:00
122 changed files with 27113 additions and 3196 deletions
+1
View File
@@ -9,6 +9,7 @@ module.exports = {
plugins: ['prettier', '@typescript-eslint', 'react-hooks'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'prettier/prettier': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': [
'error',
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v2
+2 -1
View File
@@ -2,4 +2,5 @@ node_modules
.cache
dist
.idea
coverage
coverage
.vscode
-3
View File
@@ -1,3 +0,0 @@
{
"editor.formatOnSave": true,
}
+58
View File
@@ -1,3 +1,61 @@
## [1.17.0] - 2023-12-25
- Upgrade prettier, update prettier & react-hooks eslint plugins [[#456](https://github.com/jlfwong/speedscope/pull/456)] (by @jlfwong)
- Upgrade typescript & eslint to latest, fix resulting errors [[#455](https://github.com/jlfwong/speedscope/pull/455)] (by @jlfwong)
- Re-enable eslint prettier rule after being accidentally disabled for 3 years [[#454](https://github.com/jlfwong/speedscope/pull/454)] (by @jlfwong)
- Support the chrome JSON trace format (allows viewing of hermes traces) [[#453](https://github.com/jlfwong/speedscope/pull/453)] (by @zacharyfmarion)
- Fix bug in selectQueueToTakeFromNext for trace profiles [[#450](https://github.com/jlfwong/speedscope/pull/450)] (by @zacharyfmarion)
- Add bounds checking for sampleTypeIndex [[#449](https://github.com/jlfwong/speedscope/pull/449)] (by @jlfwong)
- Update README-zh_CN.md [[#442](https://github.com/jlfwong/speedscope/pull/442)] (by @byronhe)
- Update publish-and-deploy to remove automated release creation [[#440](https://github.com/jlfwong/speedscope/pull/440)] (by @jlfwong)
## [1.16.0] - 2023-07-16
- Automate more of the release process [[#439](https://github.com/jlfwong/speedscope/pull/439)] (by @jlfwong)
- Improve profile builder performance [[#437](https://github.com/jlfwong/speedscope/pull/437)] (by @Goose97)
- Fix crash when importing big linux perf tool files [[#435](https://github.com/jlfwong/speedscope/pull/435)] (by @Goose97)
- Improve splitLines: return iterator instead [[#434](https://github.com/jlfwong/speedscope/pull/434)] (by @Goose97)
- Fix trimTextMid [[#431](https://github.com/jlfwong/speedscope/pull/431)] (by @jlfwong)
- Added support for Papyrus profiles [[#428](https://github.com/jlfwong/speedscope/pull/428)] (by @xieve)
## [1.15.2] - 2023-06-21
### Fixed
- Use more accurate line information for pprof profiles [[#430](https://github.com/jlfwong/speedscope/pull/430)] (by @dalehamel)
- Stackprof: weight on-cpu samples by period rather than timestamp delta [[#425](https://github.com/jlfwong/speedscope/pull/425)] (by @manuelfelipe)
- Prevent crashes when stackprof profiles frames are missing names [[#419](https://github.com/jlfwong/speedscope/pull/419)] (by @jez)
- fix pprof defaultSampleType [[#424](https://github.com/jlfwong/speedscope/pull/424)] (by @vasi-stripe)
## [1.15.1] - 2023-06-04
### Fixed
- Fix import from Chrome Devtools performance tab in Chrome >= 114 [[#422](https://github.com/jlfwong/speedscope/pull/422)]
- Callgrind: Subposition compression and weight correction [[#423](https://github.com/jlfwong/speedscope/pull/423)]
## [1.15.0] - 2022-10-22
### Fixed
- Replace fuzzy matching with exact substring matching for finding matching frames [[#407](https://github.com/jlfwong/speedscope/pull/407)]
## [1.14.0] - 2022-05-19
### Added
- File and line information is now displayed in hover tips [[#365](https://github.com/jlfwong/speedscope/pull/365)] (by [@sokra](https://github.com/sokra))
- Support for stackprof object mode [[#391](https://github.com/jlfwong/speedscope/pull/391)] (by [@alexcoco](https://github.com/alexcoco))
- Support for hash params to control view-mode [[#362](https://github.com/jlfwong/speedscope/pull/362)] (by [@djudd](https://github.com/djudd))
- Support for profiles over 512MB now works [[#385](https://github.com/jlfwong/speedscope/pull/385)] (by [@jlfwong](https://github.com/jlfwong))
- Support for relative URLs in profileURL hashParam [[#357](https://github.com/jlfwong/speedscope/pull/357)] (by [@danvk](https://github.com/danvk))
### Fixed
- Allow collapsed stacks with invalid lines for the Brenden Gregg stack format [[#336](https://github.com/jlfwong/speedscope/pull/336)] (by [@P403n1x87](https://github.com/P403n1x87))
- Allow pasting into the search box [[#338](https://github.com/jlfwong/speedscope/pull/338)] (by [@P403n1x87](https://github.com/P403n1x87))
- Prevent hover tips from getting unnecessarily clipped outside container bounds [[#395](https://github.com/jlfwong/speedscope/pull/395)] (by [@jlfwong](https://github.com/jlfwong))
## [1.13.0] - 2021-02-14
### Added
+12 -58
View File
@@ -1,15 +1,5 @@
This document describes processes needed by admins of this repository.
# Publishing
Publishing speedscope is a multi-step process:
1. Test the release
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.
@@ -18,8 +8,9 @@ works if you're on a linux, and almost definitely does not work on Windows.
Speedscope is tested in CI, so all the automated tests should be passing. We'll
just be doing a few sanity checks to make sure the build & deployment machinery is working correctly.
Run `scripts/prepare-test-installation.sh`. This will do a mock publish &
installation to ensure that the version we're about to publish is going to
scripts/prepare-test-installation.sh
This will do a mock publish & installation to ensure that the version we're about to publish is going to
work. At the end of this command, it should echo a `cd` command to run in your shell
to switch to the installation directory. Something like this:
@@ -36,58 +27,21 @@ Try importing a profile from disk via the browse button and make sure it works.
Next, try running `bin/cli.js dist/release/perf-vertx*`. This should immediately open
speedscope in browser, and the perf-vertx file should load immediately.
If everything looks good, proceed to "Prepare the release".
## Create & publish the new release
## Prepare the release
Ensure you have the Github CLI tools installed and you're authenticated. Try running the following if you're unsure:
1. Update the version manually in package.json (we intentionally don't use the `npm version` command)
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 && git push --tags`
gh auth status
npm whoami
## Publish to npm
Once ready to publish, run:
Assuming everything went well in the previous two phases, publishing should just be
a matter of running `npm publish`.
scripts/publish-and-deploy.sh
### Verifying the publish
## Verifying the release
To verify that the publish was successful, run `npm install -g speedscope`.
To verify that the npm 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.
## 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 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:
```
Commit release? [yes/no]:
```
If everything looks good, type `yes` then enter. This will commit to the `gh-pages` branch, and the site should automatically deploy shortly after.
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
To verify the website has finished deploying, check the version number shown in the console of https://www.speedscope.app/
+126
View File
@@ -0,0 +1,126 @@
简体中文 | [English](./README.md)
# 🔬speedscope
一个快速,交互式,基于网络的性能分析工具。 [FlameGraphs][1](火焰图)的另一个替代品。它可以轻松显示数兆的配置文件并且不会使浏览器崩溃。
给定原始分析数据,你就可以交互式的探索数据,了解应用程序中什么部分速度较慢,或者分配所有内存,或者对任何数据进行分析。
![Example Profile](https://user-images.githubusercontent.com/150329/40900669-86eced80-6781-11e8-92c1-dc667b651e72.gif)
[0]: https://en.wikipedia.org/wiki/Profiling_(computer_programming)#Statistical_profilers
[1]: https://github.com/brendangregg/FlameGraph
# 使用
访问 <https://www.speedscope.app> ,上传文件或者拖拽到页面上。配置文件不会上传到任何地方——应用程序完全在浏览器中。
## 命令行中使用
为了方便在不联网的情况下或者在终端中使用, 你可以使用npm下载speedscope:
npm install -g speedscope
调用`speedscope /path/to/profile` 就可以在默认浏览器中加载speedscope。
## 独立使用
如果你不想使用npm或者node下载,你也可以在这里下载独立的版本 <https://github.com/jlfwong/speedscope/releases>.
下载完一个版本的压缩文件之后,解压并在谷歌或者火狐浏览器中打开`index.html`文件即可。
## 支持的文件格式
Speedscope可以不同编程语言和环境的各种不同探查器中摄取概要文件。单击下面的链接获取从特定源导入的文档。
- JavaScript
- [从 Chrome 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Chrome)
- [从 Firefox 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Firefox)
- [从 Safari 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Safari)
- [从 Node.js 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Node.js)
- Ruby
- [从 stackprof 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-stackprof-(ruby))
- [从 rbspy 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-rbspy-(ruby))
- [从 ruby-prof 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-ruby-prof)
- Python
- [从 py-spy 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-py-spy-(python))
- [pyspeedscope](https://github.com/windelbouwman/pyspeedscope)
- [从 Austin 导入](https://github.com/P403n1x87/austin-python#format-conversion)
- [从 pyinstrument 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-pyinstrument-(python))
- PHP
- [从 phpspy 或者 sj-i/php-profiler 导入](https://github.com/sj-i/php-profiler/pull/101)
- Go
- [从 pprof 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-pprof-(go))
- Rust
- [flamescope](https://github.com/coolreader18/flamescope)
- Native code
- [从 Instruments.app 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Instruments.app) (macOS)
- [从 `perf` 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-perf-(linux)) (linux)
- [从 .NET Core 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-.NET-Core)
- [从 GHC (Haskell) 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Haskell)
- [从 custom sources 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources)
极力欢迎贡献添加对其他格式的支持!查看 issues ["import source" tag](https://github.com/jlfwong/speedscope/issues?q=is%3Aissue+is%3Aopen+label%3A%22import+source%22).
## 通过URL导入
要通过URL加载特定的配置文件,你可以添加一个这样的hash片段 `#profileURL=[URL-encoded profile URL]&title=[URL-encoded custom title]`. 注意:托管配置文件的服务器必须配置CORS以允许来自speedscope的AJAX请求。
## 页面
### 🕰Time Order
![Detail View](https://user-images.githubusercontent.com/150329/42108613-e6ef6d3a-7b8f-11e8-93d4-541b2cb93fe5.png)
在 "Time Order" 页面 (默认),调用堆栈按照它们在输入文件中出现的顺序从左到右排列,这通常是安排它们被记录的时间顺序。这个视图对于理解应用程序随时间变化的行为非常有帮助,比如 "首次从数据库获取到数据,然后为序列化准备数据,数据被序列化为JSON"。
水平轴表示每个堆栈的“权重”(最常见的是CPU时间),垂直轴显示在运行期间处于活动状态的堆栈。如果你点击其中一个框,你将能够看到关于它的统计摘要。
### ⬅️Left Heavy
![Left Heavy View](https://user-images.githubusercontent.com/150329/44534434-a05f8380-a6ac-11e8-86ac-e3e05e577c52.png)
在 "Left Heavy" 页面,将相同的堆栈分组在一,不管它们是否按顺序记录。然后,对堆栈进行排序,使每个父堆栈中最重的堆栈位于左侧——因此称为“左权重”。 这个视图对于理解在其他调用栈之间有成百上千个函数交错调用的情况下,时间都花费在了哪里很有用。
### 🥪 Sandwich
![Sandwich View](https://user-images.githubusercontent.com/150329/42108467-76a57baa-7b8f-11e8-815f-1df7b6ac3ede.png)
Sandwich 是一个表格视图,你可以在其中找到所有函数及其相关时间的列表。您可以按自己的时间或总时间排序。
之所以称为"Sandwich"视图,是因为如果你选择表中的某一行,就可以看到所选对象的所有调用者和被调用者的火焰图
## 导航
一旦配置文件被加载,主视图就会被分成两部分:顶部区域是“迷你地图”,底部区域是“堆栈视图”。
### 迷你地图导航
* 在任意一个轴上滚动以进行平移
* 单击并拖动可将视图缩小到特定范围
### 堆栈视图
* 在任意一个轴上滚动以进行平移
* 缩放
* 按住 Cmd+Scroll 进行缩放
* 双击一帧以使视口适应
* 点击一个框以查看关于它的摘要统计
### 键盘导航
* `+`: 放大
* `-`: 缩小
* `0`: 缩小以看到整体情况
* `w`/`a`/`s`/`d` 或者箭头键: pan around the profile
* `1`: 切换到 "Time Order" 视图
* `2`: 切换到 "Left Heavy" 视图
* `3`: 切换到 "Sandwich" 视图
* `r`: 在火焰图中折叠递归
* `Cmd+S`/`Ctrl+S` 保存现有文件
* `Cmd+O`/`Ctrl+O` 打开一个新文件
* `n`: 跳转到下一个文件/如果下一个文件存在
* `p`: 跳转到上一个文件/如果上一个文件存在
* `t`: 打开一个新文件/如果新文件存在
* `Cmd+F`/`Ctrl+F`: 打开搜索。打开时,按下 `Enter` and `Shift+Enter` 查看结果
## 参与贡献
你想成为 speedscope 的贡献者吗? 查看 [CONTRIBUTING.md](./CONTRIBUTING.md) 关于设置开发环境的说明。
+6 -2
View File
@@ -1,4 +1,5 @@
# 🔬speedscope
English | [简体中文](./README-zh_CN.md)
# 🔬speedscope
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.
@@ -46,6 +47,9 @@ speedscope is designed to ingest profiles from a variety of different profilers
- [Importing from py-spy](https://github.com/jlfwong/speedscope/wiki/Importing-from-py-spy-(python))
- [pyspeedscope](https://github.com/windelbouwman/pyspeedscope)
- [Importing from Austin](https://github.com/P403n1x87/austin-python#format-conversion)
- [Importing from pyinstrument](https://github.com/jlfwong/speedscope/wiki/Importing-from-pyinstrument-(python))
- PHP
- [Importing from phpspy or sj-i/php-profiler](https://github.com/sj-i/php-profiler/pull/101)
- Go
- [Importing from pprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-pprof-(go))
- Rust
@@ -81,7 +85,7 @@ In the "Left Heavy" view, identical stacks are grouped together, regardless of w
### 🥪 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.
The Sandwich view is a table view in which you can find a list of all functions and 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.
+14 -4
View File
@@ -66,10 +66,20 @@ async function main() {
const relPath = process.argv[2]
const sourceBuffer = await getProfileBuffer(relPath)
const filename = path.basename(relPath)
const sourceBase64 = sourceBuffer.toString('base64')
const jsSource = `speedscope.loadFileFromBase64(${JSON.stringify(filename)}, ${JSON.stringify(
sourceBase64,
)})`
let jsSource
try {
const sourceBase64 = sourceBuffer.toString('base64')
jsSource = `speedscope.loadFileFromBase64(${JSON.stringify(filename)}, ${JSON.stringify(
sourceBase64,
)})`
} catch(e) {
if (e && e.message && /Cannot create a string longer than/.exec(e.message)) {
jsSource = `alert("Sorry, ${filename} is too large to be loaded via command-line argument! Try dragging it into speedscope instead.")`
} else {
throw e
}
}
const filePrefix = `speedscope-${+new Date()}-${process.pid}`
const jsPath = path.join(os.tmpdir(), `${filePrefix}.js`)
+17375 -759
View File
File diff suppressed because it is too large Load Diff
+10 -11
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "1.13.0",
"version": "1.17.0",
"description": "",
"repository": "jlfwong/speedscope",
"main": "index.js",
@@ -34,28 +34,27 @@
"@types/jszip": "3.1.4",
"@types/node": "14.0.1",
"@types/pako": "1.0.0",
"@typescript-eslint/eslint-plugin": "2.33.0",
"@typescript-eslint/parser": "2.33.0",
"@typescript-eslint/eslint-plugin": "6.16.0",
"@typescript-eslint/parser": "6.16.0",
"acorn": "7.2.0",
"aphrodite": "2.1.0",
"eslint": "6.0.0",
"eslint-plugin-prettier": "2.6.0",
"eslint-plugin-react-hooks": "4.0.2",
"eslint": "8.0.0",
"eslint-plugin-prettier": "5.1.2",
"eslint-plugin-react-hooks": "4.6.0",
"jest": "24.3.0",
"jsverify": "0.8.3",
"jszip": "3.1.5",
"pako": "1.0.6",
"parcel-bundler": "1.12.4",
"preact": "10.4.1",
"prettier": "2.0.4",
"prettier": "3.1.1",
"protobufjs": "6.8.8",
"redux": "^4.0.5",
"source-map": "0.6.1",
"ts-jest": "24.3.0",
"typescript": "3.9.2",
"typescript-eslint-parser": "17.0.1",
"typescript": "5.3.3",
"typescript-json-schema": "0.42.0",
"uglify-es": "3.2.2"
"uglify-es": "3.2.2",
"uint8array-json-parser": "0.0.2"
},
"jest": {
"transform": {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,46 @@
Weight Self Weight Symbol Name
96.08 Gc 100.0% - example_app (40414)
96.08 Gc 100.0% - start
96.08 Gc 100.0% - main
96.08 Gc 100.0% - std::rt::lang_start::hdba6f1ebfd1bdcf8
96.08 Gc 100.0% - std::rt::lang_start_internal::hc453db0ee48af82e
96.08 Gc 100.0% - std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c
96.08 Gc 100.0% - std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc
96.08 Gc 100.0% - core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8
96.08 Gc 100.0% - example_app::main::hddab13ae8d8b6a6e
47.82 Gc 49.7% - example_app::example_a::h622d6879b734496d
46.92 Gc 48.8% 404.75 Mc std::time::Instant::elapsed::h2e3793148fc23529
45.92 Gc 47.7% 45.92 Gc mach_absolute_time
600.78 Mc 0.6% 600.78 Mc std::time::Instant::elapsed::h2e3793148fc23529
1.00 Mc 0.0% 1.00 Mc DYLD-STUB$$mach_absolute_time
634.35 Mc 0.6% 634.35 Mc core::time::Duration::as_millis::hae6ad1b9cf7bb5ec
181.50 Mc 0.1% 181.50 Mc example_app::example_a::h622d6879b734496d
84.45 Mc 0.0% - example_app::example_b::h71b31fcd89b9ffd2
81.45 Mc 0.0% - std::time::Instant::elapsed::h2e3793148fc23529
80.45 Mc 0.0% 80.45 Mc mach_absolute_time
1.00 Mc 0.0% 1.00 Mc std::time::Instant::elapsed::h2e3793148fc23529
1.00 Mc 0.0% - example_app::example_c::h631759c10d7dee93
1.00 Mc 0.0% - alloc::vec::Vec$LT$T$C$A$GT$::push::ha3426502ffc42c8b
1.00 Mc 0.0% - alloc::raw_vec::RawVec$LT$T$C$A$GT$::reserve_for_push::he4a29b8274e35dbb
1.00 Mc 0.0% - alloc::raw_vec::RawVec$LT$T$C$A$GT$::grow_amortized::h78db9ba423623c58
1.00 Mc 0.0% - alloc::raw_vec::finish_grow::h876f5af1c66d74c8
1.00 Mc 0.0% - _$LT$alloc..alloc..Global$u20$as$u20$core..alloc..Allocator$GT$::allocate::hcc733a40a34fbc94
1.00 Mc 0.0% - alloc::alloc::Global::alloc_impl::hc55feba7d6266dfa
1.00 Mc 0.0% - alloc::alloc::alloc::hfac63f3d6850b759
1.00 Mc 0.0% - _malloc_zone_malloc
1.00 Mc 0.0% - nanov2_malloc
1.00 Mc 0.0% - nanov2_allocate
1.00 Mc 0.0% - nanov2_allocate
1.00 Mc 0.0% 1.00 Mc nanov2_find_block_and_allocate
1.00 Mc 0.0% 1.00 Mc example_app::example_b::h71b31fcd89b9ffd2
1.00 Mc 0.0% 1.00 Mc core::time::Duration::as_millis::hae6ad1b9cf7bb5ec
47.45 Gc 49.3% 415.12 Mc std::time::Instant::elapsed::h2e3793148fc23529
46.39 Gc 48.2% 46.39 Gc mach_absolute_time
649.70 Mc 0.6% 649.70 Mc std::time::Instant::elapsed::h2e3793148fc23529
614.22 Mc 0.6% 614.22 Mc core::time::Duration::as_millis::hae6ad1b9cf7bb5ec
190.55 Mc 0.1% 190.55 Mc example_app::main::hddab13ae8d8b6a6e
1.34 Kc 0.0% - std::io::stdio::_print::hdecfefdeb43586ed
1.34 Kc 0.0% - _$LT$$RF$std..io..stdio..Stdout$u20$as$u20$std..io..Write$GT$::write_fmt::h2cb06dcd2b172844
1.34 Kc 0.0% - core::fmt::write::hed96bcfc6342aee5
532 cycles 0.0% - _$LT$std..io..Write..write_fmt..Adapter$LT$T$GT$$u20$as$u20$core..fmt..Write$GT$::write_str::hbe33ec23de24ce2f
532 cycles 0.0% 532 cycles _$LT$std..io..stdio..StdoutLock$u20$as$u20$std..io..Write$GT$::write_all::h22667d0a03b2151b
@@ -0,0 +1,30 @@
# callgrind format
events: Instructions
fl=alpha.c
fn=alpha
1 10
cfl=beta.c
cfn=beta
calls=1 1
1 10
cfn=gamma
calls=1 1
1 10
cfn=delta
calls=1 1
1 20
fn=delta
1 10
cfn=gamma
calls=1 1
1 10
fn=gamma
1 10
cfl=
fl=beta.c
fn=beta
1 10
@@ -18,7 +18,7 @@ calls=3 51
16 400 3000
fn=(2)
51 100 4000
51 100 15000
cfi=(2)
cfn=(3)
calls=2 20
@@ -26,4 +26,4 @@ calls=2 20
fl=(2)
fn=(3)
20 700 6000
20 700 8000
@@ -0,0 +1,24 @@
# callgrind format
events: Instructions
fl=file1.c
fn=main
16 20
cfn=func1
calls=1 50
* 400
cfi=file2.c
cfn=func2
calls=3 20
* *
fn=func1
+35 -300
cfi=file2.c
cfn=func2
calls=2 20
* 300
fl=file2.c
fn=func2
-31 +400
+16
View File
@@ -0,0 +1,16 @@
Script_abc_example_quest log opened (PC)
500:PUSH:3053:1:abcExampleQuest (24021278):abc_example_quest..exampleFunction1
1500:POP:3053:1:abcExampleQuest (24021278):abc_example_quest..exampleFunction1
1700:PUSH:3053:2:abcExampleQuest (24021278):abc_example_quest..exampleFunction2
1750:POP:3053:2:abcExampleQuest (24021278):abc_example_quest..exampleFunction2
2000:PUSH:3208:2:abcExampleQuest (24021278):abc_example_quest..exampleFunction3
2000:PUSH:3053:3:None:abc_example_quest..exampleFunction4
2000:POP:3208:2:abcExampleQuest (24021278):abc_example_quest..exampleFunction3
2000:POP:3053:3:None:abc_example_quest..exampleFunction4
2000:PUSH:3947:1:None:abc_example_quest..exampleFunction5
2250:PUSH:3949:1:None:abc_example_quest..exampleFunction6
2500:POP:3949:1:None:abc_example_quest..exampleFunction6
3000:POP:3947:1:None:abc_example_quest..exampleFunction5
3450:PUSH:3947:1:abcExampleQuest (24021278):abc_example_quest..exampleFunction1
3500:POP:3947:1:abcExampleQuest (24021278):abc_example_quest..exampleFunction1
Log closed
+24
View File
@@ -0,0 +1,24 @@
Stack_3185 log opened (PC)
50002:START:3185
50002:POP:3185:3:None:Debug..StartStackProfiling
50002:QUEUE_PUSH:3185:3: (00018A56):Location.??.GetFormID
50018:PUSH:3185:3: (00018A56):Form..GetFormID
50018:POP:3185:3: (00018A56):Form..GetFormID
50018:PUSH:3185:3:None:abc_example_mod_quest..exampleFunction1
50018:QUEUE_PUSH:3185:4: (00018A56):Location.??.GetFormID
50035:PUSH:3185:4: (00018A56):Form..GetFormID
50035:POP:3185:4: (00018A56):Form..GetFormID
50035:PUSH:3185:4:None:Game..GetModName
50035:POP:3185:4:None:Game..GetModName
50035:POP:3185:3:None:abc_example_mod_quest..exampleFunction1
50035:POP:3185:2:None:abc_example_mod_quest..exampleFunction2
50035:QUEUE_PUSH:3185:2:WhiterunPLainsDistrict03 (0001A27A):Cell.??.IsInterior
50051:PUSH:3185:2:WhiterunPLainsDistrict03 (0001A27A):Cell..IsInterior
50051:POP:3185:2:WhiterunPLainsDistrict03 (0001A27A):Cell..IsInterior
50051:POP:3185:1:abcExampleModQuest (24021278):abc_example_mod_quest..exampleFunction3
50051:QUEUE_PUSH:3185:1:None:utility.??.WaitMenuMode
50068:PUSH:3185:1:None:utility..WaitMenuMode
50602:POP:3185:1:None:utility..WaitMenuMode
50602:QUEUE_POP:3185:0:None:abc_example_mod_effect..OnEffectStart
50619:POP:3185:0:None:abc_example_mod_effect..OnEffectStart
Log closed
@@ -0,0 +1 @@
{"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}]}]}
@@ -0,0 +1,8 @@
# comment that gives extra info about the samples
// some other comment
a;b;c 1
a;b;c 1
a;b;d 4
a;b;c 3
a;b 5
invalid line
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,150 @@
{
"traceEvents": [
{
"name": "process_name",
"ph": "M",
"cat": "__metadata",
"pid": 7512,
"ts": "11550183666",
"tid": "-1",
"args": {
"name": "hermes"
}
},
{
"name": "thread_name",
"ph": "M",
"cat": "__metadata",
"pid": 7512,
"ts": "11550183666",
"tid": "1",
"args": {
"name": "mqt_js"
}
},
{
"name": "mqt_js",
"cat": "mqt_js",
"ph": "X",
"dur": 0,
"pid": 7512,
"ts": "11550183666",
"tid": "1",
"args": {}
},
{
"name": "mqt_js",
"cat": "mqt_js",
"ph": "X",
"dur": 0,
"pid": 7512,
"ts": "11550183666",
"tid": "2",
"args": {}
}
],
"samples": [
{
"cpu": "-1",
"name": "",
"ts": "11552125241",
"pid": 7512,
"tid": "1",
"weight": "1",
"sf": 1
},
{
"cpu": "-1",
"name": "",
"ts": "11552125241",
"pid": 7512,
"tid": "2",
"weight": "1",
"sf": 4
},
{
"cpu": "-1",
"name": "",
"ts": "11552146011",
"pid": 7512,
"tid": "1",
"weight": "1",
"sf": 2
},
{
"cpu": "-1",
"name": "",
"ts": "11552146011",
"pid": 7512,
"tid": "2",
"weight": "1",
"sf": 5
},
{
"cpu": "-1",
"name": "",
"ts": "11552159337",
"pid": 7512,
"tid": "1",
"weight": "1",
"sf": 3
},
{
"cpu": "-1",
"name": "",
"ts": "11552159337",
"pid": 7512,
"tid": "2",
"weight": "1",
"sf": 6
},
{
"cpu": "-1",
"name": "",
"ts": "11552169337",
"pid": 7512,
"tid": "2",
"weight": "1",
"sf": 4
},
{
"cpu": "-1",
"name": "",
"ts": "11552179337",
"pid": 7512,
"tid": "2",
"weight": "1",
"sf": 4
}
],
"stackFrames": {
"1": {
"name": "[root]",
"category": "root"
},
"2": {
"name": "function1",
"category": "JavaScript",
"parent": 1
},
"3": {
"name": "[GC Young Gen]",
"category": "Metadata",
"parent": 1
},
"4": {
"name": "[root thread 2]",
"category": "root"
},
"5": {
"name": "function3",
"category": "JavaScript",
"parent": 4
},
"6": {
"name": "function4",
"category": "Metadata",
"parent": 4
}
}
}
@@ -0,0 +1,127 @@
{
"traceEvents": [
{
"name": "process_name",
"ph": "M",
"cat": "__metadata",
"pid": 7512,
"ts": "11550183666",
"tid": "-1",
"args": {
"name": "hermes"
}
},
{
"name": "thread_name",
"ph": "M",
"cat": "__metadata",
"pid": 7512,
"ts": "11550183666",
"tid": "7695",
"args": {
"name": "mqt_js"
}
},
{
"name": "mqt_js",
"cat": "mqt_js",
"ph": "X",
"dur": 0,
"pid": 7512,
"ts": "11550183666",
"tid": "7695",
"args": {}
}
],
"samples": [
{
"cpu": "-1",
"name": "",
"ts": "11552125241",
"pid": 7512,
"tid": "7695",
"weight": "1",
"sf": 1
},
{
"cpu": "-1",
"name": "",
"ts": "11552129681",
"pid": 7512,
"tid": "7695",
"weight": "1",
"sf": 1
},
{
"cpu": "-1",
"name": "",
"ts": "11552146011",
"pid": 7512,
"tid": "7695",
"weight": "1",
"sf": 2
},
{
"cpu": "-1",
"name": "",
"ts": "11552159337",
"pid": 7512,
"tid": "7695",
"weight": "1",
"sf": 4
},
{
"cpu": "-1",
"name": "",
"ts": "11552169942",
"pid": 7512,
"tid": "7695",
"weight": "1",
"sf": 1
},
{
"cpu": "-1",
"name": "",
"ts": "11552179951",
"pid": 7512,
"tid": "7695",
"weight": "1",
"sf": 5
},
{
"cpu": "-1",
"name": "",
"ts": "11552189951",
"pid": 7512,
"tid": "7695",
"weight": "1",
"sf": 5
}
],
"stackFrames": {
"1": {
"name": "[root]",
"category": "root"
},
"2": {
"name": "function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1)",
"category": "JavaScript",
"parent": 1
},
"3": {
"name": "function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4)",
"category": "JavaScript",
"parent": 1
},
"4": {
"name": "function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3)",
"category": "JavaScript",
"parent": 3
},
"5": {
"name": "[GC Young Gen]",
"category": "Metadata",
"parent": 1
}
}
}
@@ -0,0 +1,14 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "anonymous", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "anonymous", "ts": 1, "args": { "parent": 1 }},
{"pid": 0, "tid": 0, "ph": "B", "name": "function1", "ts": 1, "args": { "parent": 2 }},
{"pid": 0, "tid": 0, "ph": "B", "name": "anonymous", "ts": 1, "args": { "parent": 3 }},
{"pid": 0, "tid": 0, "ph": "E", "name": "anonymous", "ts": 3, "args": { "parent": 3 }},
{"pid": 0, "tid": 0, "ph": "E", "name": "function1", "ts": 3, "args": { "parent": 2 }},
{"pid": 0, "tid": 0, "ph": "E", "name": "anonymous", "ts": 3, "args": { "parent": 1 }},
{"pid": 0, "tid": 0, "ph": "B", "name": "anonymous", "ts": 3, "args": { "parent": 1 }},
{"pid": 0, "tid": 0, "ph": "B", "name": "anonymous", "ts": 3, "args": { "parent": 8 }},
{"pid": 0, "tid": 0, "ph": "E", "name": "anonymous", "ts": 5, "args": { "parent": 8 }},
{"pid": 0, "tid": 0, "ph": "E", "name": "anonymous", "ts": 5, "args": { "parent": 1 }},
{"pid": 0, "tid": 0, "ph": "E", "name": "anonymous", "ts": 5}
]
+25
View File
@@ -0,0 +1,25 @@
require 'json'
require 'stackprof'
def a
for i in 0..2 do
b
c
end
end
def b
Object.new
end
def c
for i in 0..5
(1..10).to_a.sample(3).sort
end
end
profile = StackProf.run(mode: :object, raw: true) do
a
end
puts JSON.generate(profile)
+8 -2
View File
@@ -5,6 +5,7 @@ def a
for i in 0..100 do
b
c
e
end
end
@@ -28,8 +29,13 @@ def d
prod
end
profile = StackProf.run(mode: :wall, raw: true) do
def e
sleep 0.05
end
mode = (ARGV[0] || :wall).to_sym
profile = StackProf.run(mode: mode, raw: true) do
a
end
puts JSON.generate(profile)
puts JSON.generate(profile)
+1 -1
View File
@@ -58,5 +58,5 @@ echo "Build complete. Starting server on http://localhost:4444/"
echo "Hit Ctrl+C to complete or cancel the release"
echo
echo
python -m SimpleHTTPServer 4444 .
python3 -m http.server 4444
set +x
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# Print changes since the last tagged release in a format to match CHANGELOG.md
set -euo pipefail
if [ $# -lt 1 ]; then
echo "Usage: $0 <version>"
echo "e.g. $0 1.15.2"
exit 1
fi
version="$1"
# Get the most recent tagged commit that is an ancestor of HEAD
tagged_commit=$(git log --simplify-by-decoration --oneline --decorate | grep -E '^\w+ \(tag: .+\) .+$' | head -n1 | cut -d' ' -f 1)
gitlog=$(git log --graph --pretty=format:"%s" --abbrev-commit "$tagged_commit"..HEAD)
version="$1"
current_date=$(date +%Y-%m-%d)
echo "## [$version] - $current_date"
echo
while IFS= read line; do
message=$(echo "$line" | sed 's/^* //g')
message_without_pr_num=$(echo "$message" | sed 's/ (#[0-9][0-9]*)//g')
pr_number=$(echo "$message" | grep -Eo '#[0-9]+' | grep -Eo '[0-9]+'; true)
if [[ -n "$pr_number" ]]; then
author=$(gh pr view $pr_number --json author -q ".author.login")
pr_link="https://github.com/jlfwong/speedscope/pull/$pr_number"
echo "- $message_without_pr_num [[#$pr_number]($pr_link)] (by @$author)"
else
echo "- $message_without_pr_num"
fi
done <<< "$gitlog"
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# Run the full release process. This means...
# - Bumping the version in package.json
# - Updating the changelog
# - Commiting and tagging the release
# - Pushing to Github
# - Publishing a new version to npm
# - Deploying the website
# - Create a new release in Github with the zip-file standalone version
set -euxo pipefail
if [ $# -lt 1 ]; then
echo "Usage: $0 <minor | patch | version>"
echo "e.g. $0 patch"
exit 1
fi
version_arg="$1"
# Bump versions in package.json and package-lock.json, but don't commit or make tags yet
version=$(npm version "$version_arg" --no-git-tag-version --no-commit-hooks | sed 's/^v//g')
tagname="v$version"
script_dir=$(dirname "$0")
# Prepend the changelog update to CHANGELOG.md
changelog_update=$("$script_dir/print-changelog-update.sh" "$version")
echo -e "$changelog_update\n" > CHANGELOG.md.new
cat CHANGELOG.md >> CHANGELOG.md.new
mv CHANGELOG.md.new CHANGELOG.md
# Commit and tag the release
git add CHANGELOG.md package.json package-lock.json
git commit -m "$version"
git tag "$tagname"
# Push to Github
git push
git push --tags
# Publish to npm
npm publish
# Deploy the website
npm run deploy
# Create a new release on Github
"$script_dir/prepare-zip-file.sh"
# Don't double echo the below commands
set +x
echo
echo
echo "Visit https://github.com/jlfwong/speedscope/releases/new to create a new release."
echo
echo "tag: $tagname"
echo "title: $tagname"
echo "attachment: dist/release/speedscope-$version.zip"
echo "$changelog_update"
# NOTE: This part is almost-but-not-quite-automatable using a command like this:
#
# gh release create "$tagname" --title "$tagname" --notes "$changelog_update"
# --attach "dist/release/speedscope-$version.zip"
#
# There are two problems.
#
# 1. The "--attach" flag doesn't exit
# 2. I don't want the changelog notes to include the version and date like they do in CHANGELOG.md
#
# If 1. was solveable, then 2. would be easy to work-around.
+6 -6
View File
@@ -4,10 +4,10 @@ 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,
- `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)
- `app-state/`: Speedscope's application state management
- `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).
+34
View File
@@ -0,0 +1,34 @@
import {Profile} from '../lib/profile'
import {getProfileToView} from './getters'
import {flattenRecursionAtom, profileGroupAtom} from '.'
import {FlamechartViewState, SandwichViewState} from './profile-group'
import {useAtom} from '../lib/atom'
export interface ApplicationState {}
export interface ActiveProfileState {
profile: Profile
index: number
chronoViewState: FlamechartViewState
leftHeavyViewState: FlamechartViewState
sandwichViewState: SandwichViewState
}
export function useActiveProfileState(): ActiveProfileState | null {
const flattenRecursion = useAtom(flattenRecursionAtom)
const profileGroupState = useAtom(profileGroupAtom)
if (!profileGroupState) return null
if (profileGroupState.indexToView >= profileGroupState.profiles.length) return null
const index = profileGroupState.indexToView
const profileState = profileGroupState.profiles[index]
return {
...profileGroupState.profiles[profileGroupState.indexToView],
profile: getProfileToView({
profile: profileState.profile,
flattenRecursion,
}),
index: profileGroupState.indexToView,
}
}
+95
View File
@@ -0,0 +1,95 @@
import {Atom} from '../lib/atom'
export const enum ColorScheme {
// Default: respect prefers-color-schema
SYSTEM,
// Use dark theme
DARK,
// use light theme
LIGHT,
}
const localStorageKey = 'speedscope-color-scheme'
function getStoredPreference(): ColorScheme {
const storedPreference = window.localStorage && window.localStorage[localStorageKey]
if (storedPreference === 'DARK') {
return ColorScheme.DARK
} else if (storedPreference === 'LIGHT') {
return ColorScheme.LIGHT
} else {
return ColorScheme.SYSTEM
}
}
function matchMediaDarkColorScheme(): MediaQueryList {
return matchMedia('(prefers-color-scheme: dark)')
}
function nextColorScheme(scheme: ColorScheme): ColorScheme {
const systemPrefersDarkMode = matchMediaDarkColorScheme().matches
// We'll use a different cycling order for changing the color scheme depending
// on what the *current* system preference is. This should guarantee that when
// a user interacts with the color scheme toggle for the first time, it always
// changes the color scheme.
if (systemPrefersDarkMode) {
switch (scheme) {
case ColorScheme.SYSTEM: {
return ColorScheme.LIGHT
}
case ColorScheme.LIGHT: {
return ColorScheme.DARK
}
case ColorScheme.DARK: {
return ColorScheme.SYSTEM
}
}
} else {
switch (scheme) {
case ColorScheme.SYSTEM: {
return ColorScheme.DARK
}
case ColorScheme.DARK: {
return ColorScheme.LIGHT
}
case ColorScheme.LIGHT: {
return ColorScheme.SYSTEM
}
}
}
}
class ColorSchemeAtom extends Atom<ColorScheme> {
cycleToNextColorScheme = () => {
this.set(nextColorScheme(this.get()))
}
}
export const colorSchemeAtom = new ColorSchemeAtom(getStoredPreference(), 'colorScheme')
colorSchemeAtom.subscribe(() => {
const value = colorSchemeAtom.get()
switch (value) {
case ColorScheme.DARK: {
window.localStorage[localStorageKey] = 'DARK'
break
}
case ColorScheme.LIGHT: {
window.localStorage[localStorageKey] = 'LIGHT'
break
}
case ColorScheme.SYSTEM: {
delete window.localStorage[localStorageKey]
break
}
default: {
const _exhaustiveCheck: never = value
return _exhaustiveCheck
}
}
return value
})
+78
View File
@@ -0,0 +1,78 @@
import {Atom} from '../lib/atom'
import {ViewMode} from '../lib/view-mode'
import {getHashParams, HashParams} from '../lib/hash-params'
import {ProfileGroupAtom} from './profile-group'
// True if recursion should be flattened when viewing flamegraphs
export const flattenRecursionAtom = new Atom<boolean>(false, 'flattenRecursion')
// The query used in top-level views
//
// An empty string indicates that the search is open by no filter is applied.
// searchIsActive is stored separately, because we may choose to persist the
// query even when the search input is closed.
export const searchIsActiveAtom = new Atom<boolean>(false, 'searchIsActive')
export const searchQueryAtom = new Atom<string>('', 'searchQueryAtom')
// Which top-level view should be displayed
export const viewModeAtom = new Atom<ViewMode>(ViewMode.CHRONO_FLAME_CHART, 'viewMode')
// The top-level profile group from which most other data will be derived
export const profileGroupAtom = new ProfileGroupAtom(null, 'profileGroup')
viewModeAtom.subscribe(() => {
// If we switch views, the hover information is no longer relevant
profileGroupAtom.clearHoverNode()
})
// Parameters defined by the URL encoded k=v pairs after the # in the URL
const hashParams = getHashParams()
export const hashParamsAtom = new Atom<HashParams>(hashParams, 'hashParams')
// The <canvas> element used for WebGL
export const glCanvasAtom = new Atom<HTMLCanvasElement | null>(null, 'glCanvas')
// True when a file drag is currently active. Used to indicate that the
// application is a valid drop target.
export const dragActiveAtom = new Atom<boolean>(false, 'dragActive')
// True when the application is currently in a loading state. Used to
// display a loading progress bar.
// Speedscope is usable both from a local HTML file being served
// from a file:// URL, and via websites. In the case of file:// URLs,
// however, XHR will be unavailable to fetching files in adjacent directories.
const protocol = window.location.protocol
export const canUseXHR = protocol === 'http:' || protocol === 'https:'
const isImmediatelyLoading = canUseXHR && hashParams.profileURL != null
export const loadingAtom = new Atom<boolean>(isImmediatelyLoading, 'loading')
// True when the application is an error state, e.g. because the profile
// imported was invalid.
export const errorAtom = new Atom<boolean>(false, 'error')
export enum SortField {
SYMBOL_NAME,
SELF,
TOTAL,
}
export enum SortDirection {
ASCENDING,
DESCENDING,
}
export interface SortMethod {
field: SortField
direction: SortDirection
}
// The table sorting method using for the sandwich view, specifying the column
// to sort by, and the direction to sort that clumn.
export const tableSortMethodAtom = new Atom<SortMethod>(
{
field: SortField.SELF,
direction: SortDirection.DESCENDING,
},
'tableSortMethod',
)
+227
View File
@@ -0,0 +1,227 @@
import {Atom} from '../lib/atom'
import {clamp, Rect, Vec2} from '../lib/math'
import {CallTreeNode, Frame, Profile, ProfileGroup} from '../lib/profile'
import {objectsHaveShallowEquality} from '../lib/utils'
export interface FlamechartViewState {
hover: {
node: CallTreeNode
event: MouseEvent
} | null
selectedNode: CallTreeNode | null
logicalSpaceViewportSize: Vec2
configSpaceViewportRect: Rect
}
export interface CallerCalleeState {
selectedFrame: Frame
invertedCallerFlamegraph: FlamechartViewState
calleeFlamegraph: FlamechartViewState
}
export interface SandwichViewState {
callerCallee: CallerCalleeState | null
}
export interface ProfileState {
profile: Profile
chronoViewState: FlamechartViewState
leftHeavyViewState: FlamechartViewState
sandwichViewState: SandwichViewState
}
export type ProfileGroupState = {
name: string
// The index within the list of profiles currently being viewed
indexToView: number
profiles: ProfileState[]
} | null
export enum FlamechartID {
LEFT_HEAVY = 'LEFT_HEAVY',
CHRONO = 'CHRONO',
SANDWICH_INVERTED_CALLERS = 'SANDWICH_INVERTED_CALLERS',
SANDWICH_CALLEES = 'SANDWICH_CALLEES',
}
let initialFlameChartViewState: FlamechartViewState = {
hover: null,
selectedNode: null,
configSpaceViewportRect: Rect.empty,
logicalSpaceViewportSize: Vec2.zero,
}
export class ProfileGroupAtom extends Atom<ProfileGroupState> {
set(newState: ProfileGroupState) {
const oldState = this.state
if (oldState != null && newState != null && objectsHaveShallowEquality(oldState, newState)) {
return
}
super.set(newState)
}
getActiveProfile(): ProfileState | null {
if (this.state == null) return null
return this.state.profiles[this.state?.indexToView] || null
}
setProfileGroup = (group: ProfileGroup) => {
this.set({
name: group.name,
indexToView: group.indexToView,
profiles: group.profiles.map(p => ({
profile: p,
chronoViewState: initialFlameChartViewState,
leftHeavyViewState: initialFlameChartViewState,
sandwichViewState: {callerCallee: null},
})),
})
}
setProfileIndexToView = (indexToView: number) => {
if (this.state == null) return
indexToView = clamp(indexToView, 0, this.state.profiles.length - 1)
this.set({
...this.state,
indexToView,
})
}
private updateActiveProfileState(fn: (profileState: ProfileState) => ProfileState) {
if (this.state == null) return
const {indexToView, profiles} = this.state
this.set({
...this.state,
profiles: profiles.map((p, i) => {
if (i != indexToView) return p
return fn(p)
}),
})
}
private updateActiveSandwichViewState(
fn: (sandwichViewState: SandwichViewState) => SandwichViewState,
) {
this.updateActiveProfileState(p => ({
...p,
sandwichViewState: fn(p.sandwichViewState),
}))
}
setSelectedFrame = (frame: Frame | null) => {
if (this.state == null) return
const profile = this.getActiveProfile()
if (profile == null) {
return
}
this.updateActiveSandwichViewState(sandwichViewState => {
if (frame == null) {
return {callerCallee: null}
}
return {
callerCallee: {
invertedCallerFlamegraph: initialFlameChartViewState,
calleeFlamegraph: initialFlameChartViewState,
selectedFrame: frame,
},
}
})
}
private updateFlamechartState(
id: FlamechartID,
fn: (flamechartViewState: FlamechartViewState) => FlamechartViewState,
) {
switch (id) {
case FlamechartID.CHRONO: {
this.updateActiveProfileState(p => ({
...p,
chronoViewState: fn(p.chronoViewState),
}))
break
}
case FlamechartID.LEFT_HEAVY: {
this.updateActiveProfileState(p => ({
...p,
leftHeavyViewState: fn(p.leftHeavyViewState),
}))
break
}
case FlamechartID.SANDWICH_CALLEES: {
this.updateActiveSandwichViewState(s => ({
...s,
callerCallee:
s.callerCallee == null
? null
: {
...s.callerCallee,
calleeFlamegraph: fn(s.callerCallee.calleeFlamegraph),
},
}))
break
}
case FlamechartID.SANDWICH_INVERTED_CALLERS: {
this.updateActiveSandwichViewState(s => ({
...s,
callerCallee:
s.callerCallee == null
? null
: {
...s.callerCallee,
invertedCallerFlamegraph: fn(s.callerCallee.invertedCallerFlamegraph),
},
}))
break
}
}
}
setFlamechartHoveredNode(
id: FlamechartID,
hover: {node: CallTreeNode; event: MouseEvent} | null,
) {
this.updateFlamechartState(id, f => ({
...f,
hover,
}))
}
setSelectedNode(id: FlamechartID, selectedNode: CallTreeNode | null) {
this.updateFlamechartState(id, f => ({
...f,
selectedNode,
}))
}
setConfigSpaceViewportRect(id: FlamechartID, configSpaceViewportRect: Rect) {
this.updateFlamechartState(id, f => ({
...f,
configSpaceViewportRect,
}))
}
setLogicalSpaceViewportSize(id: FlamechartID, logicalSpaceViewportSize: Vec2) {
this.updateFlamechartState(id, f => ({
...f,
logicalSpaceViewportSize,
}))
}
clearHoverNode() {
// TODO(jlfwong): This causes 4 separate observer events. This is probably
// fine, since I hope that Preact/React are smart about batching re-renders?
this.setFlamechartHoveredNode(FlamechartID.CHRONO, null)
this.setFlamechartHoveredNode(FlamechartID.LEFT_HEAVY, null)
this.setFlamechartHoveredNode(FlamechartID.SANDWICH_CALLEES, null)
this.setFlamechartHoveredNode(FlamechartID.SANDWICH_INVERTED_CALLERS, null)
}
}
+4 -1
View File
@@ -102,7 +102,10 @@ export class FlamechartColorPassRenderer {
private material: Graphics.Material
private buffer: Graphics.VertexBuffer
constructor(private gl: Graphics.Context, theme: Theme) {
constructor(
private gl: Graphics.Context,
theme: Theme,
) {
const vertices = [
{pos: [-1, 1], uv: [0, 1]},
{pos: [1, 1], uv: [1, 1]},
+4 -1
View File
@@ -70,7 +70,10 @@ export class ViewportRectangleRenderer {
private material: Graphics.Material
private buffer: Graphics.VertexBuffer
constructor(private gl: Graphics.Context, theme: Theme) {
constructor(
private gl: Graphics.Context,
theme: Theme,
) {
const vertices = [
[-1, 1],
[1, 1],
@@ -212,6 +212,60 @@ exports[`importFromBGFlameGraph with UTF-16, Little Endian: indexToView 1`] = `0
exports[`importFromBGFlameGraph with UTF-16, Little Endian: profileGroup.name 1`] = `"simple-utf16-le.txt"`;
exports[`importFromBGFlameGraph with invalid lines 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-with-invalids.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
"a;b;c 3",
"a;b 5",
],
}
`;
exports[`importFromBGFlameGraph with invalid lines: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with invalid lines: profileGroup.name 1`] = `"simple-with-invalids.txt"`;
exports[`importFromBGFlameGraph: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph: profileGroup.name 1`] = `"simple.txt"`;
+105 -5
View File
@@ -41,6 +41,61 @@ Object {
}
`;
exports[`importFromCallgrind cfn reset 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "alpha.c",
"key": "alpha.c:alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 10,
"totalWeight": 50,
},
Frame {
"col": undefined,
"file": "beta.c",
"key": "beta.c:beta",
"line": undefined,
"name": "beta",
"selfWeight": 10,
"totalWeight": 10,
},
Frame {
"col": undefined,
"file": "alpha.c",
"key": "alpha.c:gamma",
"line": undefined,
"name": "gamma",
"selfWeight": 20,
"totalWeight": 20,
},
Frame {
"col": undefined,
"file": "alpha.c",
"key": "alpha.c:delta",
"line": undefined,
"name": "delta",
"selfWeight": 10,
"totalWeight": 20,
},
],
"name": "callgrind.cfn-reset.log -- Instructions",
"stacks": Array [
"alpha;beta 10",
"alpha;gamma 10",
"alpha;delta;gamma 10",
"alpha;delta 10",
"alpha 10",
],
}
`;
exports[`importFromCallgrind cfn reset: indexToView 1`] = `0`;
exports[`importFromCallgrind cfn reset: profileGroup.name 1`] = `"callgrind.cfn-reset.log"`;
exports[`importFromCallgrind multiple event types 1`] = `
Object {
"frames": Array [
@@ -100,7 +155,7 @@ Object {
"key": "file1.c:func1",
"line": undefined,
"name": "func1",
"selfWeight": 8888.888888888889,
"selfWeight": 15000,
"totalWeight": 20000,
},
Frame {
@@ -109,14 +164,14 @@ Object {
"key": "file2.c:func2",
"line": undefined,
"name": "func2",
"selfWeight": 14111.111111111111,
"totalWeight": 14111.111111111111,
"selfWeight": 8000,
"totalWeight": 8000,
},
],
"name": "callgrind.multiple-event-types.log -- Memory",
"stacks": Array [
"main;func1;func2 10.85 KB",
"main;func1 8.68 KB",
"main;func1;func2 4.88 KB",
"main;func1 14.65 KB",
"main;func2 2.93 KB",
"main 14.65 KB",
],
@@ -172,6 +227,51 @@ exports[`importFromCallgrind name compression: indexToView 1`] = `0`;
exports[`importFromCallgrind name compression: profileGroup.name 1`] = `"callgrind.name-compression.log"`;
exports[`importFromCallgrind subposition compression 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "file1.c",
"key": "file1.c:main",
"line": undefined,
"name": "main",
"selfWeight": 20,
"totalWeight": 820,
},
Frame {
"col": undefined,
"file": "file1.c",
"key": "file1.c:func1",
"line": undefined,
"name": "func1",
"selfWeight": 100,
"totalWeight": 400,
},
Frame {
"col": undefined,
"file": "file2.c",
"key": "file2.c:func2",
"line": undefined,
"name": "func2",
"selfWeight": 700,
"totalWeight": 700,
},
],
"name": "callgrind.subposition-compression.log -- Instructions",
"stacks": Array [
"main;func1;func2 300",
"main;func1 100",
"main;func2 400",
"main 20",
],
}
`;
exports[`importFromCallgrind subposition compression: indexToView 1`] = `0`;
exports[`importFromCallgrind subposition compression: profileGroup.name 1`] = `"callgrind.subposition-compression.log"`;
exports[`importFromCallgrind: indexToView 1`] = `0`;
exports[`importFromCallgrind: profileGroup.name 1`] = `"callgrind.example.log"`;
File diff suppressed because it is too large Load Diff
@@ -99,10 +99,332 @@ Object {
}
`;
exports[`importFromInstrumentsDeepCopy allocations profile 2`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": ":example_app (40414)",
"line": undefined,
"name": "example_app (40414)",
"selfWeight": 0,
"totalWeight": 96000000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":start",
"line": undefined,
"name": "start",
"selfWeight": 0,
"totalWeight": 96000000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":main",
"line": undefined,
"name": "main",
"selfWeight": 0,
"totalWeight": 96000000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":std::rt::lang_start::hdba6f1ebfd1bdcf8",
"line": undefined,
"name": "std::rt::lang_start::hdba6f1ebfd1bdcf8",
"selfWeight": 0,
"totalWeight": 96000000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":std::rt::lang_start_internal::hc453db0ee48af82e",
"line": undefined,
"name": "std::rt::lang_start_internal::hc453db0ee48af82e",
"selfWeight": 0,
"totalWeight": 96000000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c",
"line": undefined,
"name": "std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c",
"selfWeight": 0,
"totalWeight": 96000000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc",
"line": undefined,
"name": "std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc",
"selfWeight": 0,
"totalWeight": 96000000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8",
"line": undefined,
"name": "core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8",
"selfWeight": 0,
"totalWeight": 96000000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":example_app::main::hddab13ae8d8b6a6e",
"line": undefined,
"name": "example_app::main::hddab13ae8d8b6a6e",
"selfWeight": 1385999000,
"totalWeight": 96000000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":example_app::example_a::h622d6879b734496d",
"line": undefined,
"name": "example_app::example_a::h622d6879b734496d",
"selfWeight": 282000000,
"totalWeight": 47000000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":std::time::Instant::elapsed::h2e3793148fc23529",
"line": undefined,
"name": "std::time::Instant::elapsed::h2e3793148fc23529",
"selfWeight": 2000000000,
"totalWeight": 93081000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":mach_absolute_time",
"line": undefined,
"name": "mach_absolute_time",
"selfWeight": 91080000000,
"totalWeight": 91080000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":DYLD-STUB$$mach_absolute_time",
"line": undefined,
"name": "DYLD-STUB$$mach_absolute_time",
"selfWeight": 1000000,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":core::time::Duration::as_millis::hae6ad1b9cf7bb5ec",
"line": undefined,
"name": "core::time::Duration::as_millis::hae6ad1b9cf7bb5ec",
"selfWeight": 1249000000,
"totalWeight": 1249000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":example_app::example_b::h71b31fcd89b9ffd2",
"line": undefined,
"name": "example_app::example_b::h71b31fcd89b9ffd2",
"selfWeight": 1000000,
"totalWeight": 84000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":example_app::example_c::h631759c10d7dee93",
"line": undefined,
"name": "example_app::example_c::h631759c10d7dee93",
"selfWeight": 0,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":alloc::vec::Vec$LT$T$C$A$GT$::push::ha3426502ffc42c8b",
"line": undefined,
"name": "alloc::vec::Vec$LT$T$C$A$GT$::push::ha3426502ffc42c8b",
"selfWeight": 0,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":alloc::raw_vec::RawVec$LT$T$C$A$GT$::reserve_for_push::he4a29b8274e35dbb",
"line": undefined,
"name": "alloc::raw_vec::RawVec$LT$T$C$A$GT$::reserve_for_push::he4a29b8274e35dbb",
"selfWeight": 0,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":alloc::raw_vec::RawVec$LT$T$C$A$GT$::grow_amortized::h78db9ba423623c58",
"line": undefined,
"name": "alloc::raw_vec::RawVec$LT$T$C$A$GT$::grow_amortized::h78db9ba423623c58",
"selfWeight": 0,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":alloc::raw_vec::finish_grow::h876f5af1c66d74c8",
"line": undefined,
"name": "alloc::raw_vec::finish_grow::h876f5af1c66d74c8",
"selfWeight": 0,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":_$LT$alloc..alloc..Global$u20$as$u20$core..alloc..Allocator$GT$::allocate::hcc733a40a34fbc94",
"line": undefined,
"name": "_$LT$alloc..alloc..Global$u20$as$u20$core..alloc..Allocator$GT$::allocate::hcc733a40a34fbc94",
"selfWeight": 0,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":alloc::alloc::Global::alloc_impl::hc55feba7d6266dfa",
"line": undefined,
"name": "alloc::alloc::Global::alloc_impl::hc55feba7d6266dfa",
"selfWeight": 0,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":alloc::alloc::alloc::hfac63f3d6850b759",
"line": undefined,
"name": "alloc::alloc::alloc::hfac63f3d6850b759",
"selfWeight": 0,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":_malloc_zone_malloc",
"line": undefined,
"name": "_malloc_zone_malloc",
"selfWeight": 0,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":nanov2_malloc",
"line": undefined,
"name": "nanov2_malloc",
"selfWeight": 0,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":nanov2_allocate",
"line": undefined,
"name": "nanov2_allocate",
"selfWeight": 0,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":nanov2_find_block_and_allocate",
"line": undefined,
"name": "nanov2_find_block_and_allocate",
"selfWeight": 1000000,
"totalWeight": 1000000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":std::io::stdio::_print::hdecfefdeb43586ed",
"line": undefined,
"name": "std::io::stdio::_print::hdecfefdeb43586ed",
"selfWeight": 0,
"totalWeight": 1000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":_$LT$$RF$std..io..stdio..Stdout$u20$as$u20$std..io..Write$GT$::write_fmt::h2cb06dcd2b172844",
"line": undefined,
"name": "_$LT$$RF$std..io..stdio..Stdout$u20$as$u20$std..io..Write$GT$::write_fmt::h2cb06dcd2b172844",
"selfWeight": 0,
"totalWeight": 1000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":core::fmt::write::hed96bcfc6342aee5",
"line": undefined,
"name": "core::fmt::write::hed96bcfc6342aee5",
"selfWeight": 468,
"totalWeight": 1000,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":_$LT$std..io..Write..write_fmt..Adapter$LT$T$GT$$u20$as$u20$core..fmt..Write$GT$::write_str::hbe33ec23de24ce2f",
"line": undefined,
"name": "_$LT$std..io..Write..write_fmt..Adapter$LT$T$GT$$u20$as$u20$core..fmt..Write$GT$::write_str::hbe33ec23de24ce2f",
"selfWeight": 0,
"totalWeight": 532,
},
Frame {
"col": undefined,
"file": undefined,
"key": ":_$LT$std..io..stdio..StdoutLock$u20$as$u20$std..io..Write$GT$::write_all::h22667d0a03b2151b",
"line": undefined,
"name": "_$LT$std..io..stdio..StdoutLock$u20$as$u20$std..io..Write$GT$::write_all::h22667d0a03b2151b",
"selfWeight": 532,
"totalWeight": 532,
},
],
"name": "cycles-example-deep-copy.txt",
"stacks": Array [
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d;std::time::Instant::elapsed::h2e3793148fc23529;mach_absolute_time 750000:00",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d;std::time::Instant::elapsed::h2e3793148fc23529;std::time::Instant::elapsed::h2e3793148fc23529 10000:00",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d;std::time::Instant::elapsed::h2e3793148fc23529;DYLD-STUB$$mach_absolute_time 16:40",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d;std::time::Instant::elapsed::h2e3793148fc23529 6650:00",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d;core::time::Duration::as_millis::hae6ad1b9cf7bb5ec 10566:40",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d;example_app::example_a::h622d6879b734496d 3016:40",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d;example_app::example_b::h71b31fcd89b9ffd2;std::time::Instant::elapsed::h2e3793148fc23529;mach_absolute_time 1333:20",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d;example_app::example_b::h71b31fcd89b9ffd2;std::time::Instant::elapsed::h2e3793148fc23529;std::time::Instant::elapsed::h2e3793148fc23529 16:40",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d;example_app::example_b::h71b31fcd89b9ffd2;example_app::example_c::h631759c10d7dee93;alloc::vec::Vec$LT$T$C$A$GT$::push::ha3426502ffc42c8b;alloc::raw_vec::RawVec$LT$T$C$A$GT$::reserve_for_push::he4a29b8274e35dbb;alloc::raw_vec::RawVec$LT$T$C$A$GT$::grow_amortized::h78db9ba423623c58;alloc::raw_vec::finish_grow::h876f5af1c66d74c8;_$LT$alloc..alloc..Global$u20$as$u20$core..alloc..Allocator$GT$::allocate::hcc733a40a34fbc94;alloc::alloc::Global::alloc_impl::hc55feba7d6266dfa;alloc::alloc::alloc::hfac63f3d6850b759;_malloc_zone_malloc;nanov2_malloc;nanov2_allocate;nanov2_allocate;nanov2_find_block_and_allocate 16:40",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d;example_app::example_b::h71b31fcd89b9ffd2;example_app::example_b::h71b31fcd89b9ffd2 16:40",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d;example_app::example_b::h71b31fcd89b9ffd2;core::time::Duration::as_millis::hae6ad1b9cf7bb5ec 16:40",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::example_a::h622d6879b734496d 1683:20",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;std::time::Instant::elapsed::h2e3793148fc23529;mach_absolute_time 766666:40",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;std::time::Instant::elapsed::h2e3793148fc23529;std::time::Instant::elapsed::h2e3793148fc23529 10816:40",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;std::time::Instant::elapsed::h2e3793148fc23529 5850:00",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;core::time::Duration::as_millis::hae6ad1b9cf7bb5ec 10233:20",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;example_app::main::hddab13ae8d8b6a6e 3166:40",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;std::io::stdio::_print::hdecfefdeb43586ed;_$LT$$RF$std..io..stdio..Stdout$u20$as$u20$std..io..Write$GT$::write_fmt::h2cb06dcd2b172844;core::fmt::write::hed96bcfc6342aee5;_$LT$std..io..Write..write_fmt..Adapter$LT$T$GT$$u20$as$u20$core..fmt..Write$GT$::write_str::hbe33ec23de24ce2f;_$LT$std..io..stdio..StdoutLock$u20$as$u20$std..io..Write$GT$::write_all::h22667d0a03b2151b 532.00ms",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e;std::io::stdio::_print::hdecfefdeb43586ed;_$LT$$RF$std..io..stdio..Stdout$u20$as$u20$std..io..Write$GT$::write_fmt::h2cb06dcd2b172844;core::fmt::write::hed96bcfc6342aee5 468.00ms",
"example_app (40414);start;main;std::rt::lang_start::hdba6f1ebfd1bdcf8;std::rt::lang_start_internal::hc453db0ee48af82e;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd856f2663871206c;std::sys_common::backtrace::__rust_begin_short_backtrace::h4869fd82068bc1dc;core::ops::function::FnOnce::call_once::h06e0c27ee740c6a8;example_app::main::hddab13ae8d8b6a6e 19933:19",
],
}
`;
exports[`importFromInstrumentsDeepCopy allocations profile: indexToView 1`] = `0`;
exports[`importFromInstrumentsDeepCopy allocations profile: indexToView 2`] = `0`;
exports[`importFromInstrumentsDeepCopy allocations profile: profileGroup.name 1`] = `"random-allocations-deep-copy.txt"`;
exports[`importFromInstrumentsDeepCopy allocations profile: profileGroup.name 2`] = `"cycles-example-deep-copy.txt"`;
exports[`importFromInstrumentsDeepCopy time profile 1`] = `
Object {
"frames": Array [
@@ -1376,6 +1376,268 @@ Object {
}
`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt chunked 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "[unknown]",
"key": "[unknown] ([unknown])",
"line": undefined,
"name": "??? ([unknown])",
"selfWeight": 0,
"totalWeight": 0.13823499999125488,
},
Frame {
"col": undefined,
"file": "/lib/x86_64-linux-gnu/libc-2.27.so",
"key": "__libc_start_main (/lib/x86_64-linux-gnu/libc-2.27.so)",
"line": undefined,
"name": "__libc_start_main",
"selfWeight": 0,
"totalWeight": 0.13823499999125488,
},
Frame {
"col": undefined,
"file": "/workdir/simple-terminates",
"key": "main (/workdir/simple-terminates)",
"line": undefined,
"name": "main",
"selfWeight": 0,
"totalWeight": 0.13823499999125488,
},
Frame {
"col": undefined,
"file": "/workdir/simple-terminates",
"key": "_Z5alphav (/workdir/simple-terminates)",
"line": undefined,
"name": "_Z5alphav",
"selfWeight": 0.04059999997843988,
"totalWeight": 0.04059999997843988,
},
Frame {
"col": undefined,
"file": "/workdir/simple-terminates",
"key": "_Z5deltav (/workdir/simple-terminates)",
"line": undefined,
"name": "_Z5deltav",
"selfWeight": 0.023244000098202378,
"totalWeight": 0.07264250004664063,
},
Frame {
"col": undefined,
"file": "/workdir/simple-terminates",
"key": "_Z4betav (/workdir/simple-terminates)",
"line": undefined,
"name": "_Z4betav",
"selfWeight": 0.05354449988226406,
"totalWeight": 0.05447799988905899,
},
Frame {
"col": undefined,
"file": "/workdir/simple-terminates",
"key": "_Z5gammav (/workdir/simple-terminates)",
"line": undefined,
"name": "_Z5gammav",
"selfWeight": 0.01991300002555363,
"totalWeight": 0.01991300002555363,
},
Frame {
"col": undefined,
"file": "[kernel.kallsyms]",
"key": "[unknown] ([kernel.kallsyms])",
"line": undefined,
"name": "??? ([kernel.kallsyms])",
"selfWeight": 0.0009335000067949295,
"totalWeight": 0.0009335000067949295,
},
],
"name": "simple-terminat tid: 9",
"stacks": Array [
"??? ([unknown]);__libc_start_main;main;_Z5alphav 501.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 996.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.03ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 989.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.07ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.04ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav;??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 933.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.46ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.47ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 997.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.04ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.06ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 965.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 2.12ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 925.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.02ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.07ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 987.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 997.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1000.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 997.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 997.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 999.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 921.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.08ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 989.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 974.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 963.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.04ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.04ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 991.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 922.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.02ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.07ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 983.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 999.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 998.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 998.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 974.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.03ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 998.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1000.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 997.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 999.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 997.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 932.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 2.09ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 986.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 998.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 954.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 991.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.05ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 989.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 998.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1000.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 981.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 921.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 2.09ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 995.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 997.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 980.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 476.00µs",
],
}
`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt chunked 2`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "[kernel.kallsyms]",
"key": "[unknown] ([kernel.kallsyms])",
"line": undefined,
"name": "??? ([kernel.kallsyms])",
"selfWeight": 0.1372890000056941,
"totalWeight": 0.1372890000056941,
},
Frame {
"col": undefined,
"file": "[unknown]",
"key": "[unknown] ([unknown])",
"line": undefined,
"name": "??? ([unknown])",
"selfWeight": 0.0009340000106021762,
"totalWeight": 0.0014475000207312405,
},
],
"name": "swapper",
"stacks": Array [
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 17.00µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 9.55ms",
"??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]) 487.50µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 486.00µs",
"??? ([unknown]);??? ([unknown]) 446.50µs",
"??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 34.50µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 38.00µs",
"??? ([kernel.kallsyms]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 479.00µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 6.52ms",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 517.00µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 111.12ms",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 399.50µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 8.13ms",
],
}
`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt chunked: indexToView 1`] = `0`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt chunked: profileGroup.name 1`] = `"system-wide.linux-perf.txt"`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt: indexToView 1`] = `0`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt: profileGroup.name 1`] = `"system-wide.linux-perf.txt"`;
@@ -0,0 +1,196 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importFromPapyrus script profile 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "STACK 3053",
"line": undefined,
"name": "STACK 3053",
"selfWeight": 200,
"totalWeight": 1250,
},
Frame {
"col": undefined,
"file": undefined,
"key": "abc_example_quest..exampleFunction1",
"line": undefined,
"name": "abc_example_quest..exampleFunction1",
"selfWeight": 1050,
"totalWeight": 1050,
},
Frame {
"col": undefined,
"file": undefined,
"key": "abc_example_quest..exampleFunction2",
"line": undefined,
"name": "abc_example_quest..exampleFunction2",
"selfWeight": 50,
"totalWeight": 50,
},
Frame {
"col": undefined,
"file": undefined,
"key": "STACK 3208",
"line": undefined,
"name": "STACK 3208",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": "abc_example_quest..exampleFunction3",
"line": undefined,
"name": "abc_example_quest..exampleFunction3",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": "abc_example_quest..exampleFunction4",
"line": undefined,
"name": "abc_example_quest..exampleFunction4",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": "STACK 3947",
"line": undefined,
"name": "STACK 3947",
"selfWeight": 450,
"totalWeight": 1500,
},
Frame {
"col": undefined,
"file": undefined,
"key": "abc_example_quest..exampleFunction5",
"line": undefined,
"name": "abc_example_quest..exampleFunction5",
"selfWeight": 750,
"totalWeight": 1000,
},
Frame {
"col": undefined,
"file": undefined,
"key": "STACK 3949",
"line": undefined,
"name": "STACK 3949",
"selfWeight": 0,
"totalWeight": 250,
},
Frame {
"col": undefined,
"file": undefined,
"key": "abc_example_quest..exampleFunction6",
"line": undefined,
"name": "abc_example_quest..exampleFunction6",
"selfWeight": 250,
"totalWeight": 250,
},
],
"name": "script.log",
"stacks": Array [
"STACK 3053;abc_example_quest..exampleFunction1 1.00s",
"STACK 3053 200.00ms",
"STACK 3053;abc_example_quest..exampleFunction2 50.00ms",
" 250.00ms",
"STACK 3947;abc_example_quest..exampleFunction5 250.00ms",
"STACK 3947;abc_example_quest..exampleFunction5;STACK 3949;abc_example_quest..exampleFunction6 250.00ms",
"STACK 3947;abc_example_quest..exampleFunction5 500.00ms",
"STACK 3947 450.00ms",
"STACK 3947;abc_example_quest..exampleFunction1 50.00ms",
],
}
`;
exports[`importFromPapyrus script profile: indexToView 1`] = `0`;
exports[`importFromPapyrus script profile: profileGroup.name 1`] = `"script.log"`;
exports[`importFromPapyrus stack profile 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "STACK 3185",
"line": undefined,
"name": "STACK 3185",
"selfWeight": 66,
"totalWeight": 617,
},
Frame {
"col": undefined,
"file": undefined,
"key": "Debug..StartStackProfiling",
"line": undefined,
"name": "Debug..StartStackProfiling",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": "Form..GetFormID",
"line": undefined,
"name": "Form..GetFormID",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": "abc_example_mod_quest..exampleFunction1",
"line": undefined,
"name": "abc_example_mod_quest..exampleFunction1",
"selfWeight": 17,
"totalWeight": 17,
},
Frame {
"col": undefined,
"file": undefined,
"key": "Game..GetModName",
"line": undefined,
"name": "Game..GetModName",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": "Cell..IsInterior",
"line": undefined,
"name": "Cell..IsInterior",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": "utility..WaitMenuMode",
"line": undefined,
"name": "utility..WaitMenuMode",
"selfWeight": 534,
"totalWeight": 534,
},
],
"name": "stack.log",
"stacks": Array [
"STACK 3185 16.00ms",
"STACK 3185;abc_example_mod_quest..exampleFunction1 17.00ms",
"STACK 3185 33.00ms",
"STACK 3185;utility..WaitMenuMode 534.00ms",
"STACK 3185 17.00ms",
],
}
`;
exports[`importFromPapyrus stack profile: indexToView 1`] = `0`;
exports[`importFromPapyrus stack profile: profileGroup.name 1`] = `"stack.log"`;
+66 -66
View File
@@ -7,239 +7,239 @@ Object {
"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,
"line": 198,
"name": "runtime.main",
"selfWeight": 0,
"totalWeight": 136,
"totalWeight": 1360000000,
},
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,
"line": 49,
"name": "main.main",
"selfWeight": 0,
"totalWeight": 136,
"totalWeight": 1360000000,
},
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,
"line": 28,
"name": "main.delta",
"selfWeight": 22,
"totalWeight": 58,
"selfWeight": 220000000,
"totalWeight": 580000000,
},
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,
"line": 18,
"name": "main.beta",
"selfWeight": 39,
"totalWeight": 39,
"selfWeight": 390000000,
"totalWeight": 390000000,
},
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,
"line": 11,
"name": "main.alpha",
"selfWeight": 48,
"totalWeight": 48,
"selfWeight": 480000000,
"totalWeight": 480000000,
},
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,
"line": 34,
"name": "main.gamma",
"selfWeight": 27,
"totalWeight": 27,
"selfWeight": 270000000,
"totalWeight": 270000000,
},
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,
"line": 480,
"name": "runtime.morestack",
"selfWeight": 0,
"totalWeight": 11,
"totalWeight": 110000000,
},
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,
"line": 957,
"name": "runtime.newstack",
"selfWeight": 0,
"totalWeight": 11,
"totalWeight": 110000000,
},
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,
"line": 412,
"name": "runtime.duffcopy",
"selfWeight": 11,
"totalWeight": 11,
"selfWeight": 110000000,
"totalWeight": 110000000,
},
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,
"line": 247,
"name": "runtime.timerproc",
"selfWeight": 0,
"totalWeight": 1,
"totalWeight": 10000000,
},
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,
"line": 125,
"name": "runtime.goroutineReady",
"selfWeight": 0,
"totalWeight": 1,
"totalWeight": 10000000,
},
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,
"line": 301,
"name": "runtime.goready",
"selfWeight": 0,
"totalWeight": 1,
"totalWeight": 10000000,
},
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,
"line": 409,
"name": "runtime.systemstack",
"selfWeight": 0,
"totalWeight": 1,
"totalWeight": 10000000,
},
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,
"line": 302,
"name": "runtime.goready.func1",
"selfWeight": 0,
"totalWeight": 1,
"totalWeight": 10000000,
},
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,
"line": 608,
"name": "runtime.ready",
"selfWeight": 0,
"totalWeight": 1,
"totalWeight": 10000000,
},
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,
"line": 2083,
"name": "runtime.wakep",
"selfWeight": 0,
"totalWeight": 1,
"totalWeight": 10000000,
},
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,
"line": 2017,
"name": "runtime.startm",
"selfWeight": 0,
"totalWeight": 1,
"totalWeight": 10000000,
},
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,
"line": 147,
"name": "runtime.notewakeup",
"selfWeight": 0,
"totalWeight": 1,
"totalWeight": 10000000,
},
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,
"line": 36,
"name": "runtime.semawakeup",
"selfWeight": 0,
"totalWeight": 1,
"totalWeight": 10000000,
},
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,
"line": 465,
"name": "runtime.mach_semrelease",
"selfWeight": 0,
"totalWeight": 1,
"totalWeight": 10000000,
},
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,
"line": 558,
"name": "runtime.mach_semaphore_signal",
"selfWeight": 1,
"totalWeight": 1,
"selfWeight": 10000000,
"totalWeight": 10000000,
},
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,
"line": 1193,
"name": "runtime.mstart",
"selfWeight": 0,
"totalWeight": 2,
"totalWeight": 20000000,
},
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,
"line": 1227,
"name": "runtime.mstart1",
"selfWeight": 0,
"totalWeight": 2,
"totalWeight": 20000000,
},
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,
"line": 4221,
"name": "runtime.sysmon",
"selfWeight": 0,
"totalWeight": 2,
"totalWeight": 20000000,
},
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,
"line": 418,
"name": "runtime.usleep",
"selfWeight": 2,
"totalWeight": 2,
"selfWeight": 20000000,
"totalWeight": 20000000,
},
],
"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",
"runtime.main;main.main;main.delta;main.beta 140.00ms",
"runtime.main;main.main;main.alpha 260.00ms",
"runtime.main;main.main;main.gamma 270.00ms",
"runtime.morestack;runtime.newstack;runtime.duffcopy 110.00ms",
"runtime.main;main.main;main.beta 250.00ms",
"runtime.main;main.main;main.delta 220.00ms",
"runtime.main;main.main;main.delta;main.alpha 220.00ms",
"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 10.00ms",
"runtime.mstart;runtime.mstart1;runtime.sysmon;runtime.usleep 20.00ms",
],
}
`;
+2 -2
View File
@@ -17,7 +17,7 @@ Object {
"file": "__InjectedScript_CommandLineAPIModuleSource.js",
"key": ":__InjectedScript_CommandLineAPIModuleSource.js:2:10",
"line": 2,
"name": "(anonymous)",
"name": "(anonymous __InjectedScript_CommandLineAPIModuleSource.js:2)",
"selfWeight": 0.001,
"totalWeight": 0.001,
},
@@ -78,7 +78,7 @@ Object {
],
"name": "Grabación de Control temporal 1",
"stacks": Array [
"injectModule;(anonymous) 1.00ms",
"injectModule;(anonymous __InjectedScript_CommandLineAPIModuleSource.js:2) 1.00ms",
" 39.93ms",
"(program);alpha;delta;gamma 10.83ms",
" 2.46ms",
@@ -1,5 +1,453 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importCpuProfileWithProperWeights importFromStackprof cpu mode snapshot 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "sample/programs/ruby/simple.rb",
"key": 5352342240,
"line": undefined,
"name": "<main>",
"selfWeight": 0,
"totalWeight": 410000,
},
Frame {
"col": undefined,
"file": "/Users/mcorrea/src/github.com/dalehamel/speedscope/sample/programs/ruby/simple.rb",
"key": 4530376200,
"line": undefined,
"name": "<main>",
"selfWeight": 0,
"totalWeight": 410000,
},
Frame {
"col": undefined,
"file": "/Users/mcorrea/src/github.com/dalehamel/speedscope/sample/programs/ruby/simple.rb",
"key": 4530486680,
"line": 37,
"name": "block in <main>",
"selfWeight": 0,
"totalWeight": 410000,
},
Frame {
"col": undefined,
"file": "/Users/mcorrea/src/github.com/dalehamel/speedscope/sample/programs/ruby/simple.rb",
"key": 5335911080,
"line": 4,
"name": "Object#a",
"selfWeight": 0,
"totalWeight": 410000,
},
Frame {
"col": undefined,
"file": "/Users/mcorrea/src/github.com/dalehamel/speedscope/sample/programs/ruby/simple.rb",
"key": 5335911040,
"line": 12,
"name": "Object#b",
"selfWeight": 0,
"totalWeight": 154000,
},
Frame {
"col": undefined,
"file": "/Users/mcorrea/src/github.com/dalehamel/speedscope/sample/programs/ruby/simple.rb",
"key": 5335909920,
"line": 24,
"name": "Object#d",
"selfWeight": 331000,
"totalWeight": 331000,
},
Frame {
"col": undefined,
"file": "/Users/mcorrea/src/github.com/dalehamel/speedscope/sample/programs/ruby/simple.rb",
"key": 5335910560,
"line": 18,
"name": "Object#c",
"selfWeight": 0,
"totalWeight": 177000,
},
Frame {
"col": undefined,
"file": "/Users/mcorrea/src/github.com/dalehamel/speedscope/sample/programs/ruby/simple.rb",
"key": 5335909880,
"line": 32,
"name": "Object#e",
"selfWeight": 79000,
"totalWeight": 79000,
},
Frame {
"col": undefined,
"file": "",
"key": 1,
"line": undefined,
"name": "(garbage collection)",
"selfWeight": 0,
"totalWeight": 79000,
},
Frame {
"col": undefined,
"file": "",
"key": 5,
"line": undefined,
"name": "(sweeping)",
"selfWeight": 71000,
"totalWeight": 71000,
},
Frame {
"col": undefined,
"file": "",
"key": 3,
"line": undefined,
"name": "(marking)",
"selfWeight": 8000,
"totalWeight": 8000,
},
],
"name": "simple-cpu-stackprof.json",
"stacks": Array [
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 5.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(marking) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"(garbage collection);(marking) 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"(garbage collection);(sweeping) 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"(garbage collection);(marking) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"(garbage collection);(sweeping) 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 4.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 4.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"(garbage collection);(marking) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 4.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 3.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 5.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"(garbage collection);(sweeping) 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(marking) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(marking) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 4.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 4.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 4.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 3.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"(garbage collection);(sweeping) 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(marking) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 5.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 4.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 4.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(marking) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 5.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 1.00ms",
"(garbage collection);(sweeping) 2.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 4.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#e 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 4.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 2.00ms",
"(garbage collection);(sweeping) 1.00ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#c;Object#c;Object#d;Object#d 3.00ms",
],
}
`;
exports[`importCpuProfileWithProperWeights importFromStackprof cpu mode snapshot: indexToView 1`] = `0`;
exports[`importCpuProfileWithProperWeights importFromStackprof cpu mode snapshot: profileGroup.name 1`] = `"simple-cpu-stackprof.json"`;
exports[`importFromStackprof 1`] = `
Object {
"frames": Array [
@@ -851,6 +1299,396 @@ Object {
}
`;
exports[`importFromStackprof object mode 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "../../alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4317728280,
"line": undefined,
"name": "<main>",
"selfWeight": 0,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4376547240,
"line": undefined,
"name": "<main>",
"selfWeight": 0,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4379035920,
"line": null,
"name": "StackProf.run",
"selfWeight": 0,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4317464320,
"line": 21,
"name": "block in <main>",
"selfWeight": 1,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4379033920,
"line": 4,
"name": "Object#a",
"selfWeight": 2,
"totalWeight": 102,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4317869400,
"line": null,
"name": "Range#each",
"selfWeight": 0,
"totalWeight": 102,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4379033880,
"line": 11,
"name": "Object#b",
"selfWeight": 1,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4318054440,
"line": null,
"name": "Class#new",
"selfWeight": 4,
"totalWeight": 4,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4379033840,
"line": 15,
"name": "Object#c",
"selfWeight": 2,
"totalWeight": 95,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4317868920,
"line": null,
"name": "Range#to_a",
"selfWeight": 1,
"totalWeight": 37,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4379016640,
"line": null,
"name": "Enumerable#to_a",
"selfWeight": 36,
"totalWeight": 36,
},
Frame {
"col": undefined,
"file": "<internal:array>",
"key": 4317563560,
"line": 60,
"name": "Array#sample",
"selfWeight": 20,
"totalWeight": 20,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4317922760,
"line": null,
"name": "Array#sort",
"selfWeight": 36,
"totalWeight": 36,
},
],
"name": "object-stackprof.json",
"stacks": Array [
"<main>;<main>;StackProf.run;block in <main> 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b;Class#new 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 3",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b;Class#new 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b;Class#new 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
],
}
`;
exports[`importFromStackprof object mode: indexToView 1`] = `0`;
exports[`importFromStackprof object mode: profileGroup.name 1`] = `"object-stackprof.json"`;
exports[`importFromStackprof when a profile has a frame with no name 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "../../alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4317728280,
"line": undefined,
"name": "<main>",
"selfWeight": 0,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4376547240,
"line": undefined,
"name": "<main>",
"selfWeight": 0,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4379035920,
"line": null,
"name": "StackProf.run",
"selfWeight": 0,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4317464320,
"line": 21,
"name": "block in <main>",
"selfWeight": 1,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4379033920,
"line": 4,
"name": "Object#a",
"selfWeight": 2,
"totalWeight": 102,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4317869400,
"line": null,
"name": "Range#each",
"selfWeight": 0,
"totalWeight": 102,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4379033880,
"line": 11,
"name": "Object#b",
"selfWeight": 1,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4318054440,
"line": null,
"name": "Class#new",
"selfWeight": 4,
"totalWeight": 4,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4379033840,
"line": 15,
"name": "Object#c",
"selfWeight": 2,
"totalWeight": 95,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4317868920,
"line": null,
"name": "Range#to_a",
"selfWeight": 1,
"totalWeight": 37,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4379016640,
"line": null,
"name": "Enumerable#to_a",
"selfWeight": 36,
"totalWeight": 36,
},
Frame {
"col": undefined,
"file": "<internal:array>",
"key": 4317563560,
"line": 60,
"name": "Array#sample",
"selfWeight": 20,
"totalWeight": 20,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4317922760,
"line": null,
"name": "(unknown)",
"selfWeight": 36,
"totalWeight": 36,
},
],
"name": "stackprof-last-frame-no-name.json",
"stacks": Array [
"<main>;<main>;StackProf.run;block in <main> 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b;Class#new 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 3",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b;Class#new 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b;Class#new 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;(unknown) 2",
],
}
`;
exports[`importFromStackprof when a profile has a frame with no name: indexToView 1`] = `0`;
exports[`importFromStackprof when a profile has a frame with no name: profileGroup.name 1`] = `"stackprof-last-frame-no-name.json"`;
exports[`importFromStackprof: indexToView 1`] = `0`;
exports[`importFromStackprof: profileGroup.name 1`] = `"simple-stackprof.json"`;
@@ -172,6 +172,68 @@ exports[`importTraceEvents bad E events: indexToView 1`] = `0`;
exports[`importTraceEvents bad E events: profileGroup.name 1`] = `"too-many-end-events.json"`;
exports[`importTraceEvents different number of start and end calls to same function at same timestamp 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "anonymous",
"line": undefined,
"name": "anonymous",
"selfWeight": 1,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "anonymous {\\"parent\\":1}",
"line": undefined,
"name": "anonymous {\\"parent\\":1}",
"selfWeight": 0,
"totalWeight": 4,
},
Frame {
"col": undefined,
"file": undefined,
"key": "function1 {\\"parent\\":2}",
"line": undefined,
"name": "function1 {\\"parent\\":2}",
"selfWeight": 0,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "anonymous {\\"parent\\":3}",
"line": undefined,
"name": "anonymous {\\"parent\\":3}",
"selfWeight": 2,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "anonymous {\\"parent\\":8}",
"line": undefined,
"name": "anonymous {\\"parent\\":8}",
"selfWeight": 2,
"totalWeight": 2,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"anonymous 1.00µs",
"anonymous;anonymous {\\"parent\\":1};function1 {\\"parent\\":2};anonymous {\\"parent\\":3} 2.00µs",
"anonymous;anonymous {\\"parent\\":1};anonymous {\\"parent\\":8} 2.00µs",
],
}
`;
exports[`importTraceEvents different number of start and end calls to same function at same timestamp: indexToView 1`] = `0`;
exports[`importTraceEvents different number of start and end calls to same function at same timestamp: profileGroup.name 1`] = `"simultaneous-anonymous-calls.json"`;
exports[`importTraceEvents end event with empty stack 1`] = `
Object {
"frames": Array [
@@ -468,6 +530,81 @@ exports[`importTraceEvents mismatched name: indexToView 1`] = `0`;
exports[`importTraceEvents mismatched name: profileGroup.name 1`] = `"mismatched-name.json"`;
exports[`importTraceEvents multi-thread profile with samples 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "[root]:root",
"line": undefined,
"name": "[root]",
"selfWeight": 20770,
"totalWeight": 34096,
},
Frame {
"col": undefined,
"file": undefined,
"key": "function1:JavaScript",
"line": undefined,
"name": "function1",
"selfWeight": 13326,
"totalWeight": 13326,
},
],
"name": "hermes (pid 7512), mqt_js (tid 1)",
"stacks": Array [
"[root] 20.77ms",
"[root];function1 13.33ms",
],
}
`;
exports[`importTraceEvents multi-thread profile with samples 2`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "[root thread 2]:root",
"line": undefined,
"name": "[root thread 2]",
"selfWeight": 30770,
"totalWeight": 54096,
},
Frame {
"col": undefined,
"file": undefined,
"key": "function3:JavaScript",
"line": undefined,
"name": "function3",
"selfWeight": 13326,
"totalWeight": 13326,
},
Frame {
"col": undefined,
"file": undefined,
"key": "function4:Metadata",
"line": undefined,
"name": "function4",
"selfWeight": 10000,
"totalWeight": 10000,
},
],
"name": "hermes (pid 7512, tid 2)",
"stacks": Array [
"[root thread 2] 20.77ms",
"[root thread 2];function3 13.33ms",
"[root thread 2];function4 10.00ms",
"[root thread 2] 10.00ms",
],
}
`;
exports[`importTraceEvents multi-thread profile with samples: indexToView 1`] = `0`;
exports[`importTraceEvents multi-thread profile with samples: profileGroup.name 1`] = `"multi-thread-with-samples.json"`;
exports[`importTraceEvents multiprocess 1`] = `
Object {
"frames": Array [
@@ -842,6 +979,62 @@ Object {
}
`;
exports[`importTraceEvents partial json import chunked 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 3,
"totalWeight": 12,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma {\\"detail\\":\\"foobar\\"}",
"line": undefined,
"name": "gamma {\\"detail\\":\\"foobar\\"}",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "epsilon",
"line": undefined,
"name": "epsilon",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma {\\"detail\\":\\"foobar\\"} 5.00µs",
"alpha;beta;epsilon 4.00µs",
"alpha;beta 2.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents partial json import chunked: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import chunked: profileGroup.name 1`] = `"simple-partial.json"`;
exports[`importTraceEvents partial json import trailing comma 1`] = `
Object {
"frames": Array [
@@ -894,6 +1087,62 @@ Object {
}
`;
exports[`importTraceEvents partial json import trailing comma chunked 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 3,
"totalWeight": 12,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma {\\"detail\\":\\"foobar\\"}",
"line": undefined,
"name": "gamma {\\"detail\\":\\"foobar\\"}",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "epsilon",
"line": undefined,
"name": "epsilon",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma {\\"detail\\":\\"foobar\\"} 5.00µs",
"alpha;beta;epsilon 4.00µs",
"alpha;beta 2.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents partial json import trailing comma chunked: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import trailing comma chunked: profileGroup.name 1`] = `"simple-partial-trailing-comma.json"`;
exports[`importTraceEvents partial json import trailing comma: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import trailing comma: profileGroup.name 1`] = `"simple-partial-trailing-comma.json"`;
@@ -950,6 +1199,62 @@ Object {
}
`;
exports[`importTraceEvents partial json import whitespace padding chunked 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 3,
"totalWeight": 12,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma {\\"detail\\":\\"foobar\\"}",
"line": undefined,
"name": "gamma {\\"detail\\":\\"foobar\\"}",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "epsilon",
"line": undefined,
"name": "epsilon",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma {\\"detail\\":\\"foobar\\"} 5.00µs",
"alpha;beta;epsilon 4.00µs",
"alpha;beta 2.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents partial json import whitespace padding chunked: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import whitespace padding chunked: profileGroup.name 1`] = `"simple-partial-whitespace.json"`;
exports[`importTraceEvents partial json import whitespace padding: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import whitespace padding: profileGroup.name 1`] = `"simple-partial-whitespace.json"`;
@@ -1056,6 +1361,70 @@ exports[`importTraceEvents simple object: indexToView 1`] = `0`;
exports[`importTraceEvents simple object: profileGroup.name 1`] = `"simple-object.json"`;
exports[`importTraceEvents simple profile with samples 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "[root]:root",
"line": undefined,
"name": "[root]",
"selfWeight": 30779,
"totalWeight": 64710,
},
Frame {
"col": undefined,
"file": undefined,
"key": "function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1):JavaScript",
"line": undefined,
"name": "function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1)",
"selfWeight": 13326,
"totalWeight": 13326,
},
Frame {
"col": undefined,
"file": undefined,
"key": "function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4):JavaScript",
"line": undefined,
"name": "function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4)",
"selfWeight": 0,
"totalWeight": 10605,
},
Frame {
"col": undefined,
"file": undefined,
"key": "function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3):JavaScript",
"line": undefined,
"name": "function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3)",
"selfWeight": 10605,
"totalWeight": 10605,
},
Frame {
"col": undefined,
"file": undefined,
"key": "[GC Young Gen]:Metadata",
"line": undefined,
"name": "[GC Young Gen]",
"selfWeight": 10000,
"totalWeight": 10000,
},
],
"name": "hermes (pid 7512), mqt_js (tid 7695)",
"stacks": Array [
"[root] 20.77ms",
"[root];function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1) 13.33ms",
"[root];function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4);function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3) 10.61ms",
"[root] 10.01ms",
"[root];[GC Young Gen] 10.00ms",
],
}
`;
exports[`importTraceEvents simple profile with samples: indexToView 1`] = `0`;
exports[`importTraceEvents simple profile with samples: profileGroup.name 1`] = `"simple-with-samples.json"`;
exports[`importTraceEvents simple: indexToView 1`] = `0`;
exports[`importTraceEvents simple: profileGroup.name 1`] = `"simple.json"`;
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -8,7 +8,7 @@ Object {
"file": "bootstrap_node.js",
"key": " bootstrap_node.js:10:10",
"line": 10,
"name": "(anonymous)",
"name": "(anonymous bootstrap_node.js:10)",
"selfWeight": 0,
"totalWeight": 116424,
},
@@ -53,7 +53,7 @@ Object {
"file": "util.js",
"key": " util.js:1:11",
"line": 1,
"name": "(anonymous)",
"name": "(anonymous util.js:1)",
"selfWeight": 0,
"totalWeight": 29385,
},
@@ -62,7 +62,7 @@ Object {
"file": "internal/encoding.js",
"key": " internal/encoding.js:1:11",
"line": 1,
"name": "(anonymous)",
"name": "(anonymous encoding.js:1)",
"selfWeight": 0,
"totalWeight": 29385,
},
@@ -107,7 +107,7 @@ Object {
"file": "module.js",
"key": " module.js:1:11",
"line": 1,
"name": "(anonymous)",
"name": "(anonymous module.js:1)",
"selfWeight": 0,
"totalWeight": 22888,
},
@@ -116,7 +116,7 @@ Object {
"file": "fs.js",
"key": " fs.js:1:11",
"line": 1,
"name": "(anonymous)",
"name": "(anonymous fs.js:1)",
"selfWeight": 0,
"totalWeight": 22888,
},
@@ -197,7 +197,7 @@ Object {
"file": "/Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": " /Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:1:11",
"line": 1,
"name": "(anonymous)",
"name": "(anonymous simple.js:1)",
"selfWeight": 0,
"totalWeight": 64151,
},
@@ -240,22 +240,22 @@ Object {
],
"name": "simple.v8log.json",
"stacks": Array [
"(anonymous);startup;setupGlobalVariables;NativeModule.require;NativeModule.compile;(anonymous);NativeModule.require;NativeModule.compile;(anonymous);(c++) v8::internal::Runtime_CreateArrayLiteral;(c++) v8::internal::JSFunction::EnsureHasInitialMap 29.38ms",
"(anonymous);startup;setupGlobalConsole;setupInspectorCommandLineAPI;NativeModule.require;NativeModule.compile;(anonymous);NativeModule.require;NativeModule.compile;(anonymous);(c++) v8::internal::Runtime_StoreIC_Miss;(c++) v8::internal::Map::RawCopy 22.89ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 37.52ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.28ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 2.55ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.27ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 1.28ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.27ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 1.29ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.26ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 1.31ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 2.52ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c 1.27ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b 3.62ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c 1.28ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b 6.42ms",
"(anonymous bootstrap_node.js:10);startup;setupGlobalVariables;NativeModule.require;NativeModule.compile;(anonymous util.js:1);NativeModule.require;NativeModule.compile;(anonymous encoding.js:1);(c++) v8::internal::Runtime_CreateArrayLiteral;(c++) v8::internal::JSFunction::EnsureHasInitialMap 29.38ms",
"(anonymous bootstrap_node.js:10);startup;setupGlobalConsole;setupInspectorCommandLineAPI;NativeModule.require;NativeModule.compile;(anonymous module.js:1);NativeModule.require;NativeModule.compile;(anonymous fs.js:1);(c++) v8::internal::Runtime_StoreIC_Miss;(c++) v8::internal::Map::RawCopy 22.89ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b;d 37.52ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c;d 1.28ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b;d 2.55ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c;d 1.27ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b;d 1.28ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c;d 1.27ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b;d 1.29ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c;d 1.26ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b;d 1.31ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c;d 2.52ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c 1.27ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b 3.62ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c 1.28ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b 6.42ms",
],
}
`;
+4
View File
@@ -15,3 +15,7 @@ test('importFromBGFlameGraph with UTF-16, Little Endian', async () => {
test('importFromBGFlameGraph with UTF-16, Big Endian', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-utf16-be.txt')
})
test('importFromBGFlameGraph with invalid lines', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-with-invalids.txt')
})
+11 -5
View File
@@ -1,25 +1,31 @@
// https://github.com/brendangregg/FlameGraph#2-fold-stacks
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
import {TextFileContent} from './utils'
interface BGSample {
stack: FrameInfo[]
duration: number
}
function parseBGFoldedStacks(contents: string): BGSample[] {
function parseBGFoldedStacks(contents: TextFileContent): BGSample[] {
const samples: BGSample[] = []
contents.replace(/^(.*) (\d+)$/gm, (match: string, stack: string, n: string) => {
for (const line of contents.splitLines()) {
const match = /^(.*) (\d+)$/gm.exec(line)
if (!match) continue
const stack = match[1]
const n = match[2]
samples.push({
stack: stack.split(';').map(name => ({key: name, name: name})),
duration: parseInt(n, 10),
})
return match
})
}
return samples
}
export function importFromBGFlameGraph(contents: string): Profile | null {
export function importFromBGFlameGraph(contents: TextFileContent): Profile | null {
const parsed = parseBGFoldedStacks(contents)
const duration = parsed.reduce((prev: number, cur: BGSample) => prev + cur.duration, 0)
const profile = new StackListProfileBuilder(duration)
+8
View File
@@ -11,3 +11,11 @@ test('importFromCallgrind name compression', async () => {
test('importFromCallgrind multiple event types', async () => {
await checkProfileSnapshot('./sample/profiles/callgrind/callgrind.multiple-event-types.log')
})
test('importFromCallgrind subposition compression', async () => {
await checkProfileSnapshot('./sample/profiles/callgrind/callgrind.subposition-compression.log')
})
test('importFromCallgrind cfn reset', async () => {
await checkProfileSnapshot('./sample/profiles/callgrind/callgrind.cfn-reset.log')
})
+179 -59
View File
@@ -88,13 +88,17 @@
import {CallTreeProfileBuilder, Frame, FrameInfo, Profile, ProfileGroup} from '../lib/profile'
import {getOrElse, getOrInsert, KeyedSet} from '../lib/utils'
import {ByteFormatter, TimeFormatter} from '../lib/value-formatters'
import {TextFileContent} from './utils'
class CallGraph {
private frameSet = new KeyedSet<Frame>()
private totalWeights = new Map<Frame, number>()
private childrenTotalWeights = new Map<Frame, Map<Frame, number>>()
constructor(private fileName: string, private fieldName: string) {}
constructor(
private fileName: string,
private fieldName: string,
) {}
private getOrInsertFrame(info: FrameInfo): Frame {
return Frame.getOrInsert(this.frameSet, info)
@@ -128,26 +132,6 @@ class CallGraph {
}
toProfile(): Profile {
// To convert a call graph into a profile, we first need to identify what
// the "root weights" are. "root weights" are the total weight of each frame
// while at the bottom of the call-stack. The majority of functions will have
// zero weight while at the bottom of the call-stack, since most functions
// are never at the bottom of the call-stack.
const rootWeights = new Map<Frame, number>()
for (let [frame, totalWeight] of this.totalWeights) {
rootWeights.set(frame, totalWeight)
}
for (let [_, childMap] of this.childrenTotalWeights) {
for (let [child, weight] of childMap) {
rootWeights.set(child, getOrElse(rootWeights, child, () => weight) - weight)
}
}
let totalProfileWeight = 0
for (let [_, rootWeight] of rootWeights) {
totalProfileWeight += rootWeight
}
const profile = new CallTreeProfileBuilder()
let unitMultiplier = 1
@@ -169,7 +153,12 @@ class CallGraph {
const currentStack = new Set<Frame>()
const visit = (frame: Frame, callTreeWeight: number) => {
let maxWeight = 0
for (let [_, totalWeight] of this.totalWeights) {
maxWeight = Math.max(maxWeight, totalWeight)
}
const visit = (frame: Frame, subtreeTotalWeight: number) => {
if (currentStack.has(frame)) {
// Call-graphs are allowed to have cycles. Call-trees are not. In case
// we run into a cycle, we'll just avoid recursing into the same subtree
@@ -197,7 +186,7 @@ class CallGraph {
// See the comment at the top of the file for an example where this
// assumption can yield especially misleading results.
if (callTreeWeight < 1e-4 * totalProfileWeight) {
if (subtreeTotalWeight < 1e-4 * maxWeight) {
// This assumption about even distribution can cause us to generate a
// call tree with dramatically more nodes than the call graph.
//
@@ -219,45 +208,113 @@ class CallGraph {
// 1000*250=250,000 nodes in the resulting call graph.
//
// To mitigate this explosion of the # of nodes, we ignore subtrees
// whose weights are less than 0.01% of the total weight of the profile.
// whose weights are less than 0.01% of the heaviest node in the call
// graph.
return
}
// totalWeightForFrame is the total weight for the given frame in the
// entire call graph.
const callGraphWeightForFrame = getOrElse(this.totalWeights, frame, () => 0)
if (callGraphWeightForFrame === 0) {
const totalWeightForFrameInCallgraph = getOrElse(this.totalWeights, frame, () => 0)
if (totalWeightForFrameInCallgraph === 0) {
return
}
// This is the portion of the total time the given child spends within the
// given parent that we'll attribute to this specific path in the call
// tree.
const ratio = callTreeWeight / callGraphWeightForFrame
let selfWeightForNodeInCallTree = subtreeTotalWeight
let selfWeightForFrame = callGraphWeightForFrame
profile.enterFrame(frame, totalCumulative * unitMultiplier)
profile.enterFrame(frame, Math.round(totalCumulative * unitMultiplier))
currentStack.add(frame)
for (let [child, callGraphEdgeWeight] of this.childrenTotalWeights.get(frame) || []) {
selfWeightForFrame -= callGraphEdgeWeight
const childCallTreeWeight = callGraphEdgeWeight * ratio
for (let [child, totalWeightAsChild] of this.childrenTotalWeights.get(frame) || []) {
// To determine the weight of the child in the call tree, we look at the
// weight of the child in the call graph relative to its parent.
const childCallTreeWeight =
subtreeTotalWeight * (totalWeightAsChild / totalWeightForFrameInCallgraph)
let prevTotalCumulative = totalCumulative
visit(child, childCallTreeWeight)
// Even though we tried to add a child with total weight equal to
// childCallTreeWeight, we might have failed for a variety of data
// consistency reasons, or due to cycles.
//
// We want to avoid losing weight in the call tree by subtracting from
// the self weight on the assumption it was added to the subtree, so we
// only subtree from the self weight the amount that was *actually* used
// by the subtree, rather than the amount we *intended* for it to use.
const actualChildCallTreeWeight = totalCumulative - prevTotalCumulative
selfWeightForNodeInCallTree -= actualChildCallTreeWeight
}
currentStack.delete(frame)
totalCumulative += selfWeightForFrame * ratio
profile.leaveFrame(frame, totalCumulative * unitMultiplier)
totalCumulative += selfWeightForNodeInCallTree
profile.leaveFrame(frame, Math.round(totalCumulative * unitMultiplier))
}
for (let [rootFrame, rootWeight] of rootWeights) {
if (rootWeight <= 0) {
continue
// It's surprisingly hard to figure out which nodes in the call graph
// constitute the root nodes of call trees.
//
// Here are a few intuitive options, and reasons why they're not always
// correct or good.
//
// ## 1. Find nodes in the call graph that have no callers
//
// This is probably right 99% of the time in practice, but since the
// callgrind is totally general, it's totally valid to have a file
// representing a profile for the following code:
//
// function a() {
// b()
// }
// function b() {
// }
// a()
// b()
//
// In this case, even though b has a caller, some of the real calltree for
// an execution trace of the program will have b on the top of the stack.
//
// ## 2. Find nodes in the call graph that still have weight if you
// subtract all of the weight caused by callers.
//
// The callgraph format, in theory, provides inclusive times for every
// function call. That means if you have a function `alpha` with a total
// weight of 20, and its only in-edge in the call-graph has weight of 10,
// that should indicate that `alpha` exists both as the root-node of a
// calltree, and as a node in some other call-tree.
//
// In theory, you should be able to figure out the weight of it as a root
// node by subtracting the weights of all the in-edges. In practice, real
// callgrind files are inconsistent in how they do accounting for in-edges
// where you end up in weird situations where the weight of in-edges
// *exceeds* the weight of nodes (where the weight of a node is its
// self-weight plus the weight of all its out-edges).
//
// ## 3. Find the heaviest node in the call graph, build its call-tree, and
// decrease the weights of other nodes in the call graph while you
// build the call tree. After you've done this, repeat with the new
// heaviest.
//
// I think this version is probably fully correct, but the performance is
// awful. The naive-version is O(n^2) because you have to re-determine which
// node is the heaviest after each time you finish building a call-tree. You
// can't just sort, because the relative ordering also changes with the
// construction of each call tree.
//
// There's probably a clever solution here which puts all of the nodes into
// a min-heap and then deletes and re-inserts nodes as their weights change,
// but reasoning about the performance of that is a big pain in the butt.
//
// Despite not always being correct, I'm opting for option (1).
const rootNodes = new Set<Frame>(this.frameSet)
for (let [_, childMap] of this.childrenTotalWeights) {
for (let [child, _] of childMap) {
rootNodes.delete(child)
}
// If we've reached here, it means that the given root frame has some
// weight while at the top of the call-stack.
visit(rootFrame, rootWeight)
}
for (let rootNode of rootNodes) {
visit(rootNode, this.totalWeights.get(rootNode)!)
}
return profile.build()
@@ -291,8 +348,11 @@ class CallgrindParser {
private savedFileNames: {[id: string]: string} = {}
private savedFunctionNames: {[id: string]: string} = {}
constructor(contents: string, private importedFileName: string) {
this.lines = contents.split('\n')
constructor(
contents: TextFileContent,
private importedFileName: string,
) {
this.lines = [...contents.splitLines()]
this.lineNum = 0
}
@@ -343,7 +403,7 @@ class CallgrindParser {
}
private calleeFrameInfo(): FrameInfo {
const file = this.calleeFilename || '(unknown)'
const file = this.calleeFilename || this.filename || '(unknown)'
const name = this.calleeFunctionName || '(unknown)'
const key = `${file}:${name}`
return {key, name, file}
@@ -383,10 +443,20 @@ class CallgrindParser {
switch (key) {
case 'fe':
case 'fi':
case 'fi': {
// fe/fi are used to indicate the source-file of a function definition
// changed mid-definition. This is for inlined code, but doesn't
// indicate that we've actually switched to referring to a different
// function, so we mostly ignore it.
//
// We still need to do the parseNameWithCompression call in case a name
// is defined and then referenced later on for name compression.
this.parseNameWithCompression(value, this.savedFileNames)
break
}
case 'fl': {
this.filename = this.parseNameWithCompression(value, this.savedFileNames)
this.calleeFilename = this.filename
break
}
@@ -397,6 +467,8 @@ class CallgrindParser {
case 'cfi':
case 'cfl': {
// NOTE: unlike the fe/fi distinction described above, cfi and cfl are
// interchangeable.
this.calleeFilename = this.parseNameWithCompression(value, this.savedFileNames)
break
}
@@ -412,6 +484,22 @@ class CallgrindParser {
// since it'll just be copying the exact same frame over-and-over again,
// but that might be better than ignoring it.
this.parseCostLine(this.lines[this.lineNum++], 'child')
// This isn't specified anywhere in the spec, but empirically the and
// "cfn" scope should only persist for a single "call".
//
// This seems to be what KCacheGrind does too:
//
// https://github.com/KDE/kcachegrind/blob/ea4314db2785cb8f279fe884ee7f82445642b692/libcore/cachegrindloader.cpp#L1259
this.calleeFilename = null
this.calleeFunctionName = null
break
}
case 'cob':
case 'ob': {
// We ignore these for now. They're valid lines, but we don't capture or
// display information about them.
break
}
@@ -457,21 +545,52 @@ class CallgrindParser {
return name
}
private prevCostLineNumbers: number[] = []
private parseCostLine(line: string, costType: 'self' | 'child'): boolean {
// TODO(jlfwong): Handle "Subposition compression"
// TODO(jlfwong): Allow hexadecimal encoding
const parts = line.split(/\s+/)
const nums: number[] = []
for (let part of parts) {
// As far as I can tell from the specification, the callgrind format does
// not accept floating point numbers.
const asNum = parseInt(part)
if (isNaN(asNum)) {
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
if (part.length === 0) {
return false
}
nums.push(asNum)
if (part === '*' || part[0] === '-' || part[1] === '+') {
// This handles "Subposition compression"
// See: https://valgrind.org/docs/manual/cl-format.html#cl-format.overview.compression2
if (this.prevCostLineNumbers.length <= i) {
throw new Error(
`Line ${this.lineNum} has a subposition on column ${i} but ` +
`previous cost line has only ${this.prevCostLineNumbers.length} ` +
`columns. Line contents: ${line}`,
)
}
const prevCostForSubposition = this.prevCostLineNumbers[i]
if (part === '*') {
nums.push(prevCostForSubposition)
} else {
// This handles both the '-' and '+' cases
const offset = parseInt(part)
if (isNaN(offset)) {
throw new Error(
`Line ${this.lineNum} has a subposition on column ${i} but ` +
`the offset is not a number. Line contents: ${line}`,
)
}
nums.push(prevCostForSubposition + offset)
}
} else {
const asNum = parseInt(part)
if (isNaN(asNum)) {
return false
}
nums.push(asNum)
}
}
if (nums.length == 0) {
@@ -505,12 +624,13 @@ class CallgrindParser {
}
}
this.prevCostLineNumbers = nums
return true
}
}
export function importFromCallgrind(
contents: string,
contents: TextFileContent,
importedFileName: string,
): ProfileGroup | null {
return new CallgrindParser(contents, importedFileName).parse()
+11
View File
@@ -1,4 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
import {withMockedFileChunkSizeForTests} from './utils'
test('importFromChromeCPUProfile', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/65/simple.cpuprofile')
@@ -8,6 +9,12 @@ test('importFromChromeTimeline', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/65/simple-timeline.json')
})
test('importFromChromeTimeline chunked', async () => {
await withMockedFileChunkSizeForTests(100, async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/65/simple-timeline.json')
})
})
test('importFromChromeTimeline Chrome 69', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/69/simple.json')
})
@@ -23,3 +30,7 @@ test('importFromChromeTimeline Workers Chrome 66', async () => {
test('importFromChromeTimeline Workers Chrome 70', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/70/worker.json')
})
test('importFromChromeTimeline Chrome 116', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/116/Trace-20230603T221323.json')
})
+13 -2
View File
@@ -49,7 +49,7 @@ export interface CPUProfile {
timeDeltas: number[]
}
export function isChromeTimeline(rawProfile: any): boolean {
export function isChromeTimeline(rawProfile: any): rawProfile is TimelineEvent[] {
if (!Array.isArray(rawProfile)) return false
if (rawProfile.length < 1) return false
const first = rawProfile[0]
@@ -63,6 +63,14 @@ export function isChromeTimeline(rawProfile: any): boolean {
return true
}
export function isChromeTimelineObject(
rawProfile: any,
): rawProfile is {traceEvents: TimelineEvent[]} {
// Starting with Chrome 114, the timeline format output by devtools is an object
if (!('traceEvents' in rawProfile)) return false
return isChromeTimeline(rawProfile.traceEvents)
}
export function importFromChromeTimeline(events: TimelineEvent[], fileName: string): ProfileGroup {
// It seems like sometimes Chrome timeline files contain multiple CpuProfiles?
// For now, choose the first one in the list.
@@ -168,7 +176,6 @@ export function importFromChromeTimeline(events: TimelineEvent[], fileName: stri
const callFrameToFrameInfo = new Map<CPUProfileCallFrame, FrameInfo>()
function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
return getOrInsert(callFrameToFrameInfo, callFrame, callFrame => {
const name = callFrame.functionName || '(anonymous)'
const file = callFrame.url
// In Chrome profiles, line numbers & column numbers are both 0-indexed.
@@ -181,6 +188,10 @@ function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
let col = callFrame.columnNumber
if (col != null) col++
const name =
callFrame.functionName ||
(file ? `(anonymous ${file.split('/').pop()}:${line})` : '(anonymous)')
return {
key: `${name}:${file}:${line}:${col}`,
name,
-17
View File
@@ -1,17 +0,0 @@
// The bits of this API that we care about. This is implemented by WebKitEntry
// https://wicg.github.io/entries-api/#api-entry
export interface FileSystemDirectoryReader {
readEntries(cb: (entries: FileSystemEntry[]) => void, error: (err: Error) => void): void
}
export interface FileSystemEntry {
readonly isFile: boolean
readonly isDirectory: boolean
readonly name: string
readonly fullPath: string
}
export interface FileSystemDirectoryEntry extends FileSystemEntry {
createReader(): FileSystemDirectoryReader
}
export interface FileSystemFileEntry extends FileSystemEntry {
file(cb: (file: File) => void, errCb: (err: Error) => void): void
}
+30 -40
View File
@@ -1,11 +1,11 @@
import {Profile, ProfileGroup} from '../lib/profile'
import {FileSystemDirectoryEntry} from './file-system-entry'
import {
importFromChromeCPUProfile,
importFromChromeTimeline,
isChromeTimeline,
importFromOldV8CPUProfile,
isChromeTimelineObject,
} from './chrome'
import {importFromStackprof} from './stackprof'
import {importFromInstrumentsDeepCopy, importFromInstrumentsTrace} from './instruments'
@@ -22,6 +22,7 @@ import {decodeBase64} from '../lib/utils'
import {importFromChromeHeapProfile} from './v8heapalloc'
import {isTraceEventFormatted, importTraceEvents} from './trace-event'
import {importFromCallgrind} from './callgrind'
import {importFromPapyrus} from './papyrus'
export async function importProfileGroupFromText(
fileName: string,
@@ -73,26 +74,6 @@ function toGroup(profile: Profile | null): ProfileGroup | null {
return {name: profile.getName(), indexToView: 0, profiles: [profile]}
}
function fixUpJSON(content: string): string {
// This code is similar to the code from here:
// https://github.com/catapult-project/catapult/blob/27e047e0494df162022be6aa8a8862742a270232/tracing/tracing/extras/importer/trace_event_importer.html#L197-L208
//
// If the event data begins with a [, then we know it should end with a ]. The
// reason we check for this is because some tracing implementations cannot
// guarantee that a ']' gets written to the trace file. So, we are forgiving
// and if this is obviously the case, we fix it up before throwing the string
// at JSON.parse.
//
content = content.trim()
if (content[0] === '[') {
content = content.replace(/,\s*$/, '')
if (content[content.length - 1] !== ']') {
content += ']'
}
}
return content
}
async function _importProfileGroup(dataSource: ProfileDataSource): Promise<ProfileGroup | null> {
const fileName = await dataSource.name()
@@ -111,13 +92,16 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
// First pass: Check known file format names to infer the file type
if (fileName.endsWith('.speedscope.json')) {
console.log('Importing as speedscope json file')
return importSpeedscopeProfiles(JSON.parse(contents))
return importSpeedscopeProfiles(contents.parseAsJSON())
} else if (/Trace-\d{8}T\d{6}/.exec(fileName)) {
console.log('Importing as Chrome Timeline Object')
return importFromChromeTimeline(contents.parseAsJSON().traceEvents, fileName)
} else if (fileName.endsWith('.chrome.json') || /Profile-\d{8}T\d{6}/.exec(fileName)) {
console.log('Importing as Chrome Timeline')
return importFromChromeTimeline(JSON.parse(contents), fileName)
return importFromChromeTimeline(contents.parseAsJSON(), fileName)
} else if (fileName.endsWith('.stackprof.json')) {
console.log('Importing as stackprof profile')
return toGroup(importFromStackprof(JSON.parse(contents)))
return toGroup(importFromStackprof(contents.parseAsJSON()))
} else if (fileName.endsWith('.instruments.txt')) {
console.log('Importing as Instruments.app deep copy')
return toGroup(importFromInstrumentsDeepCopy(contents))
@@ -129,13 +113,13 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
return toGroup(importFromBGFlameGraph(contents))
} else if (fileName.endsWith('.v8log.json')) {
console.log('Importing as --prof-process v8 log')
return toGroup(importFromV8ProfLog(JSON.parse(contents)))
return toGroup(importFromV8ProfLog(contents.parseAsJSON()))
} else if (fileName.endsWith('.heapprofile')) {
console.log('Importing as Chrome Heap Profile')
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
return toGroup(importFromChromeHeapProfile(contents.parseAsJSON()))
} else if (fileName.endsWith('-recording.json')) {
console.log('Importing as Safari profile')
return toGroup(importFromSafari(JSON.parse(contents)))
return toGroup(importFromSafari(contents.parseAsJSON()))
} else if (fileName.startsWith('callgrind.')) {
console.log('Importing as Callgrind profile')
return importFromCallgrind(contents, fileName)
@@ -144,18 +128,21 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
// Second pass: Try to guess what file format it is based on structure
let parsed: any
try {
parsed = JSON.parse(fixUpJSON(contents))
parsed = contents.parseAsJSON()
} catch (e) {}
if (parsed) {
if (parsed['$schema'] === 'https://www.speedscope.app/file-format-schema.json') {
console.log('Importing as speedscope json file')
return importSpeedscopeProfiles(JSON.parse(contents))
return importSpeedscopeProfiles(parsed)
} else if (parsed['systemHost'] && parsed['systemHost']['name'] == 'Firefox') {
console.log('Importing as Firefox profile')
return toGroup(importFromFirefox(parsed))
} else if (isChromeTimeline(parsed)) {
console.log('Importing as Chrome Timeline')
return importFromChromeTimeline(parsed, fileName)
} else if (isChromeTimelineObject(parsed)) {
console.log('Importing as Chrome Timeline Object')
return importFromChromeTimeline(parsed.traceEvents, fileName)
} else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) {
console.log('Importing as Chrome CPU Profile')
return toGroup(importFromChromeCPUProfile(parsed))
@@ -173,13 +160,13 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
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)))
return toGroup(importFromChromeHeapProfile(parsed))
} else if ('rts_arguments' in parsed && 'initial_capabilities' in parsed) {
console.log('Importing as Haskell GHC JSON Profile')
return importFromHaskell(parsed)
} else if ('recording' in parsed && 'sampleStackTraces' in parsed.recording) {
console.log('Importing as Safari profile')
return toGroup(importFromSafari(JSON.parse(contents)))
return toGroup(importFromSafari(parsed))
}
} else {
// Format is not JSON
@@ -187,8 +174,8 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
// If the first line is "# callgrind format", it's probably in Callgrind
// Profile Format.
if (
/^# callgrind format/.exec(contents) ||
(/^events:/m.exec(contents) && /^fn=/m.exec(contents))
/^# callgrind format/.exec(contents.firstChunk()) ||
(/^events:/m.exec(contents.firstChunk()) && /^fn=/m.exec(contents.firstChunk()))
) {
console.log('Importing as Callgrind profile')
return importFromCallgrind(contents, fileName)
@@ -196,17 +183,14 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
// If the first line contains "Symbol Name", preceded by a tab, it's probably
// a deep copy from OS X Instruments.app
if (/^[\w \t\(\)]*\tSymbol Name/.exec(contents)) {
if (/^[\w \t\(\)]*\tSymbol Name/.exec(contents.firstChunk())) {
console.log('Importing as Instruments.app deep copy')
return toGroup(importFromInstrumentsDeepCopy(contents))
}
// 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+\r?\n/).length) {
console.log('Importing as collapsed stack format')
return toGroup(importFromBGFlameGraph(contents))
if (/^(Stack_|Script_|Obj_)\S+ log opened \(PC\)\n/.exec(contents.firstChunk())) {
console.log('Importing as Papyrus profile')
return toGroup(importFromPapyrus(contents))
}
const fromLinuxPerf = importFromLinuxPerf(contents)
@@ -214,6 +198,12 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
console.log('Importing from linux perf script output')
return fromLinuxPerf
}
const fromBGFlameGraph = importFromBGFlameGraph(contents)
if (fromBGFlameGraph) {
console.log('Importing as collapsed stack format')
return toGroup(fromBGFlameGraph)
}
}
// Unrecognized format
+17 -5
View File
@@ -3,7 +3,6 @@ import * as path from 'path'
import {dumpProfile, checkProfileSnapshot} from '../lib/test-utils'
import * as JSZip from 'jszip'
import {FileSystemEntry} from './file-system-entry'
import {importFromFileSystemDirectoryEntry} from '.'
describe('importFromInstrumentsDeepCopy', () => {
@@ -18,9 +17,16 @@ describe('importFromInstrumentsDeepCopy', () => {
'./sample/profiles/Instruments/7.3.1/random-allocations-deep-copy.txt',
)
})
test('allocations profile', async () => {
await checkProfileSnapshot('./sample/profiles/Instruments/13.4/cycles-example-deep-copy.txt')
})
})
class ZipBackedFileSystemEntry implements FileSystemEntry {
// This is a bit of a weird type signature. I'm making this almost a
// FileSystemEntry, but ignoring the parts of the API I don't use anywhere and
// are a pain to implement.
class ZipBackedFileSystemEntry implements Omit<FileSystemEntry, 'filesystem' | 'getParent'> {
readonly isFile: boolean
readonly isDirectory: boolean
readonly name: string
@@ -29,7 +35,10 @@ class ZipBackedFileSystemEntry implements FileSystemEntry {
private zipDir: any | null
private zipFile: JSZip.JSZipObject | null
constructor(private zip: JSZip, fullPath: string) {
constructor(
private zip: JSZip,
fullPath: string,
) {
this.fullPath = fullPath
this.zipFile = zip.file(fullPath)
@@ -69,7 +78,7 @@ class ZipBackedFileSystemEntry implements FileSystemEntry {
const ret: FileSystemEntry[] = []
this.zipDir.forEach((relativePath: string, file: {name: string}) => {
if (relativePath.split('/').length === (relativePath.endsWith('/') ? 2 : 1)) {
ret.push(new ZipBackedFileSystemEntry(this.zip, file.name))
ret.push(new ZipBackedFileSystemEntry(this.zip, file.name) as any as FileSystemEntry)
}
})
cb(ret)
@@ -86,7 +95,10 @@ describe('importFromInstrumentsTrace', () => {
JSZip.loadAsync(data).then(resolve)
})
})
const root = new ZipBackedFileSystemEntry(zip, 'simple-time-profile.trace')
const root: FileSystemDirectoryEntry = new ZipBackedFileSystemEntry(
zip,
'simple-time-profile.trace',
) as any
const profileGroup = await importFromFileSystemDirectoryEntry(root)
const profile = profileGroup.profiles[profileGroup.indexToView]
expect(dumpProfile(profile)).toMatchSnapshot()
+15 -7
View File
@@ -10,11 +10,10 @@ import {
} from '../lib/profile'
import {sortBy, getOrThrow, getOrInsert, lastOf, getOrElse, zeroPad} from '../lib/utils'
import {ByteFormatter, TimeFormatter} from '../lib/value-formatters'
import {FileSystemDirectoryEntry, FileSystemEntry, FileSystemFileEntry} from './file-system-entry'
import {MaybeCompressedDataReader} from './utils'
import {MaybeCompressedDataReader, TextFileContent} from './utils'
function parseTSV<T>(contents: string): T[] {
const lines = contents.split('\n').map(l => l.split('\t'))
function parseTSV<T>(contents: TextFileContent): T[] {
const lines = [...contents.splitLines()].map(l => l.split('\t'))
const headerLine = lines.shift()
if (!headerLine) return []
@@ -85,7 +84,16 @@ function getWeight(deepCopyRow: any): number {
case 's':
return 1000 * value
case 'min':
return 60 * 1000 * value
case 'cycles':
return value
case 'Kc':
return 1000 * value
case 'Mc':
return 1000 * 1000 * value
case 'Gc':
return 1000 * 1000 * 1000 * value
}
throw new Error(`Unrecognized units ${units}`)
}
@@ -94,7 +102,7 @@ function getWeight(deepCopyRow: any): number {
}
// Import from a deep copy made of a profile
export function importFromInstrumentsDeepCopy(contents: string): Profile {
export function importFromInstrumentsDeepCopy(contents: TextFileContent): Profile {
const profile = new CallTreeProfileBuilder()
const rows = parseTSV<PastedTimeProfileRow | PastedAllocationsProfileRow>(contents)
@@ -187,7 +195,7 @@ function readAsArrayBuffer(file: File): Promise<ArrayBuffer> {
return MaybeCompressedDataReader.fromFile(file).readAsArrayBuffer()
}
function readAsText(file: File): Promise<string> {
function readAsText(file: File): Promise<TextFileContent> {
return MaybeCompressedDataReader.fromFile(file).readAsText()
}
@@ -259,7 +267,7 @@ async function getRawSampleList(core: TraceDirectoryTree): Promise<Sample[]> {
const schemaFile = storedir.files.get('schema.xml')
if (!schemaFile) continue
const schema = await readAsText(schemaFile)
if (!/name="time-profile"/.exec(schema)) {
if (!/name="time-profile"/.exec(schema.firstChunk())) {
continue
}
const bulkstore = new BinReader(
+6
View File
@@ -1,4 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
import {withMockedFileChunkSizeForTests} from './utils'
describe('importFromLinuxPerf', () => {
test('simple.linux-perf.txt', async () => {
@@ -19,4 +20,9 @@ describe('importFromLinuxPerf', () => {
test('system-wide.linux-perf.txt', async () => {
await checkProfileSnapshot('./sample/profiles/linux-perf/system-wide.linux-perf.txt')
})
test('system-wide.linux-perf.txt chunked', async () => {
await withMockedFileChunkSizeForTests(100, async () => {
await checkProfileSnapshot('./sample/profiles/linux-perf/system-wide.linux-perf.txt')
})
})
})
+18 -5
View File
@@ -1,6 +1,7 @@
import {StackListProfileBuilder, ProfileGroup} from '../lib/profile'
import {itMap, getOrInsert} from '../lib/utils'
import {TimeFormatter} from '../lib/value-formatters'
import {TextFileContent} from './utils'
// This imports the output of the "perf script" command on linux.
//
@@ -22,8 +23,21 @@ interface PerfEvent {
stack: PerfStackFrame[]
}
function parseEvent(rawEvent: string): PerfEvent | null {
const lines = rawEvent.split('\n').filter(l => !/^\s*#/.exec(l))
function* parseEvents(contents: TextFileContent): Generator<PerfEvent | null> {
let buffer: string[] = []
for (let line of contents.splitLines()) {
if (line === '') {
yield parseEvent(buffer)
buffer = []
} else buffer.push(line)
}
if (buffer.length > 0) yield parseEvent(buffer)
}
// rawEvent is splitted into lines
function parseEvent(rawEvent: string[]): PerfEvent | null {
const lines = rawEvent.filter(l => !/^\s*#/.exec(l))
const event: PerfEvent = {
command: null,
@@ -80,13 +94,12 @@ function parseEvent(rawEvent: string): PerfEvent | null {
return event
}
export function importFromLinuxPerf(contents: string): ProfileGroup | null {
export function importFromLinuxPerf(contents: TextFileContent): ProfileGroup | null {
const profiles = new Map<string, StackListProfileBuilder>()
let eventType: string | null = null
const events = contents.split('\n\n').map(parseEvent)
for (let event of events) {
for (let event of parseEvents(contents)) {
if (event == null) continue
if (eventType != null && eventType != event.eventType) continue
if (event.time == null) continue
+9
View File
@@ -0,0 +1,9 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importFromPapyrus script profile', async () => {
await checkProfileSnapshot('./sample/profiles/papyrus/script.log')
})
test('importFromPapyrus stack profile', async () => {
await checkProfileSnapshot('./sample/profiles/papyrus/stack.log')
})
+187
View File
@@ -0,0 +1,187 @@
// This importer is for Papyrus, a proprietary DSL written by Bethesda for Skyrim and FO4. It is used both for the base
// games and for mods. You can find documentation (such as the language reference) here:
// https://ck.uesp.net/wiki/Category:Papyrus
//
// For mod authors: you can find documentation on how to start profiling from the console here:
// https://ck.uesp.net/wiki/StartPapyrusScriptProfile
// and you can also start from your script:
// https://ck.uesp.net/wiki/StartScriptProfiling_-_Debug
// If you want to profile an entire mod, it is often helpful to use `StartFormProfile` on your "main quest". This will
// then profile all scripts attached to that Form.
//
// Papyrus works with a queue system, because it is multithreaded, but only one thread can exist per script.
// (And we can only profile one script at a time.) We parse most "QUEUE_PUSH" events, specifically those that come
// directly before their corresponding "PUSH" event. Other "QUEUE_PUSH" (and all "QUEUE_POP") events come, as far as I
// can tell, from Events and are thus asynchronous. They are ignored.
//
// Stack profiling also puts a "START" operation at the top and a "STOP" operation at the bottom of the file. These are
// completely useless to us.
//
// Stack profiling also logs the Form a method is run on. For those that are not familiar with
// Papyrus terminology, a "Form" is an instance of a type defined by a script. E.g. a quest is a form that extends
// the "Quest" script, and thus it has certain methods, like "CompleteQuest()". This information would be useful
// for Debugging, but for profiling, it would hinder meaningful output in left heavy mode.
import {CallTreeProfileBuilder, Frame, Profile} from '../lib/profile'
import {KeyedSet, lastOf} from '../lib/utils'
import {TimeFormatter} from '../lib/value-formatters'
import {TextFileContent} from './utils'
type ParsedLine = {
at: number
event: string
stackInt: number
name: string
}
export function importFromPapyrus(papyrusProfile: TextFileContent): Profile {
const profile = new CallTreeProfileBuilder()
profile.setValueFormatter(new TimeFormatter('milliseconds'))
const papyrusProfileLines = [...papyrusProfile.splitLines()].filter(
line => !/^$|^Log closed$|log opened/.exec(line),
)
let startValue = -1
const firstLineParsed = parseLine(papyrusProfileLines[0])
if (firstLineParsed === null) throw Error
startValue = firstLineParsed.at
const lastLine = lastOf(papyrusProfileLines)
if (lastLine === null) throw Error
const lastLineParsed = parseLine(lastLine)
if (lastLineParsed === null) throw Error
const endValue = lastLineParsed.at
const nameSet = new KeyedSet<Frame>()
const frameStack: string[] = []
let lastEventAt = 0
let lastQueueFrameName: string
let lastQueueFrameAt: number = -1
function enterFrame(stackInt: number, at: number, frameName: string) {
function enterFrameHelper(at: number, frameName: string) {
frameStack.push(frameName)
profile.enterFrame(Frame.getOrInsert(nameSet, {name: frameName, key: frameName}), at)
lastEventAt = at
}
// Check if the last event was "QUEUE_PUSH"
if (lastQueueFrameAt > -1) {
lastQueueFrameAt = -1
// If the queue from last event matches our current frame,
if (lastQueueFrameName === frameName && lastQueueFrameAt >= lastEventAt) {
// first enter the queue frame at its earlier time
enterFrame(stackInt, lastQueueFrameAt, `QUEUE ${frameName}`)
}
}
const stackFrameStr = `STACK ${stackInt}`
// If the uppermost STACK frame on the frameStack isn't stackFrameStr
if (
[...frameStack].reverse().find(frameName => frameName.startsWith('STACK ')) !== stackFrameStr
) {
// If we're at the bottom of the frameStack, STACK frames are kept open as long as functions only run in that
// specific stack and closed with the function's end if the next function runs on a different stack.
if (frameStack.length === 1) leaveFrame(lastEventAt)
enterFrameHelper(at, stackFrameStr)
}
enterFrameHelper(at, frameName)
}
function leaveFrame(at: number) {
const frame = frameStack.pop()
if (frame === undefined) throw Error('Tried to leave frame when nothing was on stack.')
profile.leaveFrame(Frame.getOrInsert(nameSet, {name: frame, key: frame}), at)
let topOfStack = lastOf(frameStack)
// Technically, the frame is popped from queue once it is pushed onto the stack (once we have "entered the frame")
// but since we want to visualize meaningfully, we count from QUEUE_PUSH to POP and prefix with "QUEUE ".
if (topOfStack !== null && topOfStack.startsWith('QUEUE ')) {
leaveFrame(at)
topOfStack = lastOf(frameStack)
}
if (frameStack.length > 1 && topOfStack !== null && topOfStack.startsWith('STACK ')) {
leaveFrame(at)
}
lastEventAt = at
}
function tryToLeaveFrame(stackInt: number, at: number, frameName: string) {
if (lastOf(frameStack) === frameName) {
leaveFrame(at)
} else {
if (lastEventAt === 0) {
console.log(
`Tried to leave frame "${frameName}" which was never entered. Assuming it has been running since the start.`,
)
enterFrame(stackInt, 0, frameName)
leaveFrame(at)
} else {
console.log(
`Tried to leave frame "${frameName}" which was never entered. Other events have happened since the start, ignoring line.`,
)
}
}
}
function parseLine(lineStr: string): ParsedLine | null {
if (lineStr === undefined) throw Error('Probably tried to import empty file.')
const lineArr = lineStr.split(':')
if (lineArr.length < 3) return null
if (startValue !== -1) {
return {
at: parseInt(lineArr[0]) - startValue,
event: lineArr[1],
stackInt: parseInt(lineArr[2]),
name: lineArr[5],
}
} else {
// When parsing the first line, we return an absolute `at` value to initialize `startValue`
return {
at: parseInt(lineArr[0]),
event: lineArr[1],
stackInt: parseInt(lineArr[2]),
name: lineArr[5],
}
}
}
papyrusProfileLines.forEach((lineStr, i, papyrusProfileLines) => {
const parsedLine = parseLine(lineStr)
if (parsedLine === null) return // continue
if (parsedLine.event === 'PUSH') {
enterFrame(parsedLine.stackInt, parsedLine.at, parsedLine.name)
i += 1
let parsedNextLine = parseLine(papyrusProfileLines[i])
// Search all future events in the current event for one that leaves the current frame. If it exists, leave now.
// This way, we avoid speedscope choking on the possibly wrong order of events. The changed order is still
// functionally correct, as the function took less than a millisecond to execute, which is measured as 0 (ms).
while (parsedNextLine !== null && parsedNextLine.at === parsedLine.at) {
if (
parsedNextLine.name === parsedLine.name &&
parsedNextLine.stackInt === parsedLine.stackInt &&
parsedNextLine.event === 'POP'
) {
tryToLeaveFrame(parsedNextLine.stackInt, parsedNextLine.at, parsedNextLine.name)
// Delete the line that we successfully parsed and imported such that it is not processed twice
papyrusProfileLines.splice(i, 1)
parsedNextLine = null
} else {
i += 1
if (i < papyrusProfileLines.length) parsedNextLine = parseLine(papyrusProfileLines[i])
}
}
} else if (parsedLine.event === 'POP') {
tryToLeaveFrame(parsedLine.stackInt, parsedLine.at, parsedLine.name)
} else if (parsedLine.event === 'QUEUE_PUSH') {
lastQueueFrameName = parsedLine.name.replace(/\?/g, '')
lastQueueFrameAt = parsedLine.at
return
}
})
// Close frames that are still open
while (frameStack.length > 0) {
leaveFrame(endValue)
}
return profile.build()
}
+38 -5
View File
@@ -2,12 +2,32 @@ 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'
import Long from 'long'
interface SampleType {
type: string
unit: string
}
// Find the index of the SampleType which should be used as our default
function getSampleTypeIndex(profile: perftools.profiles.Profile): number {
const dflt = profile.defaultSampleType
const sampleTypes = profile.sampleType
const fallback = sampleTypes.length - 1
// string_table[0] will always be empty-string, so we can assume dflt === 0 is just the proto
// empty-value, and means no defaultSampleType was specified.
if (!dflt || !+dflt) {
return fallback
}
const idx = sampleTypes.findIndex(e => e.type === dflt)
if (idx === -1) {
return fallback
}
return idx
}
export function importAsPprofProfile(rawProfile: ArrayBuffer): Profile | null {
if (rawProfile.byteLength === 0) return null
@@ -81,7 +101,12 @@ export function importAsPprofProfile(rawProfile: ArrayBuffer): Profile | null {
if (lastLine == null) return null
if (lastLine.functionId) {
return frameInfoByFunctionID.get(i32(lastLine.functionId)) || null
let funcFrame = frameInfoByFunctionID.get(i32(lastLine.functionId))
const line = lastLine.line instanceof Long ? lastLine.line.toNumber() : lastLine.line
if (line && line > 0 && funcFrame != null) {
funcFrame.line = line
}
return funcFrame || null
} else {
return null
}
@@ -103,9 +128,12 @@ export function importAsPprofProfile(rawProfile: ArrayBuffer): Profile | null {
unit: (type.unit && stringVal(type.unit)) || 'count',
}))
const sampleTypeIndex = protoProfile.defaultSampleType
? +protoProfile.defaultSampleType
: sampleTypes.length - 1
const sampleTypeIndex = getSampleTypeIndex(protoProfile)
if (sampleTypeIndex < 0 || sampleTypeIndex >= sampleTypes.length) {
return null
}
const sampleType = sampleTypes[sampleTypeIndex]
const profileBuilder = new StackListProfileBuilder()
@@ -126,7 +154,12 @@ export function importAsPprofProfile(rawProfile: ArrayBuffer): Profile | null {
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]
if (s.value == null || s.value.length <= sampleTypeIndex) {
return null
}
const value = s.value[sampleTypeIndex]
profileBuilder.appendSampleWithWeight(stack.filter(f => f != null) as FrameInfo[], +value)
}
+1 -1
View File
@@ -72,7 +72,7 @@ function makeStack(frames: StackFrame[]): FrameInfo[] {
file: url,
line,
col: column,
name: name || '(anonymous)',
name: name || (url ? `(anonymous ${url.split('/').pop()}:${line})` : '(anonymous)'),
}))
.reverse()
}
+35
View File
@@ -1,5 +1,40 @@
import {readFileSync} from 'fs'
import {importProfileGroupFromText} from '.'
import {checkProfileSnapshot} from '../lib/test-utils'
test('importFromStackprof', async () => {
await checkProfileSnapshot('./sample/profiles/stackprof/simple-stackprof.json')
})
test('importFromStackprof object mode', async () => {
await checkProfileSnapshot('./sample/profiles/stackprof/object-stackprof.json')
})
test('importFromStackprof when a profile has a frame with no name', async () => {
await checkProfileSnapshot('./sample/profiles/stackprof/stackprof-last-frame-no-name.json')
})
describe('importCpuProfileWithProperWeights', () => {
test('importFromStackprof cpu mode snapshot', async () => {
await checkProfileSnapshot('./sample/profiles/stackprof/simple-cpu-stackprof.json')
})
test('uses samples count for weight when importing cpu profile', async () => {
const profileFile = readFileSync('./sample/profiles/stackprof/simple-cpu-stackprof.json')
const profileGroup = await importProfileGroupFromText(
'simple-cpu-stackprof.json',
profileFile.toString(),
)
expect(profileGroup).not.toBeNull()
if (profileGroup) {
const profile = profileGroup.profiles[profileGroup.indexToView]
expect(profile).not.toBeNull()
if (profile) {
expect(profile.getWeightUnit()).toBe('microseconds')
expect(profile.getTotalWeight()).toBe(489000)
}
}
})
})
+30 -12
View File
@@ -1,25 +1,28 @@
// https://github.com/tmm1/stackprof
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
import {TimeFormatter} from '../lib/value-formatters'
import {RawValueFormatter, TimeFormatter} from '../lib/value-formatters'
interface StackprofFrame {
name: string
name?: string
file?: string
line?: number
}
export interface StackprofProfile {
frames: {[number: string]: StackprofFrame}
mode: string
raw: number[]
raw_timestamp_deltas: number[]
samples: number
interval: number
}
export function importFromStackprof(stackprofProfile: StackprofProfile): Profile {
const duration = stackprofProfile.raw_timestamp_deltas.reduce((a, b) => a + b, 0)
const profile = new StackListProfileBuilder(duration)
const {frames, mode, raw, raw_timestamp_deltas, interval} = stackprofProfile
const profile = new StackListProfileBuilder()
profile.setValueFormatter(new TimeFormatter('microseconds')) // default to time format unless we're in object mode
const {frames, raw, raw_timestamp_deltas} = stackprofProfile
let sampleIndex = 0
let prevStack: FrameInfo[] = []
@@ -30,25 +33,40 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile
let stack: FrameInfo[] = []
for (let j = 0; j < stackHeight; j++) {
const id = raw[i++]
stack.push({
let frameName = frames[id].name
if (frameName == null) {
frameName = '(unknown)'
}
const frame = {
key: id,
...frames[id],
})
name: frameName,
}
stack.push(frame)
}
if (stack.length === 1 && stack[0].name === '(garbage collection)') {
stack = prevStack.concat(stack)
}
const nSamples = raw[i++]
let sampleDuration = 0
for (let j = 0; j < nSamples; j++) {
sampleDuration += raw_timestamp_deltas[sampleIndex++]
switch (mode) {
case 'object':
profile.appendSampleWithWeight(stack, nSamples)
profile.setValueFormatter(new RawValueFormatter())
break
case 'cpu':
profile.appendSampleWithWeight(stack, nSamples * interval)
break
default:
let sampleDuration = 0
for (let j = 0; j < nSamples; j++) {
sampleDuration += raw_timestamp_deltas[sampleIndex++]
}
profile.appendSampleWithWeight(stack, sampleDuration)
}
profile.appendSampleWithWeight(stack, sampleDuration)
prevStack = stack
}
profile.setValueFormatter(new TimeFormatter('microseconds'))
return profile.build()
}
+31
View File
@@ -1,4 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
import {withMockedFileChunkSizeForTests} from './utils'
test('importTraceEvents simple', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple.json')
@@ -16,18 +17,40 @@ test('importTraceEvents partial json import', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial.json')
})
test('importTraceEvents partial json import chunked', async () => {
await withMockedFileChunkSizeForTests(100, async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial.json')
})
})
test('importTraceEvents partial json import trailing comma', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-trailing-comma.json')
})
test('importTraceEvents partial json import trailing comma chunked', async () => {
await withMockedFileChunkSizeForTests(100, async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-trailing-comma.json')
})
})
test('importTraceEvents partial json import whitespace padding', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-whitespace.json')
})
test('importTraceEvents partial json import whitespace padding chunked', async () => {
await withMockedFileChunkSizeForTests(100, async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-whitespace.json')
})
})
test('importTraceEvents bad E events', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/too-many-end-events.json')
})
test('importTraceEvents different number of start and end calls to same function at same timestamp', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simultaneous-anonymous-calls.json')
})
test('importTraceEvents event re-ordering', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/must-retain-original-order.json')
})
@@ -99,3 +122,11 @@ test('importTraceEvents invalid x nesting', async () => {
test('importTraceEvents event reordering name match', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/event-reordering-name-match.json')
})
test('importTraceEvents simple profile with samples', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-with-samples.json')
})
test('importTraceEvents multi-thread profile with samples', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/multi-thread-with-samples.json')
})
+328 -136
View File
@@ -1,5 +1,11 @@
import {sortBy, zeroPad, getOrInsert, lastOf} from '../lib/utils'
import {ProfileGroup, CallTreeProfileBuilder, FrameInfo, Profile} from '../lib/profile'
import {
ProfileGroup,
CallTreeProfileBuilder,
FrameInfo,
Profile,
StackListProfileBuilder,
} from '../lib/profile'
import {TimeFormatter} from '../lib/value-formatters'
// This file concerns import from the "Trace Event Format", authored by Google
@@ -65,16 +71,53 @@ interface XTraceEvent extends TraceEvent {
// The trace format supports a number of event types that we ignore.
type ImportableTraceEvent = BTraceEvent | ETraceEvent | XTraceEvent
interface StackFrame {
line: string
column: string
funcLine: string
funcColumn: string
name: string
category: string
// A parent function may or may not exist
parent?: number
}
interface Sample {
cpu: string
name: string
ts: string
pid: number
tid: string
weight: string
// Will refer to an element in the stackFrames object
sf: number
stackFrameData?: StackFrame
}
interface TraceWithSamples {
traceEvents: TraceEvent[]
samples: Sample[]
stackFrames: {[key: string]: StackFrame}
}
interface TraceEventObject {
traceEvents: TraceEvent[]
}
type Trace = TraceEvent[] | TraceEventObject | TraceWithSamples
function pidTidKey(pid: number, tid: number): string {
// We zero-pad the PID and TID to make sorting them by pid/tid pair later easier.
return `${zeroPad('' + pid, 10)}:${zeroPad('' + tid, 10)}`
}
function partitionByPidTid(events: ImportableTraceEvent[]): Map<string, ImportableTraceEvent[]> {
const map = new Map<string, ImportableTraceEvent[]>()
function partitionByPidTid<T extends {tid: number | string; pid: number | string}>(
events: T[],
): Map<string, T[]> {
const map = new Map<string, T[]>()
for (let ev of events) {
const list = getOrInsert(map, pidTidKey(ev.pid, ev.tid), () => [])
const list = getOrInsert(map, pidTidKey(Number(ev.pid), Number(ev.tid)), () => [])
list.push(ev)
}
@@ -103,12 +146,12 @@ function selectQueueToTakeFromNext(
// If we got here, the 'B' event queue and the 'E' event queue have events at
// the front with equal timestamps.
// If the front of the 'E' queue matches the front of the 'B' queue by name,
// If the front of the 'E' queue matches the front of the 'B' queue by key,
// then it means we have a zero duration event. Process the 'B' queue first
// to ensure it opens before we try to close it.
//
// Otherwise, process the 'E' queue first.
return bFront.name === eFront.name ? 'B' : 'E'
return frameInfoForEvent(bFront).key === frameInfoForEvent(eFront).key ? 'B' : 'E'
}
function convertToEventQueues(events: ImportableTraceEvent[]): [BTraceEvent[], ETraceEvent[]] {
@@ -244,110 +287,147 @@ function frameInfoForEvent(event: TraceEvent): FrameInfo {
}
}
function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
const importableEvents = filterIgnoredEventTypes(events)
const partitioned = partitionByPidTid(importableEvents)
/**
* Constructs an array mapping pid-tid keys to profile builders. Both the traceEvent[]
* format and the sample + stack frame based object format specify the process and thread
* names based on metadata so we share this logic.
*
* See https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.xqopa5m0e28f
*/
function getProfileNameByPidTid(
events: TraceEvent[],
partitionedTraceEvents: Map<string, TraceEvent[]>,
): Map<string, string> {
const processNamesByPid = getProcessNamesByPid(events)
const threadNamesByPidTid = getThreadNamesByPidTid(events)
const profilePairs: [string, Profile][] = []
const profileNamesByPidTid = new Map<string, string>()
partitioned.forEach(eventsForThread => {
if (eventsForThread.length === 0) return
partitionedTraceEvents.forEach(importableEvents => {
if (importableEvents.length === 0) return
const {pid, tid} = eventsForThread[0]
const profile = new CallTreeProfileBuilder()
profile.setValueFormatter(new TimeFormatter('microseconds'))
const {pid, tid} = importableEvents[0]
const profileKey = pidTidKey(pid, tid)
const processName = processNamesByPid.get(pid)
const threadName = threadNamesByPidTid.get(pidTidKey(pid, tid))
const threadName = threadNamesByPidTid.get(profileKey)
if (processName != null && threadName != null) {
profile.setName(`${processName} (pid ${pid}), ${threadName} (tid ${tid})`)
profileNamesByPidTid.set(
profileKey,
`${processName} (pid ${pid}), ${threadName} (tid ${tid})`,
)
} else if (processName != null) {
profile.setName(`${processName} (pid ${pid}, tid ${tid})`)
profileNamesByPidTid.set(profileKey, `${processName} (pid ${pid}, tid ${tid})`)
} else if (threadName != null) {
profile.setName(`${threadName} (pid ${pid}, tid ${tid})`)
profileNamesByPidTid.set(profileKey, `${threadName} (pid ${pid}, tid ${tid})`)
} else {
profile.setName(`pid ${pid}, tid ${tid}`)
profileNamesByPidTid.set(profileKey, `pid ${pid}, tid ${tid}`)
}
})
return profileNamesByPidTid
}
function eventListToProfile(importableEvents: ImportableTraceEvent[], name: string): Profile {
// The trace event format is hard to deal with because it specifically
// allows events to be recorded out of order, *but* event ordering is still
// important for events with the same timestamp. Because of this, rather
// than thinking about the entire event stream as a single queue of events,
// we're going to first construct two time-ordered lists of events:
//
// 1. ts ordered queue of 'B' events
// 2. ts ordered queue of 'E' events
//
// We deal with 'X' events by converting them to one entry in the 'B' event
// queue and one entry in the 'E' event queue.
//
// The high level goal is to deal with 'B' events in 'ts' order, breaking
// ties by the order the events occurred in the file, and deal with 'E'
// events in 'ts' order, breaking ties in whatever order causes the 'E'
// events to match whatever is on the top of the stack.
const [bEventQueue, eEventQueue] = convertToEventQueues(importableEvents)
const profileBuilder = new CallTreeProfileBuilder()
profileBuilder.setValueFormatter(new TimeFormatter('microseconds'))
profileBuilder.setName(name)
const frameStack: BTraceEvent[] = []
const enterFrame = (b: BTraceEvent) => {
frameStack.push(b)
profileBuilder.enterFrame(frameInfoForEvent(b), b.ts)
}
const tryToLeaveFrame = (e: ETraceEvent) => {
const b = lastOf(frameStack)
if (b == null) {
console.warn(
`Tried to end frame "${
frameInfoForEvent(e).key
}", but the stack was empty. Doing nothing instead.`,
)
return
}
// The trace event format is hard to deal with because it specifically
// allows events to be recorded out of order, *but* event ordering is still
// important for events with the same timestamp. Because of this, rather
// than thinking about the entire event stream as a single queue of events,
// we're going to first construct two time-ordered lists of events:
//
// 1. ts ordered queue of 'B' events
// 2. ts ordered queue of 'E' events
//
// We deal with 'X' events by converting them to one entry in the 'B' event
// queue and one entry in the 'E' event queue.
//
// The high level goal is to deal with 'B' events in 'ts' order, breaking
// ties by the order the events occurred in the file, and deal with 'E'
// events in 'ts' order, breaking ties in whatever order causes the 'E'
// events to match whatever is on the top of the stack.
const [bEventQueue, eEventQueue] = convertToEventQueues(eventsForThread)
const eFrameInfo = frameInfoForEvent(e)
const bFrameInfo = frameInfoForEvent(b)
const frameStack: BTraceEvent[] = []
const enterFrame = (b: BTraceEvent) => {
frameStack.push(b)
profile.enterFrame(frameInfoForEvent(b), b.ts)
if (e.name !== b.name) {
console.warn(
`ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Doing nothing instead.`,
)
return
}
const tryToLeaveFrame = (e: ETraceEvent) => {
const b = lastOf(frameStack)
if (b == null) {
console.warn(
`Tried to end frame "${
frameInfoForEvent(e).key
}", but the stack was empty. Doing nothing instead.`,
)
return
}
const eFrameInfo = frameInfoForEvent(e)
const bFrameInfo = frameInfoForEvent(b)
if (e.name !== b.name) {
console.warn(
`ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Doing nothing instead.`,
)
return
}
if (eFrameInfo.key !== bFrameInfo.key) {
console.warn(
`ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Ending ${bFrameInfo.key} instead.`,
)
}
frameStack.pop()
profile.leaveFrame(bFrameInfo, e.ts)
if (eFrameInfo.key !== bFrameInfo.key) {
console.warn(
`ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Ending ${bFrameInfo.key} instead.`,
)
}
while (bEventQueue.length > 0 || eEventQueue.length > 0) {
const queueName = selectQueueToTakeFromNext(bEventQueue, eEventQueue)
switch (queueName) {
case 'B': {
enterFrame(bEventQueue.shift()!)
break
}
case 'E': {
// Before we take the first event in the 'E' queue, let's first see if
// there are any e events that exactly match the top of the stack.
// We'll prioritize first by key, then by name if we can't find a key
// match.
const stackTop = lastOf(frameStack)
if (stackTop != null) {
const bFrameInfo = frameInfoForEvent(stackTop)
frameStack.pop()
profileBuilder.leaveFrame(bFrameInfo, e.ts)
}
let swapped: boolean = false
while (bEventQueue.length > 0 || eEventQueue.length > 0) {
const queueName = selectQueueToTakeFromNext(bEventQueue, eEventQueue)
switch (queueName) {
case 'B': {
enterFrame(bEventQueue.shift()!)
break
}
case 'E': {
// Before we take the first event in the 'E' queue, let's first see if
// there are any e events that exactly match the top of the stack.
// We'll prioritize first by key, then by name if we can't find a key
// match.
const stackTop = lastOf(frameStack)
if (stackTop != null) {
const bFrameInfo = frameInfoForEvent(stackTop)
let swapped: boolean = false
for (let i = 1; i < eEventQueue.length; i++) {
const eEvent = eEventQueue[i]
if (eEvent.ts > eEventQueue[0].ts) {
// Only consider 'E' events with the same ts as the front of the queue.
break
}
const eFrameInfo = frameInfoForEvent(eEvent)
if (bFrameInfo.key === eFrameInfo.key) {
// We have a match! Process this one first.
const temp = eEventQueue[0]
eEventQueue[0] = eEventQueue[i]
eEventQueue[i] = temp
swapped = true
break
}
}
if (!swapped) {
// There was no key match, let's see if we can find a name match
for (let i = 1; i < eEventQueue.length; i++) {
const eEvent = eEventQueue[i]
if (eEvent.ts > eEventQueue[0].ts) {
@@ -355,8 +435,7 @@ function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
break
}
const eFrameInfo = frameInfoForEvent(eEvent)
if (bFrameInfo.key === eFrameInfo.key) {
if (eEvent.name === stackTop.name) {
// We have a match! Process this one first.
const temp = eEventQueue[0]
eEventQueue[0] = eEventQueue[i]
@@ -365,50 +444,156 @@ function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
break
}
}
if (!swapped) {
// There was no key match, let's see if we can find a name match
for (let i = 1; i < eEventQueue.length; i++) {
const eEvent = eEventQueue[i]
if (eEvent.ts > eEventQueue[0].ts) {
// Only consider 'E' events with the same ts as the front of the queue.
break
}
if (eEvent.name === stackTop.name) {
// We have a match! Process this one first.
const temp = eEventQueue[0]
eEventQueue[0] = eEventQueue[i]
eEventQueue[i] = temp
swapped = true
break
}
}
}
// If swapped is still false at this point, it means we're about to
// pop a stack frame that doesn't even match by name. Bummer.
}
const e = eEventQueue.shift()!
tryToLeaveFrame(e)
break
// If swapped is still false at this point, it means we're about to
// pop a stack frame that doesn't even match by name. Bummer.
}
default:
const _exhaustiveCheck: never = queueName
return _exhaustiveCheck
const e = eEventQueue.shift()!
tryToLeaveFrame(e)
break
}
default:
const _exhaustiveCheck: never = queueName
return _exhaustiveCheck
}
}
for (let i = frameStack.length - 1; i >= 0; i--) {
const frame = frameInfoForEvent(frameStack[i])
console.warn(`Frame "${frame.key}" was still open at end of profile. Closing automatically.`)
profileBuilder.leaveFrame(frame, profileBuilder.getTotalWeight())
}
return profileBuilder.build()
}
/**
* Returns an array containing the time difference in microseconds between the current
* sample and the next sample
*/
function getTimeDeltasForSamples(samples: Sample[]): number[] {
const timeDeltas: number[] = []
let lastTimeStamp = Number(samples[0].ts)
samples.forEach((sample: Sample, idx: number) => {
if (idx === 0) return
const timeDiff = Number(sample.ts) - lastTimeStamp
lastTimeStamp = Number(sample.ts)
timeDeltas.push(timeDiff)
})
timeDeltas.push(0)
return timeDeltas
}
/**
* The chrome json trace event spec only specifies name and category
* as required stack frame properties
*
* https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.b4y98p32171
*/
function frameInfoForSampleFrame({name, category}: StackFrame): FrameInfo {
return {
key: `${name}:${category}`,
name: name,
}
}
function getActiveFramesForSample(
stackFrames: {[key: string]: StackFrame},
frameId: number,
): FrameInfo[] {
const frames = []
let parent: number | undefined = frameId
while (parent) {
const frame: StackFrame = stackFrames[parent]
if (!frame) {
throw new Error(`Could not find frame for id ${parent}`)
}
for (let i = frameStack.length - 1; i >= 0; i--) {
const frame = frameInfoForEvent(frameStack[i])
console.warn(`Frame "${frame.key}" was still open at end of profile. Closing automatically.`)
profile.leaveFrame(frame, profile.getTotalWeight())
frames.push(frameInfoForSampleFrame(frame))
parent = frame.parent
}
return frames.reverse()
}
function sampleListToProfile(contents: TraceWithSamples, samples: Sample[], name: string): Profile {
const profileBuilder = new StackListProfileBuilder()
profileBuilder.setValueFormatter(new TimeFormatter('microseconds'))
profileBuilder.setName(name)
const timeDeltas = getTimeDeltasForSamples(samples)
samples.forEach((sample, index) => {
const timeDelta = timeDeltas[index]
const activeFrames = getActiveFramesForSample(contents.stackFrames, sample.sf)
profileBuilder.appendSampleWithWeight(activeFrames, timeDelta)
})
return profileBuilder.build()
}
function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
const importableEvents = filterIgnoredEventTypes(events)
const partitionedTraceEvents = partitionByPidTid(importableEvents)
const profileNamesByPidTid = getProfileNameByPidTid(events, partitionedTraceEvents)
const profilePairs: [string, Profile][] = []
profileNamesByPidTid.forEach((name, profileKey) => {
const importableEventsForPidTid = partitionedTraceEvents.get(profileKey)
if (!importableEventsForPidTid) {
throw new Error(`Could not find events for key: ${importableEventsForPidTid}`)
}
profilePairs.push([pidTidKey(pid, tid), profile.build()])
profilePairs.push([profileKey, eventListToProfile(importableEventsForPidTid, name)])
})
// For now, we just sort processes by pid & tid.
// TODO: The standard specifies that metadata events with the name
// "process_sort_index" and "thread_sort_index" can be used to influence the
// order, but for simplicity we'll ignore that until someone complains :)
sortBy(profilePairs, p => p[0])
return {
name: '',
indexToView: 0,
profiles: profilePairs.map(p => p[1]),
}
}
function sampleListToProfileGroup(contents: TraceWithSamples): ProfileGroup {
const importableEvents = filterIgnoredEventTypes(contents.traceEvents)
const partitionedTraceEvents = partitionByPidTid(importableEvents)
const partitionedSamples = partitionByPidTid(contents.samples)
const profileNamesByPidTid = getProfileNameByPidTid(contents.traceEvents, partitionedTraceEvents)
const profilePairs: [string, Profile][] = []
profileNamesByPidTid.forEach((name, profileKey) => {
const samplesForPidTid = partitionedSamples.get(profileKey)
if (!samplesForPidTid) {
throw new Error(`Could not find samples for key: ${samplesForPidTid}`)
}
if (samplesForPidTid.length === 0) {
return
}
profilePairs.push([profileKey, sampleListToProfile(contents, samplesForPidTid, name)])
})
// For now, we just sort processes by pid & tid.
@@ -456,26 +641,33 @@ function isTraceEventList(maybeEventList: any): maybeEventList is TraceEvent[] {
return true
}
function isTraceEventObject(
maybeTraceEventObject: any,
): maybeTraceEventObject is {traceEvents: TraceEvent[]} {
function isTraceEventObject(maybeTraceEventObject: any): maybeTraceEventObject is TraceEventObject {
if (!('traceEvents' in maybeTraceEventObject)) return false
return isTraceEventList(maybeTraceEventObject['traceEvents'])
}
export function isTraceEventFormatted(
rawProfile: any,
): rawProfile is {traceEvents: TraceEvent[]} | TraceEvent[] {
function isTraceEventWithSamples(
maybeTraceEventObject: any,
): maybeTraceEventObject is TraceWithSamples {
return (
'traceEvents' in maybeTraceEventObject &&
'stackFrames' in maybeTraceEventObject &&
'samples' in maybeTraceEventObject &&
isTraceEventList(maybeTraceEventObject['traceEvents'])
)
}
export function isTraceEventFormatted(rawProfile: any): rawProfile is Trace {
// We're only going to support the JSON formatted profiles for now.
// The spec also discusses support for data embedded in ftrace supported data: https://lwn.net/Articles/365835/.
return isTraceEventObject(rawProfile) || isTraceEventList(rawProfile)
}
export function importTraceEvents(
rawProfile: {traceEvents: TraceEvent[]} | TraceEvent[],
): ProfileGroup {
if (isTraceEventObject(rawProfile)) {
export function importTraceEvents(rawProfile: Trace): ProfileGroup {
if (isTraceEventWithSamples(rawProfile)) {
return sampleListToProfileGroup(rawProfile)
} else if (isTraceEventObject(rawProfile)) {
return eventListToProfileGroup(rawProfile.traceEvents)
} else if (isTraceEventList(rawProfile)) {
return eventListToProfileGroup(rawProfile)
+207 -38
View File
@@ -1,26 +1,223 @@
import * as pako from 'pako'
import {JSON_parse} from 'uint8array-json-parser'
export interface ProfileDataSource {
name(): Promise<string>
readAsArrayBuffer(): Promise<ArrayBuffer>
readAsText(): Promise<string>
readAsText(): Promise<TextFileContent>
}
export interface TextFileContent {
splitLines(): Iterable<string>
firstChunk(): string
parseAsJSON(): any
}
// V8 has a maximum string size. To support files whose contents exceeds that
// size, we provide an alternate string interface for text backed by a
// Uint8Array instead.
//
// We provide a simple splitLines() which returns simple strings under the
// assumption that most extremely large text profiles will be broken into many
// lines. This isn't true in the general case, but will be true for most common
// large files.
//
// See: https://github.com/v8/v8/blob/8b663818fc311217c2cdaaab935f020578bfb7a8/src/objects/string.h#L479-L483
//
// At time of writing (2021/03/27), the maximum string length in V8 is
// 32 bit systems: 2^28 - 16 = ~268M chars
// 64 bit systems: 2^29 - 24 = ~537M chars
//
// https://source.chromium.org/chromium/chromium/src/+/main:v8/include/v8-primitive.h;drc=cb88fe94d9044d860cc75c89e1bc270ab4062702;l=125
//
// We'll be conservative and feed in 2^27 bytes at a time (~134M chars
// assuming utf-8 encoding)
let TEXT_FILE_CHUNK_SIZE = 1 << 27
export async function withMockedFileChunkSizeForTests(chunkSize: number, cb: () => any) {
const original = TEXT_FILE_CHUNK_SIZE
TEXT_FILE_CHUNK_SIZE = chunkSize
try {
await cb()
} finally {
TEXT_FILE_CHUNK_SIZE = original
}
}
function permissivelyParseJSONString(content: string) {
// This code is similar to the code from here:
// https://github.com/catapult-project/catapult/blob/27e047e0494df162022be6aa8a8862742a270232/tracing/tracing/extras/importer/trace_event_importer.html#L197-L208
//
// If the event data begins with a [, then we know it should end with a ]. The
// reason we check for this is because some tracing implementations cannot
// guarantee that a ']' gets written to the trace file. So, we are forgiving
// and if this is obviously the case, we fix it up before throwing the string
// at JSON.parse.
content = content.trim()
if (content[0] === '[') {
content = content.replace(/,\s*$/, '')
if (content[content.length - 1] !== ']') {
content += ']'
}
}
return JSON.parse(content)
}
function permissivelyParseJSONUint8Array(byteArray: Uint8Array) {
let indexOfFirstNonWhitespaceChar = 0
for (let i = 0; i < byteArray.length; i++) {
if (!/\s/.exec(String.fromCharCode(byteArray[i]))) {
indexOfFirstNonWhitespaceChar = i
break
}
}
if (
byteArray[indexOfFirstNonWhitespaceChar] === '['.charCodeAt(0) &&
byteArray[byteArray.length - 1] !== ']'.charCodeAt(0)
) {
// Strip trailing whitespace from the end of the array
let trimmedLength = byteArray.length
while (trimmedLength > 0 && /\s/.exec(String.fromCharCode(byteArray[trimmedLength - 1]))) {
trimmedLength--
}
// Ignore trailing comma
if (String.fromCharCode(byteArray[trimmedLength - 1]) === ',') {
trimmedLength--
}
if (String.fromCharCode(byteArray[trimmedLength - 1]) !== ']') {
// Clone the array, ignoring any whitespace & trailing comma, then append a ']'
//
// Note: We could save a tiny bit of space here by avoiding copying the
// leading whitespace, but it's a trivial perf boost and it complicates
// the code.
const newByteArray = new Uint8Array(trimmedLength + 1)
newByteArray.set(byteArray.subarray(0, trimmedLength))
newByteArray[trimmedLength] = ']'.charCodeAt(0)
byteArray = newByteArray
}
}
return JSON_parse(byteArray)
}
export class BufferBackedTextFileContent implements TextFileContent {
private chunks: string[] = []
private byteArray: Uint8Array
constructor(buffer: ArrayBuffer) {
const byteArray = (this.byteArray = new Uint8Array(buffer))
let encoding: string = 'utf-8'
if (byteArray.length > 2) {
if (byteArray[0] === 0xff && byteArray[1] === 0xfe) {
// UTF-16, Little Endian encoding
encoding = 'utf-16le'
} else if (byteArray[0] === 0xfe && byteArray[1] === 0xff) {
// UTF-16, Big Endian encoding
encoding = 'utf-16be'
}
}
if (typeof TextDecoder !== 'undefined') {
// If TextDecoder is available, we'll try to use it to decode the string.
const decoder = new TextDecoder(encoding)
for (let chunkNum = 0; chunkNum < buffer.byteLength / TEXT_FILE_CHUNK_SIZE; chunkNum++) {
const offset = chunkNum * TEXT_FILE_CHUNK_SIZE
const view = new Uint8Array(
buffer,
offset,
Math.min(buffer.byteLength - offset, TEXT_FILE_CHUNK_SIZE),
)
const chunk = decoder.decode(view, {stream: true})
this.chunks.push(chunk)
}
} else {
// JavaScript strings are UTF-16 encoded, but we're reading data from disk
// that we're going to blindly assume it's ASCII encoded. This codepath
// only exists for older browser support.
console.warn('This browser does not support TextDecoder. Decoding text as ASCII.')
this.chunks.push('')
for (let i = 0; i < byteArray.length; i++) {
this.chunks[this.chunks.length - 1] += String.fromCharCode(byteArray[i])
;(this.chunks[this.chunks.length - 1] as any) | 0 // This forces the string to be flattened
if (this.chunks[this.chunks.length - 1].length >= TEXT_FILE_CHUNK_SIZE) {
this.chunks.push('')
}
}
}
}
splitLines(): Iterable<string> {
const iterator = function* (this: BufferBackedTextFileContent) {
let lineBuffer: string = ''
for (let chunk of this.chunks) {
const fragments = chunk.split('\n')
for (let i = 0; i < fragments.length; i++) {
if (i === 0) lineBuffer += fragments[i]
else {
yield lineBuffer
lineBuffer = fragments[i]
}
}
}
yield lineBuffer
}
return {
[Symbol.iterator]: iterator.bind(this),
}
}
firstChunk(): string {
return this.chunks[0] || ''
}
parseAsJSON(): any {
// We only use the Uint8Array version of JSON.parse when necessary, because
// it's around 4x slower than native.
if (this.chunks.length === 1) {
return permissivelyParseJSONString(this.chunks[0])
}
return permissivelyParseJSONUint8Array(this.byteArray)
}
}
export class StringBackedTextFileContent implements TextFileContent {
constructor(private s: string) {}
splitLines(): string[] {
return this.s.split('\n')
}
firstChunk(): string {
return this.s
}
parseAsJSON(): any {
return permissivelyParseJSONString(this.s)
}
}
export class TextProfileDataSource implements ProfileDataSource {
constructor(private fileName: string, private contents: string) {}
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
async readAsArrayBuffer() {
return new ArrayBuffer(0)
}
async readAsText() {
return this.contents
return new StringBackedTextFileContent(this.contents)
}
}
@@ -49,37 +246,9 @@ export class MaybeCompressedDataReader implements ProfileDataSource {
return await this.uncompressedData
}
async readAsText(): Promise<string> {
async readAsText(): Promise<TextFileContent> {
const buffer = await this.readAsArrayBuffer()
// By default, we assume the file is utf-8 encoded.
let encoding = 'utf-8'
const array = new Uint8Array(buffer)
if (array.length > 2) {
if (array[0] === 0xff && array[1] === 0xfe) {
// UTF-16, Little Endian encoding
encoding = 'utf-16le'
} else if (array[0] === 0xfe && array[1] === 0xff) {
// UTF-16, Big Endian encoding
encoding = 'utf-16be'
}
}
if (typeof TextDecoder !== 'undefined') {
const decoder = new TextDecoder(encoding)
return decoder.decode(buffer)
} else {
// JavaScript strings are UTF-16 encoded, but we're reading data from disk
// that we're going to blindly assume it's ASCII encoded. This codepath
// only exists for older browser support.
console.warn('This browser does not support TextDecoder. Decoding text as ASCII.')
let ret: string = ''
for (let i = 0; i < array.length; i++) {
ret += String.fromCharCode(array[i])
}
return ret
}
return new BufferBackedTextFileContent(buffer)
}
static fromFile(file: File): MaybeCompressedDataReader {
+3 -1
View File
@@ -37,10 +37,12 @@ interface HeapProfile {
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
const name =
callFrame.functionName ||
(file ? `(anonymous ${file.split('/').pop()}:${line})` : '(anonymous)')
return {
key: `${name}:${file}:${line}:${col}`,
name,
+13 -4
View File
@@ -80,12 +80,21 @@ function codeToFrameInfo(code: Code, v8log: V8LogProfile): FrameInfo {
case 'JS': {
const matches = name.match(/([a-zA-Z0-9\._\-$]*) ([a-zA-Z0-9\.\-_\/$]*):(\d+):(\d+)/)
if (matches) {
const file = matches[2]
const line = parseInt(matches[3], 10)
const col = parseInt(matches[4], 10)
const functionName =
matches[1].length > 0
? matches[1]
: file
? `(anonymous ${file.split('/').pop()}:${line})`
: '(anonymous)'
return {
key: name,
name: matches[1].length > 0 ? matches[1] : '(anonymous)',
file: matches[2].length > 0 ? matches[2] : '(unknown file)',
line: parseInt(matches[3], 10),
col: parseInt(matches[4], 10),
name: functionName,
file: file.length > 0 ? file : '(unknown file)',
line,
col,
}
}
break
@@ -1,5 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importSpeedscopeProfiles 0.0.1 evented profile (no pretty print) 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": 0,
"line": undefined,
"name": "a",
"selfWeight": 0,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": 1,
"line": undefined,
"name": "b",
"selfWeight": 5,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": 2,
"line": undefined,
"name": "c",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": 3,
"line": undefined,
"name": "d",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "simple.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
"a;b;c 3",
"a;b 5",
],
}
`;
exports[`importSpeedscopeProfiles 0.0.1 evented profile (no pretty print): indexToView 1`] = `0`;
exports[`importSpeedscopeProfiles 0.0.1 evented profile (no pretty print): profileGroup.name 1`] = `"simple.txt"`;
exports[`importSpeedscopeProfiles 0.0.1 evented profile 1`] = `
Object {
"frames": Array [
@@ -2,7 +2,7 @@
exports[`source-map remapping of chrome-85-esbuild 1`] = `
Array [
"((anonymous) @ alpha.ts:5:5) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:170)",
"((anonymous typescript-source-map-test.js:1) @ alpha.ts:5:5) <- ((anonymous typescript-source-map-test.js:1) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:170)",
"(e @ beta.ts:3:8) <- (e @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:75)",
"(get floop @ kludge.ts:13:7) <- (get floop @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:276)",
"(l constructor @ kludge.ts:4:3) <- (l @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:226)",
@@ -15,7 +15,7 @@ Array [
exports[`source-map remapping of chrome-85-parcel 1`] = `
Array [
"((anonymous) @ ../alpha.ts:5:5) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:8:152)",
"((anonymous typescript-source-map-test.js:8) @ ../alpha.ts:5:5) <- ((anonymous typescript-source-map-test.js:8) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:8:152)",
"(e @ ../gamma.ts:1:22) <- (e @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:2:104)",
"(get @ ../kludge.ts:13:3) <- (get @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:10:284)",
"(o.zap @ ../kludge.ts:9:3) <- (o.zap @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:10:211)",
@@ -32,7 +32,7 @@ Array [
exports[`source-map remapping of chrome-85-webpack 1`] = `
Array [
"((anonymous) @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
"((anonymous typescript-source-map-test.js:1) @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- ((anonymous typescript-source-map-test.js:1) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
"(get @ webpack://speedscope-sourcemap-test-project/kludge.ts:13:3) <- (get @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:368)",
"(n.zap @ webpack://speedscope-sourcemap-test-project/kludge.ts:9:3) <- (n.zap @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:305)",
"(n constructor @ webpack://speedscope-sourcemap-test-project/kludge.ts:4:3) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:243)",
@@ -58,8 +58,8 @@ Array [
exports[`source-map remapping of safari-13-webpack 1`] = `
Array [
"((anonymous) @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
"(zap @ webpack://speedscope-sourcemap-test-project/kludge.ts:9:3) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:305)",
"((anonymous typescript-source-map-test.js:1) @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- ((anonymous typescript-source-map-test.js:1) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
"(zap @ webpack://speedscope-sourcemap-test-project/kludge.ts:9:3) <- ((anonymous typescript-source-map-test.js:1) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:305)",
"(get @ webpack://speedscope-sourcemap-test-project/kludge.ts:13:3) <- (get @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:368)",
"(n constructor @ webpack://speedscope-sourcemap-test-project/kludge.ts:4:3) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:243)",
"(n @ webpack://speedscope-sourcemap-test-project/gamma.ts:1:22) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:34)",
+104
View File
@@ -0,0 +1,104 @@
import {Atom} from './atom'
describe('Atom', () => {
test('basic get/set', () => {
const num = new Atom<number>(0, 'test1')
expect(num.get()).toEqual(0)
num.set(1)
expect(num.get()).toEqual(1)
})
test('basic subscribe/unsubscribe', () => {
let count = 0
let latestNum = 0
function listener() {
count++
latestNum = num.get()
}
const num = new Atom<number>(0, 'test2')
num.subscribe(listener)
expect(count).toEqual(0)
expect(latestNum).toEqual(0)
num.set(3)
expect(count).toEqual(1)
expect(latestNum).toEqual(3)
num.set(7)
expect(count).toEqual(2)
expect(latestNum).toEqual(7)
num.unsubscribe(listener)
num.set(11)
expect(count).toEqual(2)
expect(latestNum).toEqual(7)
expect(num.get()).toEqual(11)
})
test('basic no-op on no-value change', () => {
let count = 0
let latestNum = 0
function listener() {
count++
latestNum = num.get()
}
const num = new Atom<number>(0, 'test3')
num.subscribe(listener)
expect(count).toEqual(0)
expect(latestNum).toEqual(0)
num.set(3)
expect(count).toEqual(1)
expect(latestNum).toEqual(3)
num.set(3)
expect(count).toEqual(1)
expect(latestNum).toEqual(3)
})
test('bound getters and setters', () => {
const num = new Atom<number>(0, 'test1')
let setter = num.set
let getter = num.get
expect(getter()).toEqual(0)
setter(1)
expect(getter()).toEqual(1)
expect(num.get()).toEqual(1)
num.set(2)
expect(getter()).toEqual(2)
})
test('subclassing', () => {
class ListAtom<T> extends Atom<T[]> {
push(t: T) {
const next = [...this.state, t]
this.set(next)
}
removeLast() {
const next = [...this.state]
next.pop()
this.set(next)
}
}
const myListAtom = new ListAtom<number>([1, 2, 3], '')
let count = 0
myListAtom.subscribe(() => {
count++
})
expect(myListAtom.get()).toEqual([1, 2, 3])
myListAtom.push(4)
expect(myListAtom.get()).toEqual([1, 2, 3, 4])
myListAtom.removeLast()
expect(myListAtom.get()).toEqual([1, 2, 3])
expect(count).toEqual(2)
})
})
+156
View File
@@ -0,0 +1,156 @@
// This is a very small application state management library.
//
// All it provides is a way of specifying state "atoms" (basically a value with
// a setter and a way to be notified when the value updates), and a single
// preact hook to manage the subscribe/unsubscribe process for you.
//
// At the moment, atoms are intended to be globally defined, but the system
// could easily be adapted to pass down the atoms via context rather than being
// globally available.
//
// To support complex data-types, you can either have simple functions that call
// the public setter, or subclass Atom. So, for example, here are a few
// different ways of making a list atom with convenient helpers:
//
// # Simple functions being passed atom
//
// const myListAtom = new Atom<number[]>([], "myList")
// function push<T>(atom: Atom<T[]>, t: T) {
// const next = [...atom.get(), t]
// atom.set(next)
// }
// function removeLast<T>(atom: Atom<T[]>, t: T) {
// const next = [...atom.get()]
// next.pop()
// atom.set(next)
// }
//
//
// # Simple functions operating on global atoms
//
// const myListAtom = new Atom<number[]>([], "myLisT")
// function push(t: number) {
// myListAtom.set([...myListAtom.get(), t])
// }
// function removeLast() {
// const next = [...myListAtom.get()]
// myListAtom.set(next)
// }
//
//
// # Subclassing
//
// class ListAtom<T> extends Atom<T[]> {
// push(t: T) {
// const next = [...this.state, t]
// this.set(next)
// }
// removeLast() {
// const next = [...this.state]
// next.pop()
// this.set(next)
// }
// }
// const myListAtom = new ListAtom<number>([], "myList")
//
// This library is inspired by https://recoiljs.org/
import {useLayoutEffect, useState} from 'preact/hooks'
type AtomListener = () => void
let AtomDev: {[key: string]: Atom<any>} | null = null
let hotReloadStash: Map<string, any> | null = null
declare const module: any
if (process.env.NODE_ENV === 'development') {
;(window as any)['Atom'] = AtomDev = {}
module.hot.dispose(() => {
if (AtomDev) {
hotReloadStash = new Map()
for (let key in AtomDev) {
hotReloadStash.set(key, AtomDev[key].get())
}
}
;(window as any)['Atom_hotReloadStash'] = hotReloadStash
})
hotReloadStash = (window as any)['Atom_hotReloadStash'] || null
}
export class Atom<T> {
private observers: AtomListener[] = []
constructor(
protected state: T,
debugKey: string,
) {
if (process.env.NODE_ENV === 'development') {
if (hotReloadStash?.has(debugKey)) {
// If we have a stored value from a previous hot reload, use that
// instead of whatever was passed to the constructor.
this.state = hotReloadStash.get(debugKey)
}
if (AtomDev) {
if (debugKey in AtomDev) {
console.warn(`[Atom] Multiple atoms tried to register with the key ${debugKey}`)
}
AtomDev[debugKey] = this
}
}
// We do the bind here rather than in the definition to facilitate
// inheritance (we want the value defined on both the prototype and the
// instance).
this.set = this.set.bind(this)
this.get = this.get.bind(this)
}
set(t: T) {
if (this.state === t) {
// No-op if the value didn't actually change
return
}
this.state = t
this.observers.forEach(fn => fn())
}
get(): T {
return this.state
}
subscribe(listener: AtomListener) {
this.observers.push(listener)
}
unsubscribe(listener: AtomListener) {
const index = this.observers.indexOf(listener)
if (index !== -1) {
this.observers.splice(index, 1)
}
}
}
export function useAtom<T>(atom: Atom<T>): T {
const [value, setValue] = useState(atom.get())
useLayoutEffect(() => {
// We need to setValue here because it's possible something has changed the
// value in the store between the atom.get() call above and layout. In most
// cases this should no-op.
setValue(atom.get())
function listener() {
setValue(atom.get())
}
atom.subscribe(listener)
return () => {
atom.unsubscribe(listener)
}
}, [atom])
return value
}
+8 -8
View File
@@ -20,14 +20,14 @@ export class Color {
hPrime < 1
? [C, X, 0]
: hPrime < 2
? [X, C, 0]
: hPrime < 3
? [0, C, X]
: hPrime < 4
? [0, X, C]
: hPrime < 5
? [X, 0, C]
: [C, 0, X]
? [X, C, 0]
: hPrime < 3
? [0, C, X]
: hPrime < 4
? [0, X, C]
: hPrime < 5
? [X, 0, C]
: [C, 0, X]
const m = L - (0.3 * R1 + 0.59 * G1 + 0.11 * B1)
+6
View File
@@ -5,6 +5,12 @@ describe('importSpeedscopeProfiles', () => {
await checkProfileSnapshot('./sample/profiles/speedscope/0.0.1/simple.speedscope.json')
})
test('0.0.1 evented profile (no pretty print)', async () => {
await checkProfileSnapshot(
'./sample/profiles/speedscope/0.0.1/simple-no-pretty-print.speedscope.json',
)
})
test('0.1.2 sampled profile', async () => {
await checkProfileSnapshot('./sample/profiles/speedscope/0.1.2/simple-sampled.speedscope.json')
})
+23
View File
@@ -1,7 +1,23 @@
import {ViewMode} from '../lib/view-mode'
export interface HashParams {
profileURL?: string
title?: string
localProfilePath?: string
viewMode?: ViewMode
}
function getViewMode(value: string): ViewMode | null {
switch (value) {
case 'time-ordered':
return ViewMode.CHRONO_FLAME_CHART
case 'left-heavy':
return ViewMode.LEFT_HEAVY_FLAME_GRAPH
case 'sandwich':
return ViewMode.SANDWICH_VIEW
default:
return null
}
}
export function getHashParams(hashContents = window.location.hash): HashParams {
@@ -20,6 +36,13 @@ export function getHashParams(hashContents = window.location.hash): HashParams {
result.title = value
} else if (key === 'localProfilePath') {
result.localProfilePath = value
} else if (key === 'view') {
const mode = getViewMode(value)
if (mode !== null) {
result.viewMode = mode
} else {
console.error(`Ignoring invalid view specifier: ${value}`)
}
}
}
return result
+8 -2
View File
@@ -5,7 +5,10 @@ export function clamp(x: number, minVal: number, maxVal: number) {
}
export class Vec2 {
constructor(readonly x: number, readonly y: number) {}
constructor(
readonly x: number,
readonly y: number,
) {}
withX(x: number) {
return new Vec2(x, this.y)
}
@@ -251,7 +254,10 @@ export class AffineTransform {
}
export class Rect {
constructor(readonly origin: Vec2, readonly size: Vec2) {}
constructor(
readonly origin: Vec2,
readonly size: Vec2,
) {}
isEmpty() {
return this.width() == 0 || this.height() == 0
+6
View File
@@ -0,0 +1,6 @@
import {Component} from 'preact'
export type VoidState = {
__dummyField: void
}
export abstract class StatelessComponent<P> extends Component<P, VoidState> {}
-74
View File
@@ -1,74 +0,0 @@
/**
* As of Preact 10.x, they no longer have an officially supported preact-redux library.
* It's possible to use react-redux with some hacks, but these hacks cause npm run pack
* to error out because of (intentionally) unmet peer dependencies.
*
* I could stack more hacks to fix this problem, but I'd rather just drop the dependency
* and remove the need to do any dependency hacking by writing the very small part of
* react-redux that I actually need myself.
*/
import {h} from 'preact'
import * as redux from 'redux'
import {createContext, ComponentChildren} from 'preact'
import {Dispatch, Action} from './typed-redux'
import {useState, useContext, useCallback, useLayoutEffect} from 'preact/hooks'
const PreactRedux = createContext<redux.Store<any> | null>(null)
interface ProviderProps {
store: redux.Store<any>
children?: ComponentChildren
}
export function Provider(props: ProviderProps) {
return <PreactRedux.Provider value={props.store} children={props.children} />
}
function useStore<T>(): redux.Store<T> {
const store = useContext(PreactRedux)
if (store == null) {
throw new Error('Called useStore when no store exists in context')
}
return store
}
export function useDispatch(): Dispatch {
const store = useStore()
return store.dispatch
}
export function useActionCreator<T, U>(
creator_: (payload: T) => Action<U>,
cacheArgs: any[],
): (t: T) => void {
const dispatch = useDispatch()
/* eslint-disable react-hooks/exhaustive-deps */
const creator = useCallback(creator_, cacheArgs)
return useCallback((t: T) => dispatch(creator(t)), [dispatch, creator])
}
export function useSelector<T, U>(selector_: (t: T) => U, cacheArgs: any[]): U {
const store = useStore<T>()
/* eslint-disable react-hooks/exhaustive-deps */
const selector = useCallback(selector_, cacheArgs)
const getValueFromStore = useCallback(() => selector(store.getState()), [store, selector])
const [value, setValue] = useState(getValueFromStore)
useLayoutEffect(() => {
// We need to setValue here because it's possible something has changed the
// value in the store between the useSelector call and layout. In most cases
// this should no-op.
setValue(getValueFromStore())
return store.subscribe(() => {
setValue(getValueFromStore())
})
}, [store, getValueFromStore])
return value
}
+50
View File
@@ -0,0 +1,50 @@
import {exactMatchStrings} from './profile-search'
function assertMatch(text: string, pattern: string, expected: string) {
const match = exactMatchStrings(text, pattern)
let highlighted = ''
let last = 0
for (let range of match) {
highlighted += `${text.slice(last, range[0])}[${text.slice(range[0], range[1])}]`
last = range[1]
}
highlighted += text.slice(last)
expect(highlighted).toEqual(expected)
}
function assertNoMatch(text: string, pattern: string) {
assertMatch(text, pattern, text)
}
describe('exactMatchStrings', () => {
test('no match', () => {
assertNoMatch('a', 'b')
assertNoMatch('aa', 'ab')
assertNoMatch('a', 'aa')
assertNoMatch('ca', 'ac')
})
test('full text match', () => {
assertMatch('hello', 'hello', '[hello]')
assertMatch('multiple words', 'multiple words', '[multiple words]')
})
test('case sensitivity', () => {
assertMatch('HELLO', 'hello', '[HELLO]')
assertMatch('Hello', 'hello', '[Hello]')
assertMatch('hello', 'Hello', '[hello]')
assertMatch('hello', 'HELLO', '[hello]')
})
test('multiple occurrences', () => {
assertMatch('hello hello', 'hello', '[hello] [hello]')
assertMatch('hellohello', 'hello', '[hello][hello]')
})
test('overlapping occurrences', () => {
assertMatch('aaaaa', 'aa', '[aa][aa]a')
assertMatch('abababa', 'aba', '[aba]b[aba]')
})
})
+41 -8
View File
@@ -1,5 +1,4 @@
import {Profile, Frame, CallTreeNode} from './profile'
import {FuzzyMatch, fuzzyMatchStrings} from './fuzzy-find'
import {Flamechart, FlamechartFrame} from './flamechart'
import {Rect, Vec2} from './math'
@@ -8,19 +7,50 @@ export enum FlamechartType {
LEFT_HEAVY_FLAME_GRAPH,
}
// In previous versions of speedscope, searching for strings within the profile
// was done using fuzzy finding. As it turns out, this was surprising behavior
// to most people, so we've switched to a more traditional substring search that
// more closely mimics browser behavior.
//
// This is case insensitive for both the needle & the haystack. This means
// searching for "hello" will match "Hello" and "HELLO", and searching for
// "HELLO" will match both "hello" and "Hello". This matches Chrome's behavior
// as far as I can tell.
//
// See https://github.com/jlfwong/speedscope/issues/352
//
// Return ranges for all matches in order to highlight them.
export function exactMatchStrings(text: string, pattern: string): [number, number][] {
const lowerText = text.toLocaleLowerCase()
const lowerPattern = pattern.toLocaleLowerCase()
let lastIndex = 0
const matchedRanges: Array<[number, number]> = []
while (true) {
let index = lowerText.indexOf(lowerPattern, lastIndex)
if (index === -1) {
return matchedRanges
}
matchedRanges.push([index, index + pattern.length])
lastIndex = index + pattern.length
}
}
// A utility class for storing cached search results to avoid recomputation when
// the search results & profile did not change.
export class ProfileSearchResults {
constructor(readonly profile: Profile, readonly searchQuery: string) {}
constructor(
readonly profile: Profile,
readonly searchQuery: string,
) {}
private matches: Map<Frame, FuzzyMatch> | null = null
getMatchForFrame(frame: Frame): FuzzyMatch | null {
private matches: Map<Frame, [number, number][] | null> | null = null
getMatchForFrame(frame: Frame): [number, number][] | null {
if (!this.matches) {
this.matches = new Map()
this.profile.forEachFrame(frame => {
const match = fuzzyMatchStrings(frame.name, this.searchQuery)
if (match == null) return
this.matches!.set(frame, match)
const match = exactMatchStrings(frame.name, this.searchQuery)
this.matches!.set(frame, match.length === 0 ? null : match)
})
}
return this.matches.get(frame) || null
@@ -38,7 +68,10 @@ interface CachedFlamechartResult {
}
export class FlamechartSearchResults {
constructor(readonly flamechart: Flamechart, readonly profileResults: ProfileSearchResults) {}
constructor(
readonly flamechart: Flamechart,
readonly profileResults: ProfileSearchResults,
) {}
private matches: CachedFlamechartResult | null = null
private getResults(): CachedFlamechartResult {
+9 -6
View File
@@ -99,7 +99,10 @@ export class CallTreeNode extends HasWeights {
this.frozen = true
}
constructor(readonly frame: Frame, readonly parent: CallTreeNode | null) {
constructor(
readonly frame: Frame,
readonly parent: CallTreeNode | null,
) {
super()
}
}
@@ -439,14 +442,13 @@ export class Profile {
}
export class StackListProfileBuilder extends Profile {
_appendSample(stack: FrameInfo[], weight: number, useAppendOrder: boolean) {
_appendSample(stack: Frame[], weight: number, useAppendOrder: boolean) {
if (isNaN(weight)) throw new Error('invalid weight')
let node = useAppendOrder ? this.appendOrderCalltreeRoot : this.groupedCalltreeRoot
let framesInStack = new Set<Frame>()
for (let frameInfo of stack) {
const frame = Frame.getOrInsert(this.frames, frameInfo)
for (let frame of stack) {
const last = useAppendOrder
? lastOf(node.children)
: node.children.find(c => c.frame === frame)
@@ -500,8 +502,9 @@ export class StackListProfileBuilder extends Profile {
throw new Error('Samples must have positive weights')
}
this._appendSample(stack, weight, true)
this._appendSample(stack, weight, false)
const frames = stack.map(fr => Frame.getOrInsert(this.frames, fr))
this._appendSample(frames, weight, true)
this._appendSample(frames, weight, false)
}
private pendingSample: {
+5 -1
View File
@@ -76,7 +76,11 @@ class Panel {
private GRAPH_WIDTH = 74 * PR
private GRAPH_HEIGHT = 30 * PR
constructor(private name: string, private fg: string, private bg: string) {
constructor(
private name: string,
private fg: string,
private bg: string,
) {
this.canvas.width = this.WIDTH
this.canvas.height = this.HEIGHT
this.canvas.style.cssText = 'width:80px;height:48px'
+1 -1
View File
@@ -105,6 +105,6 @@ export async function expectImportFailure(filepath: string) {
await importProfilesFromArrayBuffer(path.basename(filepath), arrayBuffer)
fail('Expected import to fail but it succeeded')
} catch (error) {
expect(error.message).toMatchSnapshot()
expect((error as Error).message).toMatchSnapshot()
}
}
+4 -4
View File
@@ -43,8 +43,8 @@ export function buildTrimmedText(text: string, length: number): TrimmedTextResul
let prefixLength = Math.floor(length / 2)
const suffixLength = length - prefixLength - 1
const prefix = text.substr(0, prefixLength)
const suffix = text.substr(text.length - suffixLength, suffixLength)
const prefix = text.substring(0, prefixLength)
const suffix = text.substring(text.length - suffixLength, text.length)
const trimmedString = prefix + ELLIPSIS + suffix
return {
trimmedString,
@@ -69,11 +69,11 @@ export function trimTextMid(
0,
text.length,
n => {
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n).trimmedString)
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, Math.floor(n)).trimmedString)
},
maxWidth,
)
return buildTrimmedText(text, lo)
return buildTrimmedText(text, Math.floor(lo))
}
enum IndexTypeInTrimmed {
-68
View File
@@ -1,68 +0,0 @@
import * as redux from 'redux'
import {Component} from 'preact'
export interface Action<TPayload> extends redux.Action<string> {
payload: TPayload
}
export interface ActionCreator<TPayload> {
// Returns an action with a non-empty payload
(payload: TPayload): Action<TPayload>
// Returns an action with an empty payload ({})
(): Action<TPayload>
matches(action: Action<any>): action is Action<TPayload>
}
const usedActionTypes = new Set<string>()
export function actionCreator(type: string): ActionCreator<void>
export function actionCreator<TPayload>(type: string): ActionCreator<TPayload>
export function actionCreator(type: string) {
if (usedActionTypes.has(type)) {
throw new Error(`Cannot re-use action type name: ${type}`)
}
const creator: any = (payload = {}) => {
return {type, payload}
}
creator.matches = (action: Action<any>) => {
return action.type === type
}
return creator
}
export type Reducer<T> = (state: T | undefined, action: Action<any>) => T
export type ReducerWithActionType<T, A> = (state: T | undefined, action: Action<A>) => T
export function setter<T>(
setterAction: ActionCreator<T>,
defaultVal: T,
): (state: T | undefined, action: Action<any>) => T {
return (state = defaultVal, action) => {
if (setterAction.matches(action)) {
return action.payload
}
return state
}
}
export type Dispatch = redux.Dispatch<Action<any>>
export type VoidState = {
__dummyField: void
}
export abstract class StatelessComponent<P> extends Component<P, VoidState> {}
export function bindActionCreator<T>(
dispatch: Dispatch,
actionCreator: (payload: T) => Action<T>,
): (t: T) => void {
return (t: T) => {
dispatch(actionCreator(t))
}
}
+68 -1
View File
@@ -17,7 +17,14 @@ import {
findIndexBisect,
} from './utils'
import {TextEncoder} from 'util'
import * as jsc from 'jsverify'
import {
BufferBackedTextFileContent,
StringBackedTextFileContent,
withMockedFileChunkSizeForTests,
} from '../import/utils'
test('sortBy', () => {
const ls = ['a3', 'b2', 'c1', 'd4']
@@ -35,7 +42,10 @@ test('getOrInsert', () => {
})
class ValueType {
private constructor(readonly a: string, readonly num: number) {}
private constructor(
readonly a: string,
readonly num: number,
) {}
get key() {
return `${this.a}_${this.num}`
}
@@ -229,3 +239,60 @@ test('decodeBase64', () => {
return true
})
})
test('BufferBackedTextFileContent.firstChunk', async () => {
await withMockedFileChunkSizeForTests(2, () => {
const str = 'may\nyour\nrope\nbe\nlong'
const buffer = new TextEncoder().encode(str).buffer
const content = new BufferBackedTextFileContent(buffer)
expect(content.firstChunk()).toEqual('ma')
})
})
test('BufferBackedTextFileContent.splitLines', async () => {
function testWithString(str: string) {
const buffer = new TextEncoder().encode(str).buffer
const content = new BufferBackedTextFileContent(buffer)
expect([...content.splitLines()]).toEqual(str.split('\n'))
}
await withMockedFileChunkSizeForTests(2, () => {
testWithString('may\nyour\nrope\nbe\nlong')
testWithString('one-single-line')
testWithString('multiple\n\nnew\n\nlines')
testWithString('end-with-new-line\n')
testWithString('')
})
})
test('BufferBackedTextFileContent.parseAsJSON', async () => {
await withMockedFileChunkSizeForTests(2, () => {
// parseAsJSON is special cased to permissively allow trailing commas
// and a mission closing bracket
const str = '[200,300,400,'
const buffer = new TextEncoder().encode(str).buffer
const content = new BufferBackedTextFileContent(buffer)
expect(content.parseAsJSON()).toEqual([200, 300, 400])
})
})
test('StringBackedTextFileContent.firstChunk', async () => {
const str = 'may\nyour\nrope\nbe\nlong'
const content = new StringBackedTextFileContent(str)
expect(content.firstChunk()).toEqual(str)
})
test('StringBackedTextFileContent.splitLines', async () => {
const str = 'may\nyour\nrope\nbe\nlong'
const content = new StringBackedTextFileContent(str)
expect(content.splitLines()).toEqual(['may', 'your', 'rope', 'be', 'long'])
})
test('StringBackedTextFileContent.parseAsJSON', async () => {
// parseAsJSON is special cased to permissively allow trailing commas
// and a mission closing bracket
const str = '[200,300,400,'
const content = new StringBackedTextFileContent(str)
expect(content.parseAsJSON()).toEqual([200, 300, 400])
})
+9 -11
View File
@@ -213,17 +213,15 @@ export function lazyStatic<T>(cb: () => T): () => T {
}
}
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
},
)
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.
+5
View File
@@ -0,0 +1,5 @@
export const enum ViewMode {
CHRONO_FLAME_CHART,
LEFT_HEAVY_FLAME_GRAPH,
SANDWICH_VIEW,
}
+3 -11
View File
@@ -1,7 +1,5 @@
import {h, render} from 'preact'
import {createAppStore} from './store'
import {ApplicationContainer} from './views/application-container'
import {Provider} from './lib/preact-redux'
import {ThemeProvider} from './views/themes/theme'
console.log(`speedscope v${require('../package.json').version}`)
@@ -15,16 +13,10 @@ if (module.hot) {
module.hot.accept()
}
const lastStore: any = (window as any)['store']
const store = lastStore ? createAppStore(lastStore.getState()) : createAppStore()
;(window as any)['store'] = store
render(
<Provider store={store}>
<ThemeProvider>
<ApplicationContainer />
</ThemeProvider>
</Provider>,
<ThemeProvider>
<ApplicationContainer />
</ThemeProvider>,
document.body,
document.body.lastElementChild || undefined,
)
-53
View File
@@ -1,53 +0,0 @@
import {actionCreator} from '../lib/typed-redux'
import {CallTreeNode, Frame, ProfileGroup} from '../lib/profile'
import {SortMethod} from '../views/profile-table-view'
import {ColorScheme, ViewMode} from '.'
import {FlamechartID} from './flamechart-view-state'
import {Rect, Vec2} from '../lib/math'
import {HashParams} from '../lib/hash-params'
import {actionCreatorWithIndex} from './profiles-state'
export namespace actions {
export const setProfileGroup = actionCreator<ProfileGroup>('setProfileGroup')
export const setProfileIndexToView = actionCreator<number>('setProfileIndexToView')
export const setGLCanvas = actionCreator<HTMLCanvasElement | null>('setGLCanvas')
export const setViewMode = actionCreator<ViewMode>('setViewMode')
export const setFlattenRecursion = actionCreator<boolean>('setFlattenRecursion')
export const setSearchQuery = actionCreator<string>('setSearchQuery')
export const setSearchIsActive = actionCreator<boolean>('setSearchIsActive')
export const setDragActive = actionCreator<boolean>('setDragActive')
export const setLoading = actionCreator<boolean>('setLoading')
export const setError = actionCreator<boolean>('setError')
export const setHashParams = actionCreator<HashParams>('setHashParams')
export const setColorScheme = actionCreator<ColorScheme>('setColorScheme')
export namespace sandwichView {
export const setTableSortMethod = actionCreator<SortMethod>('sandwichView.setTableSortMethod')
export const setSelectedFrame = actionCreatorWithIndex<Frame | null>(
'sandwichView.setSelectedFrame',
)
}
export namespace flamechart {
export const setHoveredNode = actionCreatorWithIndex<{
id: FlamechartID
hover: {node: CallTreeNode; event: MouseEvent} | null
}>('flamechart.setHoveredNode')
export const setSelectedNode = actionCreatorWithIndex<{
id: FlamechartID
selectedNode: CallTreeNode | null
}>('flamechart.setSelectedNode')
export const setConfigSpaceViewportRect = actionCreatorWithIndex<{
id: FlamechartID
configSpaceViewportRect: Rect
}>('flamechart.setConfigSpaceViewportRect')
export const setLogicalSpaceViewportSize = actionCreatorWithIndex<{
id: FlamechartID
logicalSpaceViewportSize: Vec2
}>('flamechart.setLogicalSpaceViewportSpace')
}
}
-105
View File
@@ -1,105 +0,0 @@
import {storeTest, profileGroupTwoSampled} from './store-test-utils'
import {actions} from './actions'
import {Rect, Vec2} from '../lib/math'
import {FlamechartID} from './flamechart-view-state'
import {ViewMode} from '.'
describe('flamechart view state', () => {
storeTest('setHoveredNode', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(null)
const {profileGroup} = getState()
const hover = {
node: profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0],
event: {} as MouseEvent,
}
dispatch(
actions.flamechart.setHoveredNode({
profileIndex: 0,
args: {
id: FlamechartID.CHRONO,
hover,
},
}),
)
expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(hover)
expect(getState().profileGroup!.profiles[0].leftHeavyViewState.hover).toEqual(null)
expect(getState().profileGroup!.profiles[1].chronoViewState.hover).toEqual(null)
// Changing view mode should clear the hover state
dispatch(actions.setViewMode(ViewMode.LEFT_HEAVY_FLAME_GRAPH))
expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(null)
})
storeTest('setSelectedNode', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
expect(getState().profileGroup!.profiles[0].chronoViewState.selectedNode).toEqual(null)
const {profileGroup} = getState()
const selectedNode = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0]
dispatch(
actions.flamechart.setSelectedNode({
profileIndex: 0,
args: {
id: FlamechartID.CHRONO,
selectedNode,
},
}),
)
expect(getState().profileGroup!.profiles[0].chronoViewState.selectedNode).toEqual(selectedNode)
expect(getState().profileGroup!.profiles[0].leftHeavyViewState.selectedNode).toEqual(null)
expect(getState().profileGroup!.profiles[1].chronoViewState.selectedNode).toEqual(null)
})
storeTest('setLogicalSpaceViewportSize', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
expect(getState().profileGroup!.profiles[0].chronoViewState.logicalSpaceViewportSize).toEqual(
Vec2.zero,
)
const logicalSpaceViewportSize = new Vec2(3, 5)
dispatch(
actions.flamechart.setLogicalSpaceViewportSize({
profileIndex: 0,
args: {
id: FlamechartID.CHRONO,
logicalSpaceViewportSize,
},
}),
)
expect(getState().profileGroup!.profiles[0].chronoViewState.logicalSpaceViewportSize).toEqual(
logicalSpaceViewportSize,
)
expect(
getState().profileGroup!.profiles[0].leftHeavyViewState.logicalSpaceViewportSize,
).toEqual(Vec2.zero)
expect(getState().profileGroup!.profiles[1].chronoViewState.logicalSpaceViewportSize).toEqual(
Vec2.zero,
)
})
storeTest('setConfigSpaceViewportRect', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
expect(getState().profileGroup!.profiles[0].chronoViewState.configSpaceViewportRect).toEqual(
Rect.empty,
)
dispatch(
actions.flamechart.setConfigSpaceViewportRect({
profileIndex: 0,
args: {
id: FlamechartID.CHRONO,
configSpaceViewportRect: new Rect(Vec2.zero, Vec2.unit),
},
}),
)
expect(getState().profileGroup!.profiles[0].chronoViewState.configSpaceViewportRect).toEqual(
new Rect(Vec2.zero, Vec2.unit),
)
expect(getState().profileGroup!.profiles[0].leftHeavyViewState.configSpaceViewportRect).toEqual(
Rect.empty,
)
expect(getState().profileGroup!.profiles[1].chronoViewState.configSpaceViewportRect).toEqual(
Rect.empty,
)
})
})
-62
View File
@@ -1,62 +0,0 @@
import {CallTreeNode} from '../lib/profile'
import {Rect, Vec2} from '../lib/math'
import {Reducer} from '../lib/typed-redux'
import {actions} from './actions'
export enum FlamechartID {
LEFT_HEAVY = 'LEFT_HEAVY',
CHRONO = 'CHRONO',
SANDWICH_INVERTED_CALLERS = 'SANDWICH_INVERTED_CALLERS',
SANDWICH_CALLEES = 'SANDWICH_CALLEES',
}
export interface FlamechartViewState {
hover: {
node: CallTreeNode
event: MouseEvent
} | null
selectedNode: CallTreeNode | null
logicalSpaceViewportSize: Vec2
configSpaceViewportRect: Rect
}
export function createFlamechartViewStateReducer(
id: FlamechartID,
profileIndex: number,
): Reducer<FlamechartViewState> {
let initialState: FlamechartViewState = {
hover: null,
selectedNode: null,
configSpaceViewportRect: Rect.empty,
logicalSpaceViewportSize: Vec2.zero,
}
function applies(action: {payload: {profileIndex: number; args: {id: FlamechartID}}}) {
const {payload} = action
return payload.args.id === id && payload.profileIndex === profileIndex
}
return (state = initialState, action) => {
if (actions.flamechart.setHoveredNode.matches(action) && applies(action)) {
const {hover} = action.payload.args
return {...state, hover}
}
if (actions.flamechart.setSelectedNode.matches(action) && applies(action)) {
const {selectedNode} = action.payload.args
return {...state, selectedNode}
}
if (actions.flamechart.setConfigSpaceViewportRect.matches(action) && applies(action)) {
const {configSpaceViewportRect} = action.payload.args
return {...state, configSpaceViewportRect}
}
if (actions.flamechart.setLogicalSpaceViewportSize.matches(action) && applies(action)) {
const {logicalSpaceViewportSize} = action.payload.args
return {...state, logicalSpaceViewportSize}
}
if (actions.setViewMode.matches(action)) {
// If we switch views, the hover information is no longer relevant
return {...state, hover: null}
}
return state
}
}
-51
View File
@@ -1,51 +0,0 @@
import {ViewMode} from '.'
import {actions} from './actions'
import {storeTest} from './store-test-utils'
import {SortField, SortDirection} from '../views/profile-table-view'
storeTest('flattenRecursion', ({getState, dispatch}) => {
expect(getState().flattenRecursion).toBe(false)
dispatch(actions.setFlattenRecursion(true))
expect(getState().flattenRecursion).toBe(true)
})
storeTest('viewMode', ({getState, dispatch}) => {
expect(getState().viewMode).toBe(ViewMode.CHRONO_FLAME_CHART)
dispatch(actions.setViewMode(ViewMode.SANDWICH_VIEW))
expect(getState().viewMode).toBe(ViewMode.SANDWICH_VIEW)
})
storeTest('dragActive', ({getState, dispatch}) => {
expect(getState().dragActive).toBe(false)
dispatch(actions.setDragActive(true))
expect(getState().dragActive).toBe(true)
})
storeTest('loading', ({getState, dispatch}) => {
expect(getState().loading).toBe(false)
dispatch(actions.setLoading(true))
expect(getState().loading).toBe(true)
})
storeTest('error', ({getState, dispatch}) => {
expect(getState().error).toBe(false)
dispatch(actions.setError(true))
expect(getState().error).toBe(true)
})
storeTest('setTableSortMethod', ({getState, dispatch}) => {
expect(getState().tableSortMethod).toEqual({
field: SortField.SELF,
direction: SortDirection.DESCENDING,
})
dispatch(
actions.sandwichView.setTableSortMethod({
field: SortField.SYMBOL_NAME,
direction: SortDirection.ASCENDING,
}),
)
expect(getState().tableSortMethod).toEqual({
field: SortField.SYMBOL_NAME,
direction: SortDirection.ASCENDING,
})
})
-191
View File
@@ -1,191 +0,0 @@
import {actions} from './actions'
/**
* The root node of application state. We use redux (https://redux.js.org/)
* as our state management solution.
*/
import * as redux from 'redux'
import {setter, Reducer, Action} from '../lib/typed-redux'
import {HashParams, getHashParams} from '../lib/hash-params'
import {ProfileGroupState, profileGroup} from './profiles-state'
import {SortMethod, SortField, SortDirection} from '../views/profile-table-view'
import {useSelector} from '../lib/preact-redux'
import {Profile} from '../lib/profile'
import {FlamechartViewState} from './flamechart-view-state'
import {SandwichViewState} from './sandwich-view-state'
import {getProfileToView} from './getters'
export const enum ViewMode {
CHRONO_FLAME_CHART,
LEFT_HEAVY_FLAME_GRAPH,
SANDWICH_VIEW,
}
export const enum ColorScheme {
// Default: respect prefers-color-schema
SYSTEM,
// Use dark theme
DARK,
// use light theme
LIGHT,
}
export interface ApplicationState {
// The top-level profile group from which most other data will be derived
profileGroup: ProfileGroupState
// Parameters defined by the URL encoded k=v pairs after the # in the URL
hashParams: HashParams
glCanvas: HTMLCanvasElement | null
// Which top-level view should be displayed
viewMode: ViewMode
// True if recursion should be flattened when viewing flamegraphs
flattenRecursion: boolean
// The query used in top-level views
//
// An empty string indicates that the search is open by no filter is applied.
// searchIsActive is stored separately, because we may choose to persist the
// query even when the search input is closed.
searchQuery: string
searchIsActive: boolean
// True when a file drag is currently active. Used to indicate that the
// application is a valid drop target.
dragActive: boolean
// True when the application is currently in a loading state. Used to
// display a loading progress bar.
loading: boolean
// True when the application is an error state, e.g. because the profile
// imported was invalid.
error: boolean
// The table sorting method using for the sandwich view, specifying the column
// to sort by, and the direction to sort that clumn.
tableSortMethod: SortMethod
// The color scheme to use for the entire UI
colorScheme: ColorScheme
}
const protocol = window.location.protocol
// Speedscope is usable both from a local HTML file being served
// from a file:// URL, and via websites. In the case of file:// URLs,
// however, XHR will be unavailable to fetching files in adjacent directories.
export const canUseXHR = protocol === 'http:' || protocol === 'https:'
function colorScheme(state: ColorScheme | undefined, action: Action<any>): ColorScheme {
const localStorageKey = 'speedscope-color-scheme'
if (state === undefined) {
const storedPreference = window.localStorage && window.localStorage[localStorageKey]
if (storedPreference === 'DARK') {
return ColorScheme.DARK
} else if (storedPreference === 'LIGHT') {
return ColorScheme.LIGHT
} else {
return ColorScheme.SYSTEM
}
}
if (actions.setColorScheme.matches(action)) {
const value = action.payload
switch (value) {
case ColorScheme.DARK: {
window.localStorage[localStorageKey] = 'DARK'
break
}
case ColorScheme.LIGHT: {
window.localStorage[localStorageKey] = 'LIGHT'
break
}
case ColorScheme.SYSTEM: {
delete window.localStorage[localStorageKey]
break
}
default: {
const _exhaustiveCheck: never = value
return _exhaustiveCheck
}
}
return value
}
return state
}
export function createAppStore(initialState?: ApplicationState): redux.Store<ApplicationState> {
const hashParams = getHashParams()
const loading = canUseXHR && hashParams.profileURL != null
const reducer: Reducer<ApplicationState> = redux.combineReducers({
profileGroup,
hashParams: setter<HashParams>(actions.setHashParams, hashParams),
flattenRecursion: setter<boolean>(actions.setFlattenRecursion, false),
viewMode: setter<ViewMode>(actions.setViewMode, ViewMode.CHRONO_FLAME_CHART),
searchQuery: setter<string>(actions.setSearchQuery, ''),
searchIsActive: setter<boolean>(actions.setSearchIsActive, false),
glCanvas: setter<HTMLCanvasElement | null>(actions.setGLCanvas, null),
dragActive: setter<boolean>(actions.setDragActive, false),
loading: setter<boolean>(actions.setLoading, loading),
error: setter<boolean>(actions.setError, false),
tableSortMethod: setter<SortMethod>(actions.sandwichView.setTableSortMethod, {
field: SortField.SELF,
direction: SortDirection.DESCENDING,
}),
colorScheme,
})
return redux.createStore(reducer, initialState)
}
export function useAppSelector<T>(selector: (t: ApplicationState) => T, cacheArgs: any[]): T {
/* eslint-disable react-hooks/exhaustive-deps */
return useSelector(selector, cacheArgs)
}
export interface ActiveProfileState {
profile: Profile
index: number
chronoViewState: FlamechartViewState
leftHeavyViewState: FlamechartViewState
sandwichViewState: SandwichViewState
}
export function useActiveProfileState(): ActiveProfileState | null {
return useAppSelector(state => {
const {profileGroup} = state
if (!profileGroup) return null
if (profileGroup.indexToView >= profileGroup.profiles.length) return null
const index = profileGroup.indexToView
const profileState = profileGroup.profiles[index]
return {
...profileGroup.profiles[profileGroup.indexToView],
profile: getProfileToView({
profile: profileState.profile,
flattenRecursion: state.flattenRecursion,
}),
index: profileGroup.indexToView,
}
}, [])
}

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