Compare commits

...
27 Commits
Author SHA1 Message Date
Jamie Wong 6a979bb568 1.12.1 2020-11-12 02:35:59 -08:00
Jamie Wong 36911599cb Fix infinite recursion in resizing on retina displays (fixes #327) 2020-11-12 02:34:19 -08:00
Jamie Wong b32ff08ca3 1.12.0 2020-11-12 01:57:41 -08:00
Jamie Wong 361bdc9cd2 Fix bug in remapRangesToTrimmedText (#326)
Welp, looks like I missed a pretttty important edge case in my test.

Fixes #324
2020-11-12 01:52:47 -08:00
Jamie Wong a1384b03be Add a system for theming, use it to implement dark mode (#323)
Dark mode:
![image](https://user-images.githubusercontent.com/150329/98463526-9680a880-2170-11eb-9fc2-9018604ff1ad.png)

Light mode:
![image](https://user-images.githubusercontent.com/150329/98463537-a5fff180-2170-11eb-8b60-afe2096d848e.png)

Fixes #220
2020-11-12 01:51:55 -08:00
Jamie Wong 5f32640060 1.11.1 2020-10-25 01:50:28 -07:00
Jamie Wong a10c834f99 Fix trace-event import for many cases where there are 'ts' collisions (#322)
The trace event format has a very unfortunate combination of requirements in order to give a best-effort interpretation of a given trace file:

1. Events may be recorded out-of-order by timestamp
2. Events with the *same* timestamp should be processed in the order they were provided in the file. Mostly.

The first requirement is written explicitly [in the spec](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview).

> The events do not have to be in timestamp-sorted order.

The second one isn't explicitly written, but it's implicitly true because otherwise the interpretation of a file is ambiguous. For example, the following file has all events with the same `ts` field, but re-ordering the fields changes the interpretation.

```
[
  { "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 20, "name": "alpha" },
  { "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 20, "name": "beta" }
}
```

If we allowed arbitrary reordering, it would be ambiguous whether the alpha frame should be nested inside of the beta frame or vice versa. Since traces are interpreted as call trees, it's not okay to just arbitrarily choose.

So you might next guess that a reasonable approach would be to do a [stable sort](https://wiki.c2.com/?StableSort) by "ts", then process the events one-by-one. This almost works, except for two additional problems. The first problem is that in some situations this would still yield invalid results.

```
[
  {"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
  {"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 0},
  {"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 1},
  {"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 1}
]
```

If we were to follow this rule, we would try to execute the `"E"` for alpha before the `"E"` for beta, even though beta is on the top of the stack. So in *that* case, we actually need to execute the `"E"` for beta first, otherwise the resulting profile is incorrect.

The other problem with this approach of using the stable sort order is the question of how to deal with `"X"` events. speedscope translates `"X"` events into a `"B"` and `"E"` event pair. But where should it put the `"E"` event? Your first guess might be "at the index where the `"X"` events occur in the file". This runs into trouble in cases like this:

```
[
  { "pid": 0, "tid": 0, "ph": "X", "ts": 9, "dur": 1, "name": "beta" },
  { "pid": 0, "tid": 0, "ph": "X", "ts": 9, "dur": 2, "name": "gamma" },
]
```

The most natural translation of this would be to convert it into the following `"B"` and `"E"` events:

```
[
  { "pid": 0, "tid": 0, "ph": "B", "ts": 9, "name": "beta" },
  { "pid": 0, "tid": 0, "ph": "E", "ts": 10, "name": "beta" },
  { "pid": 0, "tid": 0, "ph": "B", "ts": 9, "name": "gamma" },
  { "pid": 0, "tid": 0, "ph": "E", "ts": 11, "name": "gamma" },
]
```

Which, after a stable sort turns into this:

```
[
  { "pid": 0, "tid": 0, "ph": "B", "ts": 9, "name": "beta" },
  { "pid": 0, "tid": 0, "ph": "B", "ts": 9, "name": "gamma" },
  { "pid": 0, "tid": 0, "ph": "E", "ts": 10, "name": "beta" },
  { "pid": 0, "tid": 0, "ph": "E", "ts": 11, "name": "gamma" },
]
```

Notice that we again have a problem where we open "beta" before "gamma", but we need to close "beta" first because it ends first!

Ultimately, I couldn't figure out any sort order that would allow me to predict ahead-of-time what order to process the events in. So instead, I create two event queues: one for `"B"` events, and one for `"E"` events, and then try to be clever about how I merge them together.

AFAICT, chrome://tracing does not sort events before processing them, which is kind of baffling. But chrome://tracing also has really bizarre behaviour for things like this where the resulting flamegraph isn't even a valid tree (there are overlapping ranges):

```
[
  { "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 10, "name": "alpha" },
  { "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 10, "name": "beta" }
}
```

So I'm going to call this "good enough" for now.

Fixes #223
Fixes #320
2020-10-25 01:45:13 -07:00
Jamie Wong de3ab89eb5 Fix import of trace event files where B/E events' args don't match (#321)
In #273, I changed `CallTreeProfileBuilder.leaveFrame` to fail hard when you request to leave a frame different from the one at the top of the stack. It turns out we were intentionally doing this for trace event imports, because `args` are part of the frame key, and we want to allow profiles to be imported where the `"B"` and `"E"` events have differing `args` field.

This PR fixes the import code to permissively allow the `"args"` field to not match between the `"B"` and `"E"` fields.

**A note on intentional differences between speedscope and chrome://tracing**

`chrome://tracing` will close whichever frame is at the top when it gets an `"E"` event, regardless of whether the name or the args match. speedscope will ignore the event entirely if the `"name"` field doesn't match, but will warn but still close the frame if the `"name"`s match but the `"args"` don't.
```
[
  {"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
  {"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
  {"pid": 0, "tid": 0, "ph": "E", "name": "gamma", "ts": 2},
  {"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 9},
  {"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 10}
]
```
### speedscope
![image](https://user-images.githubusercontent.com/150329/97098205-7365dd00-1637-11eb-9869-4e81ebebcee1.png)
```
warning: ts=2: Request to end "gamma" when "beta" was on the top of the stack. Doing nothing instead.
```
### chrome://tracing
![image](https://user-images.githubusercontent.com/150329/97098215-87114380-1637-11eb-909c-b2e70c7291a4.png)
2020-10-24 21:58:31 -07:00
Jamie Wong aee2dfdf89 1.11.0 2020-10-13 01:05:26 -07:00
Jamie Wong d9b3950274 Support remapping profiles using source maps (#317)
This PR adds the ability to remap an already-loaded profile using a JavaScript source map. This is useful for e.g. recording minified profiles in production, and then remapping their symbols when the source map isn't made directly available to the browser in production.

This is a bit of a hidden feature. The way it works is to drop a profile into speedscope, then drop the sourcemap file on top of it.

To test this, I used a small project @cricklet made (https://gist.github.com/cricklet/0deaaa7dd63657adb6818f0a52362651), and also tested against speedscope itself.

To test against speedscope itself, I profiled loading a file in speedscope in Chrome, then dropped the resulting Chrome timeline profile into speedscope, and dropped speedscope's own sourcemap on top. Before dropping the source map, the symbols look like this:

![image](https://user-images.githubusercontent.com/150329/94977230-b2878f00-04cc-11eb-8907-02a1f1485653.png)

After dropping the source map, they look like this:

![image](https://user-images.githubusercontent.com/150329/94977253-d4811180-04cc-11eb-9f88-1e7a02149331.png)

I also added automated tests using a small JS bundle constructed with various different JS bundlers to make sure it was doing a sensible thing in each case.

# Background

Remapping symbols in profiles using source-maps proved to be more complex than I originally thought because of an idiosyncrasy of which line & column are referenced for stack frames in browsers. Rather than the line & column referencing the first character of the symbol, they instead reference the opening paren for the function definition.

Here's an example file where it's not immediately apparent which line & column is going to be referenced by each stack frame:

```
class Kludge {
  constructor() {
    alpha()
  }

  zap() {
    alpha()
  }
}

function alpha() {
  for (let i = 0; i < 1000; i++) {
    beta()
    delta()
  }
}

function beta() {
  for (let i = 0; i < 10; i++) {
    gamma()
  }
}

const delta = function () {
  for (let i = 0; i < 10; i++) {
    gamma()
  }
}

const gamma =
() => {
  let prod = 1
  for (let i = 1; i < 1000; i++) {
    prod *= i
  }
  return prod
}

const k = new Kludge()
k.zap()
```

The resulting profile looks like this:
![image](https://user-images.githubusercontent.com/150329/94976830-0db88200-04cb-11eb-86d7-934365a17c53.png)

The relevant line & column for each function are...

```
// Kludge: line 2, column 14
class Kludge {
  constructor() {
             ^
...
// zap: line 6, column 6
  zap() {
     ^
...
// alpha: line 11, column 15
function alpha() {
          ^
...
// delta: line 24, column 24
const delta = function () {
                       ^
...
// gamma: line 31, column 1
const gamma =
() => {
^
```

If we look up the source map entry that corresponds to the opening paren, we'll nearly always get nothing. Instead, we'll look at the entry *preceding* the one which contains the opening paren, and hope that has our symbol name. It seems this works at least some of the time.

Another complication is that some, but not all source maps include the original names of functions. For ones that don't, but do include the original source-code, we try to deduce it ourselves with varying amounts of success.

Supersedes #306
Fixes #139
2020-10-12 18:03:31 -07:00
Gabriele N. Tornetta 177696e359 Update link to Austin instructions (#319)
The Austin format conversion tools have been moved to the dedicated austin-python module. The README has been updated to point to the new instructions.
2020-10-11 12:05:20 -07:00
Jamie Wong 16e32dc08e Normalize line & column numbers to be 1-based in Chrome & Firefox imports (#318)
This also fixes a dumb bug in the Firefox import that just completely failed to import column numbers.
2020-10-02 14:56:22 -07:00
Jamie Wong 8a4f38a8cb 1.10.0 2020-09-29 18:51:49 -07:00
Jamie Wong 9361a6baf2 Switch from Travis CI to GitHub Actions for test runs (#316)
Switch from Travis CI to GitHub Actions for test runs
2020-09-29 16:39:18 -07:00
Jamie Wong ede9c74d50 Remove accidentally added/retained dependencies on react and react-redux (#315)
speedscope no longer relies upon react-redux, and never depended upon react. Let's clean these up.
2020-09-29 16:20:27 -07:00
Jamie Wong f758130455 Add support for imports of UTF-16 encoded text w/ Byte Order Mark (BOM) (#314)
Before this PR, we blindly assumed that all text imported into speedscope was UTF-8 encoded. This, unsurprisingly, is not always true. After this PR, we support text that's UTF-16 encoded, with either the little-endian or big-endian byte-order-mark.

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

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

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

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

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

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

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

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

## Problem

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

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

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

## Solution

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #212 
Fixes #155 
Fixes #74 (though this was maybe already fixed)
2020-07-18 22:37:15 -07:00
113 changed files with 23251 additions and 1711 deletions
+43
View File
@@ -0,0 +1,43 @@
name: Node.js CI
on:
push:
branches:
- master
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
env:
CI: true
- name: Coveralls Parallel
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
flag-name: run-${{ matrix.node-version }}
parallel: true
finish:
needs: test
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
-6
View File
@@ -1,6 +0,0 @@
language: node_js
node_js:
- '10'
- '12'
- '13'
- 'node'
+64 -1
View File
@@ -1,4 +1,67 @@
## Unreleased
## [1.12.1] - 2020-11-12
### Fixed
- Fixed for retina displays [[#327](https://github.com/jlfwong/speedscope/issues/327)]
## [1.12.0] - 2020-11-12
### Added
- Dark mode! [[#323](https://github.com/jlfwong/speedscope/pull/323)]
### Fixed
- Fixed incorrect highlighting when search result overlaps "…" [[#326](https://github.com/jlfwong/speedscope/pull/326)]
## [1.11.1] - 2020-10-25
### Fixed
- Fix trace-event import for many cases where there are 'ts' collisions [[#322](https://github.com/jlfwong/speedscope/pull/322)]
- Fix import of trace event files where B/E events' args don't match [[#321](https://github.com/jlfwong/speedscope/pull/321)]
## [1.11.0] - 2020-10-13
### Added
- Support remapping profiles using source maps [[#317](https://github.com/jlfwong/speedscope/pull/317)]
### Fixed
- Fix line & column numbers in imports from Chrome & Firefox [[#318](https://github.com/jlfwong/speedscope/pull/318)]
## [1.10.0] - 2020-09-29
### Added
- Support for importing profiles from Safari [[#300](https://github.com/jlfwong/speedscope/pull/300)] (by [@radex](https://github.com/radex))
### Fixed
- Fixed browser not opening on Windows when using the CLI [[#307](https://github.com/jlfwong/speedscope/pull/307)] (by [@spillerrec](https://github.com/spillerrec))
- Fixed import of UTF-16 encoded files w/ BOM [[#314](https://github.com/jlfwong/speedscope/pull/314)]
- Removed accidental dependency on React [[#315](https://github.com/jlfwong/speedscope/pull/315)]
## [1.9.0] - 2020-08-05
### Added
- Provide prev/next buttons to cycle through search results, make search results more visually prominen [[#304](https://github.com/jlfwong/speedscope/pull/304)]
### Fixed
- Fix accumulated errors in Chrome profile imports caused by zeroed negative timeDeltas [[#305](https://github.com/jlfwong/speedscope/pull/305)] (by [@taneliang](https://github.com/taneliang))
## [1.8.0] - 2020-07-19
### Added
- Added search highlighting in time order & left heavy views [[#297](https://github.com/jlfwong/speedscope/pull/297)]
### Fixed
- Fix performance issues for the caller/callee flamegraphs in the sandwich view [[#296](https://github.com/jlfwong/speedscope/pull/296)]
## [1.7.0] - 2020-07-13
+3
View File
@@ -36,6 +36,7 @@ speedscope is designed to ingest profiles from a variety of different profilers
- JavaScript
- [Importing from Chrome](https://github.com/jlfwong/speedscope/wiki/Importing-from-Chrome)
- [Importing from Firefox](https://github.com/jlfwong/speedscope/wiki/Importing-from-Firefox)
- [Importing from Safari](https://github.com/jlfwong/speedscope/wiki/Importing-from-Safari)
- [Importing from Node.js](https://github.com/jlfwong/speedscope/wiki/Importing-from-Node.js)
- Ruby
- [Importing from stackprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-stackprof-(ruby))
@@ -44,6 +45,7 @@ speedscope is designed to ingest profiles from a variety of different profilers
- Python
- [Importing from py-spy](https://github.com/jlfwong/speedscope/wiki/Importing-from-py-spy-(python))
- [pyspeedscope](https://github.com/windelbouwman/pyspeedscope)
- [Importing from Austin](https://github.com/P403n1x87/austin-python#format-conversion)
- Go
- [Importing from pprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-pprof-(go))
- Rust
@@ -116,6 +118,7 @@ Once a profile has loaded, the main view is split into two: the top area is the
* `n`: Go to next profile/thread if one is available
* `p`: Go to previous profile/thread if one is available
* `t`: Open the profile/thread selector if available
* `Cmd+F`/`Ctrl+F`: to open search. While open, `Enter` and `Shift+Enter` cycle through results
## Contributing
+6 -2
View File
@@ -4,7 +4,7 @@ const fs = require('fs')
const os = require('os')
const stream = require('stream')
const opn = require('opn')
const open = require('open')
const helpString = `Usage: speedscope [filepath]
@@ -89,7 +89,11 @@ async function main() {
console.log('Opening', urlToOpen, 'in your default browser')
await opn(urlToOpen, {wait: false})
// We'd like to avoid blocking the terminal on the browsing closing,
// but for some reason this doesn't work at all on Windows if we
// don't use wait: true.
const wait = process.platform === "win32";
await open(urlToOpen, {wait})
}
main()
+188 -101
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "1.5.3",
"version": "1.11.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1432,6 +1432,12 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
@@ -1457,6 +1463,12 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
@@ -1518,6 +1530,12 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
@@ -3423,27 +3441,6 @@
"parse-json": "^4.0.0"
}
},
"coveralls": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.1.tgz",
"integrity": "sha512-FAzXwiDOYLGDWH+zgoIA+8GbWv50hlx+kpEJyvzLKOdnIBv9uWoVl4DhqGgyUHpiRjAlF8KYZSipWXYtllWH6Q==",
"dev": true,
"requires": {
"js-yaml": "^3.6.1",
"lcov-parse": "^0.0.10",
"log-driver": "^1.2.5",
"minimist": "^1.2.0",
"request": "^2.79.0"
},
"dependencies": {
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
}
}
},
"create-ecdh": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
@@ -3653,6 +3650,14 @@
"requires": {
"mdn-data": "2.0.4",
"source-map": "^0.6.1"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"css-what": {
@@ -3761,6 +3766,14 @@
"requires": {
"mdn-data": "2.0.6",
"source-map": "^0.6.1"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"mdn-data": {
@@ -4194,6 +4207,13 @@
"resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
"integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true
}
}
},
@@ -5793,15 +5813,6 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dev": true,
"requires": {
"react-is": "^16.7.0"
}
},
"hosted-git-info": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz",
@@ -5882,6 +5893,14 @@
"commander": "^2.20.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.12"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
}
}
@@ -6249,6 +6268,11 @@
"integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
"dev": true
},
"is-docker": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz",
"integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw=="
},
"is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@@ -6397,7 +6421,8 @@
"is-wsl": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
"dev": true
},
"isarray": {
"version": "1.0.0",
@@ -6518,6 +6543,12 @@
"requires": {
"glob": "^7.1.3"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
@@ -6876,6 +6907,14 @@
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"supports-color": {
@@ -7020,6 +7059,12 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
@@ -7076,7 +7121,8 @@
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
"dev": true
},
"js-yaml": {
"version": "3.13.1",
@@ -7283,12 +7329,6 @@
"integrity": "sha1-iAy4qrJWAmOC4C9T7AiWgqdMW2o=",
"dev": true
},
"lcov-parse": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz",
"integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=",
"dev": true
},
"left-pad": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
@@ -7395,12 +7435,6 @@
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
"dev": true
},
"log-driver": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz",
"integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==",
"dev": true
},
"log-symbols": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
@@ -7420,6 +7454,7 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
"dev": true,
"requires": {
"js-tokens": "^3.0.0"
}
@@ -7885,7 +7920,8 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
},
"object-copy": {
"version": "0.1.0",
@@ -8139,10 +8175,30 @@
"mimic-fn": "^1.0.0"
}
},
"open": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/open/-/open-7.2.0.tgz",
"integrity": "sha512-4HeyhxCvBTI5uBePsAdi55C5fmqnWZ2e2MlmvWi5KW5tdH5rxoiv/aMtbeVxKZc3eWkT1GymMnLG8XC4Rq4TDQ==",
"requires": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
},
"dependencies": {
"is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"requires": {
"is-docker": "^2.0.0"
}
}
}
},
"opn": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
"integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==",
"dev": true,
"requires": {
"is-wsl": "^1.1.0"
}
@@ -8310,6 +8366,12 @@
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
@@ -8568,6 +8630,12 @@
}
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
@@ -8795,6 +8863,14 @@
"chalk": "^2.4.1",
"source-map": "^0.6.1",
"supports-color": "^5.4.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"supports-color": {
@@ -8847,6 +8923,14 @@
"chalk": "^2.4.1",
"source-map": "^0.6.1",
"supports-color": "^5.4.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"supports-color": {
@@ -8899,6 +8983,14 @@
"chalk": "^2.4.1",
"source-map": "^0.6.1",
"supports-color": "^5.4.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"supports-color": {
@@ -8951,6 +9043,14 @@
"chalk": "^2.4.1",
"source-map": "^0.6.1",
"supports-color": "^5.4.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"supports-color": {
@@ -9244,26 +9344,6 @@
"sisteransi": "^1.0.3"
}
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
},
"dependencies": {
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
}
}
},
"protobufjs": {
"version": "6.8.8",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz",
@@ -9512,44 +9592,11 @@
"integrity": "sha1-CMbgSgFo9utiHCKrbLEVG9n0pk0=",
"dev": true
},
"react": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz",
"integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2"
}
},
"react-is": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
},
"react-redux": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz",
"integrity": "sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.5.5",
"hoist-non-react-statics": "^3.3.0",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-is": "^16.9.0"
},
"dependencies": {
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
}
}
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==",
"dev": true
},
"read-pkg": {
"version": "3.0.0",
@@ -10315,6 +10362,14 @@
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"source-map-url": {
@@ -10418,6 +10473,15 @@
"esutils": "^2.0.2",
"optionator": "^0.8.1",
"source-map": "~0.6.1"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true
}
}
}
}
@@ -11183,6 +11247,14 @@
"commander": "^2.19.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.10"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"test-exclude": {
@@ -11616,6 +11688,12 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz",
"integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
@@ -11671,6 +11749,15 @@
"esutils": "^2.0.2",
"optionator": "^0.8.1",
"source-map": "~0.6.1"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true
}
}
},
"extend": {
+4 -6
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "1.7.0",
"version": "1.12.1",
"description": "",
"repository": "jlfwong/speedscope",
"main": "index.js",
@@ -13,7 +13,7 @@
"prettier": "prettier --write 'src/**/*.ts' 'src/**/*.tsx'",
"lint": "eslint 'src/**/*.ts' 'src/**/*.tsx'",
"jest": "./scripts/test-setup.sh && jest --runInBand",
"coverage": "npm run jest -- --coverage && coveralls < coverage/lcov.info",
"coverage": "npm run jest -- --coverage",
"typecheck": "tsc --noEmit",
"test": "./scripts/ci.sh",
"serve": "parcel assets/index.html --open --no-autoinstall"
@@ -38,7 +38,6 @@
"@typescript-eslint/parser": "2.33.0",
"acorn": "7.2.0",
"aphrodite": "2.1.0",
"coveralls": "3.0.1",
"eslint": "6.0.0",
"eslint-plugin-prettier": "2.6.0",
"eslint-plugin-react-hooks": "4.0.2",
@@ -50,8 +49,8 @@
"preact": "10.4.1",
"prettier": "2.0.4",
"protobufjs": "6.8.8",
"react-redux": "^7.2.0",
"redux": "^4.0.5",
"source-map": "0.6.1",
"ts-jest": "24.3.0",
"typescript": "3.9.2",
"typescript-eslint-parser": "17.0.1",
@@ -79,7 +78,6 @@
]
},
"dependencies": {
"opn": "5.3.0",
"react": "^16.13.1"
"open": "7.2.0"
}
}
File diff suppressed because one or more lines are too long
+4
View File
@@ -0,0 +1,4 @@
This directory contains profiles & source-maps to test if source-map
remapping of profiles is working correctly. See the corresponding
"sourcemaps" directory in programs/javascript to see how these were
generated.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["gamma.ts", "beta.ts", "delta.ts", "alpha.ts", "kludge.ts", "typescript-source-map-test.ts"],
"sourcesContent": ["export const gamma = () => {\n let prod = 1\n for (let i = 1; i < 1000; i++) {\n prod *= i\n }\n return prod\n}\n", "import {gamma} from './gamma'\n\nexport function beta() {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n", "import {gamma} from './gamma'\n\nexport const delta = function () {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n", "import {beta} from './beta'\nimport {delta} from './delta'\n\nexport function alpha() {\n ;(function () {\n for (let i = 0; i < 1000; i++) {\n beta()\n delta()\n }\n })()\n}\n", "import {alpha} from './alpha'\n\nexport class Kludge {\n constructor() {\n alpha()\n console.log(this.floop)\n }\n\n zap() {\n alpha()\n }\n\n get floop(): number {\n alpha()\n return 1\n }\n}\n", "import {Kludge} from './kludge'\n\nconst k = new Kludge()\nk.zap()\n"],
"mappings": "MAAO,KAAM,GAAQ,KACnB,GAAI,GAAO,EACX,OAAS,GAAI,EAAG,EAAI,IAAM,IACxB,GAAQ,EAEV,MAAO,ICHF,aACL,OAAS,GAAI,EAAG,EAAI,GAAI,IACtB,ICFG,KAAM,GAAQ,WACnB,OAAS,GAAI,EAAG,EAAI,GAAI,IACtB,KCDG,aACJ,AAAC,YACA,OAAS,GAAI,EAAG,EAAI,IAAM,IACxB,IACA,QCPN,QAGE,cACE,IACA,QAAQ,IAAI,KAAK,OAGnB,MACE,OAGE,SACF,WACO,GCZX,KAAM,GAAI,GAAI,GACd,EAAE",
"names": []
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"version":3,"sources":["gamma.ts","beta.ts","delta.ts","alpha.ts","kludge.ts","typescript-source-map-test.ts"],"names":[],"mappings":";AAAO,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,WAAA,EAAA,IAAM,EAAQ,WAEd,IADD,IAAA,EAAO,EACF,EAAI,EAAG,EAAI,IAAM,IACxB,GAAQ,EAEH,OAAA,GALF,QAAA,MAAA;;ACMN,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,KAAA,EAND,IAAA,EAAA,QAAA,WAEM,SAAU,IACT,IAAA,IAAI,EAAI,EAAG,EAAI,GAAI,KACtB,EAAA,EAAA;;ACFG,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,WAAA,EAFP,IAAA,EAAA,QAAA,WAEa,EAAQ,WACd,IAAA,IAAI,EAAI,EAAG,EAAI,GAAI,KACtB,EAAA,EAAA,UAFG,QAAA,MAAA;;ACQN,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,MAAA,EAVD,IAAA,EAAA,QAAA,UACA,EAAA,QAAA,WAEM,SAAU,KACZ,WACK,IAAA,IAAI,EAAI,EAAG,EAAI,IAAM,KACxB,EAAA,EAAA,SACA,EAAA,EAAA,SAHF;;ACFJ,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,YAAA,EAFA,IAAA,EAAA,QAAA,WAEA,EAAA,WACE,SAAA,KACE,EAAA,EAAA,SACA,QAAQ,IAAI,KAAK,OAWrB,OARE,EAAA,UAAA,IAAA,YACE,EAAA,EAAA,UAGF,OAAA,eAAI,EAAA,UAAA,QAAK,CAAT,IAAA,WAES,OADP,EAAA,EAAA,SACO,GAFA,YAAA,EAVX,cAAA,IAcA,EAdA,GAAA,QAAA,OAAA;;ACCA,aAHA,IAAA,EAAA,QAAA,YAEM,EAAI,IAAI,EAAJ,OACV,EAAE","file":"typescript-source-map-test.js","sourceRoot":"..","sourcesContent":["export const gamma = () => {\n let prod = 1\n for (let i = 1; i < 1000; i++) {\n prod *= i\n }\n return prod\n}\n","import {gamma} from './gamma'\n\nexport function beta() {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n","import {gamma} from './gamma'\n\nexport const delta = function () {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n","import {beta} from './beta'\nimport {delta} from './delta'\n\nexport function alpha() {\n ;(function () {\n for (let i = 0; i < 1000; i++) {\n beta()\n delta()\n }\n })()\n}\n","import {alpha} from './alpha'\n\nexport class Kludge {\n constructor() {\n alpha()\n console.log(this.floop)\n }\n\n zap() {\n alpha()\n }\n\n get floop(): number {\n alpha()\n return 1\n }\n}\n","import {Kludge} from './kludge'\n\nconst k = new Kludge()\nk.zap()\n"]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"version":3,"sources":["webpack://speedscope-sourcemap-test-project/./gamma.ts","webpack://speedscope-sourcemap-test-project/./beta.ts","webpack://speedscope-sourcemap-test-project/./delta.ts","webpack://speedscope-sourcemap-test-project/./alpha.ts","webpack://speedscope-sourcemap-test-project/./typescript-source-map-test.ts","webpack://speedscope-sourcemap-test-project/./kludge.ts"],"names":["gamma","prod","i","beta","delta","alpha","console","log","this","floop","zap"],"mappings":"mBAAO,IAAMA,EAAQ,WAEnB,IADA,IAAIC,EAAO,EACFC,EAAI,EAAGA,EAAI,IAAMA,IACxBD,GAAQC,EAEV,OAAOD,GCHF,SAASE,IACd,IAAK,IAAID,EAAI,EAAGA,EAAI,GAAIA,IACtBF,ICFG,IAAMI,EAAQ,WACnB,IAAK,IAAIF,EAAI,EAAGA,EAAI,GAAIA,IACtBF,KCDG,SAASK,KACb,WACC,IAAK,IAAIH,EAAI,EAAGA,EAAI,IAAMA,IACxBC,IACAC,IAHH,ICFO,ICAV,WACE,aACEC,IACAC,QAAQC,IAAIC,KAAKC,OAWrB,OARE,YAAAC,IAAA,WACEL,KAGF,sBAAI,oBAAK,C,IAAT,WAEE,OADAA,IACO,G,gCAEX,EAdA,KDCEK,O","file":"typescript-source-map-test.js","sourcesContent":["export const gamma = () => {\n let prod = 1\n for (let i = 1; i < 1000; i++) {\n prod *= i\n }\n return prod\n}\n","import {gamma} from './gamma'\n\nexport function beta() {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n","import {gamma} from './gamma'\n\nexport const delta = function () {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n","import {beta} from './beta'\nimport {delta} from './delta'\n\nexport function alpha() {\n ;(function () {\n for (let i = 0; i < 1000; i++) {\n beta()\n delta()\n }\n })()\n}\n","import {Kludge} from './kludge'\n\nconst k = new Kludge()\nk.zap()\n","import {alpha} from './alpha'\n\nexport class Kludge {\n constructor() {\n alpha()\n console.log(this.floop)\n }\n\n zap() {\n alpha()\n }\n\n get floop(): number {\n alpha()\n return 1\n }\n}\n"],"sourceRoot":""}
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,25 @@
[
{ "pid": 0, "tid": 0, "ph": "B", "ts": 0, "name": "A" },
{ "pid": 0, "tid": 0, "ph": "E", "ts": 2, "name": "A" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 2, "name": "B" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 10, "dur": 2, "name": "C" },
{ "pid": 0, "tid": 0, "ph": "B", "ts": 10, "name": "D" },
{ "pid": 0, "tid": 0, "ph": "E", "ts": 12, "name": "D" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 20, "dur": 1, "name": "E" },
{ "pid": 0, "tid": 0, "ph": "B", "ts": 20, "name": "F" },
{ "pid": 0, "tid": 0, "ph": "E", "ts": 22, "name": "F" },
{ "pid": 0, "tid": 0, "ph": "B", "ts": 30, "name": "G" },
{ "pid": 0, "tid": 0, "ph": "E", "ts": 32, "name": "G" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 30, "dur": 1, "name": "H" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 41, "dur": 1, "name": "I" },
{ "pid": 0, "tid": 0, "ph": "B", "ts": 40, "name": "J" },
{ "pid": 0, "tid": 0, "ph": "E", "ts": 42, "name": "J" },
{ "pid": 0, "tid": 0, "ph": "B", "ts": 50, "name": "K" },
{ "pid": 0, "tid": 0, "ph": "E", "ts": 52, "name": "K" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 51, "dur": 1, "name": "L" }
]
@@ -0,0 +1,5 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 1},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 2}
]
@@ -0,0 +1,6 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 1}, "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "B", "args": {"x": 2}, "ts": 1},
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "args": {"x": 2}, "ts": 10},
{"pid": 0, "tid": 0, "ph": "X", "name": "Z", "args": {"x": 1}, "ts": 10, "dur": 1}
]
@@ -0,0 +1,7 @@
[
{"tid": 1, "ph": "X", "pid": 0, "name": "alpha", "args": {"x": 0}, "ts": 0, "dur": 10},
{"tid": 1, "ph": "B", "pid": 0, "name": "beta", "args": {"x": 0}, "ts": 1},
{"tid": 1, "ph": "B", "pid": 0, "name": "gamma", "args": {"x": 0}, "ts": 1},
{"tid": 1, "ph": "E", "pid": 0, "name": "beta", "args": {"x": 1}, "ts": 2},
{"tid": 1, "ph": "E", "pid": 0, "name": "gamma", "args": {"x": 1}, "ts": 2}
]
@@ -0,0 +1,4 @@
[
{ "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 10, "name": "alpha" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 10, "name": "beta" }
]
@@ -0,0 +1,7 @@
[
{ "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 10, "name": "alpha" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 1, "dur": 1, "name": "beta" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 1, "dur": 1, "name": "gamma" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 1, "name": "gamma" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 1, "name": "beta" }
]
@@ -0,0 +1,4 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 1}, "ts": 0},
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "args": {"x": 2}, "ts": 10}
]
@@ -0,0 +1,4 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 10}
]
@@ -0,0 +1,5 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 1}, "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 2}, "ts": 1},
{"pid": 0, "tid": 0, "ph": "X", "name": "A", "args": {"x": 1}, "ts": 10, "dur": 1}
]
@@ -0,0 +1,5 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 1}, "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "B", "args": {"x": 2}, "ts": 1},
{"pid": 0, "tid": 0, "ph": "B", "name": "C", "args": {"x": 2}, "ts": 2}
]
@@ -0,0 +1,6 @@
[
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 9},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 10},
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1}
]
@@ -0,0 +1,10 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "B", "ts": 1},
{"pid": 0, "tid": 0, "ph": "E", "name": "B", "ts": 9},
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "ts": 0},
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "ts": 10},
{"pid": 0, "tid": 0, "ph": "B", "name": "C", "ts": 2},
{"pid": 0, "tid": 0, "ph": "E", "name": "C", "ts": 8}
]
@@ -0,0 +1,6 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 1}, "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 2}, "ts": 1},
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "args": {"x": 1}, "ts": 9},
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "args": {"x": 2}, "ts": 10}
]
@@ -0,0 +1,6 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 9},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 10}
]
@@ -0,0 +1,15 @@
[
{ "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 20, "name": "alpha" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 1, "dur": 2, "name": "beta" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 2, "dur": 1, "name": "gamma" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 2, "name": "gamma" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 6, "dur": 1, "name": "beta" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 10, "dur": 1, "name": "gamma" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 9, "dur": 2, "name": "beta" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 15, "dur": 1, "name": "beta" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 14, "dur": 2, "name": "gamma" }
]
@@ -0,0 +1,15 @@
[
{ "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 20, "name": "alpha" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 1, "dur": 2, "name": "beta" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 1, "dur": 1, "name": "gamma" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 2, "name": "gamma" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 1, "name": "beta" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 9, "dur": 1, "name": "beta" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 9, "dur": 2, "name": "gamma" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 13, "dur": 1, "name": "gamma" },
{ "pid": 0, "tid": 0, "ph": "X", "ts": 13, "dur": 2, "name": "beta" }
]
@@ -0,0 +1,9 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "B", "ts": 1},
{"pid": 0, "tid": 0, "ph": "E", "name": "B", "ts": 1},
{"pid": 0, "tid": 0, "ph": "E", "name": "C", "ts": 2},
{"pid": 0, "tid": 0, "ph": "B", "name": "C", "ts": 2},
{"pid": 0, "tid": 0, "ph": "X", "name": "D", "ts": 3, "dur": 0},
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "ts": 10}
]
@@ -0,0 +1,22 @@
## Source Map Test Project
This directory contains test files used to test whether the remapping of
performance profiles using sourcemaps work correctly.
Run `npm run build` to build the artifacts, then open the appropriate files in
the `html` directory in whatever browser you're testing.
The idea is to sourcemaps generated by a variety of tools, and also to take
profiles from a variety of browsers, and hopefully see that they all get
remapped as expected.
This project is set up to go through three levels of source-map indirection,
and also using multiple different build chains.
1. TypeScript -> JavaScript source generation
2. JavaScript source -> JavaScript bundling
3. Minification
Some bundlers will swap the order of steps 2 & 3, or potentially merge them,
but it's complex yet realistic enough that this will hoepfully suss out
problems.
@@ -0,0 +1,11 @@
import {beta} from './beta'
import {delta} from './delta'
export function alpha() {
;(function () {
for (let i = 0; i < 1000; i++) {
beta()
delta()
}
})()
}
@@ -0,0 +1,7 @@
import {gamma} from './gamma'
export function beta() {
for (let i = 0; i < 10; i++) {
gamma()
}
}
@@ -0,0 +1,7 @@
import {gamma} from './gamma'
export const delta = function () {
for (let i = 0; i < 10; i++) {
gamma()
}
}
@@ -0,0 +1,7 @@
export const gamma = () => {
let prod = 1
for (let i = 1; i < 1000; i++) {
prod *= i
}
return prod
}
@@ -0,0 +1,3 @@
<title>ESbuild</title>
<script src="../dist/esbuild/typescript-source-map-test.js"></script>
<h1>ESbuild Source Map Test</h1>
@@ -0,0 +1,3 @@
<title>Parcel</title>
<script src="../dist/parcel/typescript-source-map-test.js"></script>
<h1>Parcel Source Map Test</h1>
@@ -0,0 +1,3 @@
<title>Webpack</title>
<script src="../dist/webpack/typescript-source-map-test.js"></script>
<h1>Webpack Source Map Test</h1>
@@ -0,0 +1,17 @@
import {alpha} from './alpha'
export class Kludge {
constructor() {
alpha()
console.log(this.floop)
}
zap() {
alpha()
}
get floop(): number {
alpha()
return 1
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
{
"name": "speedscope-sourcemap-test-project",
"version": "1.0.0",
"description": "",
"private": "true",
"main": "index.js",
"scripts": {
"build": "npm run parcel && npm run webpack && npm run esbuild",
"parcel": "parcel build -o parcel/typescript-source-map-test typescript-source-map-test.ts",
"webpack": "webpack",
"esbuild": "esbuild --sourcemap --minify --bundle --outdir=dist/esbuild typescript-source-map-test.ts"
},
"author": "",
"license": "ISC",
"devDependencies": {
"esbuild": "^0.7.14",
"parcel": "^1.12.4",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^5.0.0",
"webpack-cli": "^4.0.0"
}
}
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"sourceMap": true,
"module": "es6",
"target": "es5",
"jsx": "react"
}
}
@@ -0,0 +1,4 @@
import {Kludge} from './kludge'
const k = new Kludge()
k.zap()
@@ -0,0 +1,22 @@
const path = require('path')
module.exports = {
entry: './typescript-source-map-test.ts',
devtool: 'source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'typescript-source-map-test.js',
path: path.resolve(__dirname, 'dist', 'webpack'),
},
}
+13 -4
View File
@@ -4,6 +4,8 @@ import {TextureRenderer} from './texture-renderer'
import {Rect, Vec2} from '../lib/math'
import {ViewportRectangleRenderer} from './overlay-rectangle-renderer'
import {FlamechartColorPassRenderer} from './flamechart-color-pass-renderer'
import {Color} from '../lib/color'
import {Theme} from '../views/themes/theme'
type FrameCallback = () => void
@@ -13,13 +15,19 @@ export class CanvasContext {
public readonly textureRenderer: TextureRenderer
public readonly viewportRectangleRenderer: ViewportRectangleRenderer
public readonly flamechartColorPassRenderer: FlamechartColorPassRenderer
public readonly theme: Theme
constructor(canvas: HTMLCanvasElement) {
constructor(canvas: HTMLCanvasElement, theme: Theme) {
this.gl = new WebGL.Context(canvas)
this.rectangleBatchRenderer = new RectangleBatchRenderer(this.gl)
this.textureRenderer = new TextureRenderer(this.gl)
this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl)
this.flamechartColorPassRenderer = new FlamechartColorPassRenderer(this.gl)
this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl, theme)
this.flamechartColorPassRenderer = new FlamechartColorPassRenderer(this.gl, theme)
this.theme = theme
// Whenever the canvas is resized, draw immediately. This prevents
// flickering during resizing.
this.gl.addAfterResizeEventHandler(this.onBeforeFrame)
const webGLInfo = this.gl.getWebGLInfo()
if (webGLInfo) {
@@ -48,7 +56,8 @@ export class CanvasContext {
private onBeforeFrame = () => {
this.animationFrameRequest = null
this.gl.setViewport(0, 0, this.gl.renderTargetWidthInPixels, this.gl.renderTargetHeightInPixels)
this.gl.clear(new Graphics.Color(1, 1, 1, 1))
const color = Color.fromCSSHex(this.theme.bgPrimaryColor)
this.gl.clear(new Graphics.Color(color.r, color.g, color.b, color.a))
for (const handler of this.beforeFrameHandlers) {
handler()
+5 -10
View File
@@ -1,4 +1,5 @@
import {Vec2, Rect, AffineTransform} from '../lib/math'
import {Theme} from '../views/themes/theme'
import {Graphics} from './graphics'
import {setUniformAffineTransform} from './utils'
@@ -20,7 +21,7 @@ const vert = `
}
`
const frag = `
const frag = (colorForBucket: string) => `
precision mediump float;
uniform vec2 uvSpacePixelSize;
@@ -49,13 +50,7 @@ const frag = `
return 2.0 * abs(fract(x) - 0.5) - 1.0;
}
vec3 colorForBucket(float t) {
float x = triangle(30.0 * t);
float H = 360.0 * (0.9 * t);
float C = 0.25 + 0.2 * x;
float L = 0.80 - 0.15 * x;
return hcl2rgb(H, C, L);
}
${colorForBucket}
void main() {
vec4 here = texture2D(colorTexture, vUv);
@@ -107,7 +102,7 @@ export class FlamechartColorPassRenderer {
private material: Graphics.Material
private buffer: Graphics.VertexBuffer
constructor(private gl: Graphics.Context) {
constructor(private gl: Graphics.Context, theme: Theme) {
const vertices = [
{pos: [-1, 1], uv: [0, 1]},
{pos: [1, 1], uv: [1, 1]},
@@ -124,7 +119,7 @@ export class FlamechartColorPassRenderer {
this.buffer = gl.createVertexBuffer(vertexFormat.stride * vertices.length)
this.buffer.uploadFloats(floats)
this.material = gl.createMaterial(vertexFormat, vert, frag)
this.material = gl.createMaterial(vertexFormat, vert, frag(theme.colorForBucketGLSL))
}
render(props: FlamechartColorPassRenderProps) {
+21 -2
View File
@@ -83,6 +83,15 @@ export namespace Graphics {
public alphaF: number,
) {}
equals(other: Color): boolean {
return (
this.redF === other.redF &&
this.greenF === other.greenF &&
this.blueF === other.blueF &&
this.alphaF === other.alphaF
)
}
static TRANSPARENT = new Color(0, 0, 0, 0)
}
@@ -160,6 +169,14 @@ export namespace Graphics {
setUnpremultipliedBlendState() {
this.setBlendState(BlendOperation.SOURCE_ALPHA, BlendOperation.INVERSE_SOURCE_ALPHA)
}
protected resizeEventHandlers = new Set<() => void>()
addAfterResizeEventHandler(callback: () => void): void {
this.resizeEventHandlers.add(callback)
}
removeAfterResizeEventHandler(callback: () => void): void {
this.resizeEventHandlers.delete(callback)
}
}
export interface Material {
@@ -459,7 +476,7 @@ export namespace WebGL {
const bounds = canvas.getBoundingClientRect()
if (
this._width === widthInAppUnits &&
this._width === widthInPixels &&
this._height === heightInPixels &&
bounds.width === widthInAppUnits &&
bounds.height === heightInAppUnits
@@ -476,13 +493,15 @@ export namespace WebGL {
this.setViewport(0, 0, widthInPixels, heightInPixels)
this._width = widthInPixels
this._height = heightInPixels
this.resizeEventHandlers.forEach(cb => cb())
}
clear(color: Graphics.Color) {
this._updateRenderTargetAndViewport()
this._updateBlendState()
if (color != this._currentClearColor) {
if (!color.equals(this._currentClearColor)) {
this._gl.clearColor(color.redF, color.greenF, color.blueF, color.alphaF)
this._currentClearColor = color
}
+41 -35
View File
@@ -1,4 +1,6 @@
import {Color} from '../lib/color'
import {AffineTransform, Rect} from '../lib/math'
import {Theme} from '../views/themes/theme'
import {Graphics} from './graphics'
import {setUniformAffineTransform, setUniformVec2} from './utils'
@@ -18,53 +20,57 @@ const vert = `
}
`
const frag = `
precision mediump float;
const frag = (theme: Theme) => {
const {r, g, b} = Color.fromCSSHex(theme.fgSecondaryColor)
const rgb = `${r.toFixed(1)}, ${g.toFixed(1)}, ${b.toFixed(1)}`
return `
precision mediump float;
uniform mat3 configSpaceToPhysicalViewSpace;
uniform vec2 physicalSize;
uniform vec2 physicalOrigin;
uniform vec2 configSpaceViewportOrigin;
uniform vec2 configSpaceViewportSize;
uniform float framebufferHeight;
uniform mat3 configSpaceToPhysicalViewSpace;
uniform vec2 physicalSize;
uniform vec2 physicalOrigin;
uniform vec2 configSpaceViewportOrigin;
uniform vec2 configSpaceViewportSize;
uniform float framebufferHeight;
void main() {
vec2 origin = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportOrigin, 1.0)).xy;
vec2 size = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportSize, 0.0)).xy;
void main() {
vec2 origin = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportOrigin, 1.0)).xy;
vec2 size = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportSize, 0.0)).xy;
vec2 halfSize = physicalSize / 2.0;
vec2 halfSize = physicalSize / 2.0;
float borderWidth = 2.0;
float borderWidth = 2.0;
origin = floor(origin * halfSize) / halfSize + borderWidth * vec2(1.0, 1.0);
size = floor(size * halfSize) / halfSize - 2.0 * borderWidth * vec2(1.0, 1.0);
origin = floor(origin * halfSize) / halfSize + borderWidth * vec2(1.0, 1.0);
size = floor(size * halfSize) / halfSize - 2.0 * borderWidth * vec2(1.0, 1.0);
vec2 coord = gl_FragCoord.xy;
coord.x = coord.x - physicalOrigin.x;
coord.y = framebufferHeight - coord.y - physicalOrigin.y;
vec2 clamped = clamp(coord, origin, origin + size);
vec2 gap = clamped - coord;
float maxdist = max(abs(gap.x), abs(gap.y));
vec2 coord = gl_FragCoord.xy;
coord.x = coord.x - physicalOrigin.x;
coord.y = framebufferHeight - coord.y - physicalOrigin.y;
vec2 clamped = clamp(coord, origin, origin + size);
vec2 gap = clamped - coord;
float maxdist = max(abs(gap.x), abs(gap.y));
// TOOD(jlfwong): Could probably optimize this to use mix somehow.
if (maxdist == 0.0) {
// Inside viewport rectangle
gl_FragColor = vec4(0, 0, 0, 0);
} else if (maxdist < borderWidth) {
// Inside viewport rectangle at border
gl_FragColor = vec4(0.7, 0.7, 0.7, 0.8);
} else {
// Outside viewport rectangle
gl_FragColor = vec4(0.7, 0.7, 0.7, 0.5);
// TOOD(jlfwong): Could probably optimize this to use mix somehow.
if (maxdist == 0.0) {
// Inside viewport rectangle
gl_FragColor = vec4(0, 0, 0, 0);
} else if (maxdist < borderWidth) {
// Inside viewport rectangle at border
gl_FragColor = vec4(${rgb}, 0.8);
} else {
// Outside viewport rectangle
gl_FragColor = vec4(${rgb}, 0.5);
}
}
}
`
`
}
export class ViewportRectangleRenderer {
private material: Graphics.Material
private buffer: Graphics.VertexBuffer
constructor(private gl: Graphics.Context) {
constructor(private gl: Graphics.Context, theme: Theme) {
const vertices = [
[-1, 1],
[1, 1],
@@ -78,7 +84,7 @@ export class ViewportRectangleRenderer {
}
this.buffer = gl.createVertexBuffer(vertexFormat.stride * vertices.length)
this.buffer.upload(new Uint8Array(new Float32Array(floats).buffer))
this.material = gl.createMaterial(vertexFormat, vert, frag)
this.material = gl.createMaterial(vertexFormat, vert, frag(theme))
}
render(props: ViewportRectangleRendererProps) {
@@ -104,6 +104,114 @@ exports[`importFromBGFlameGraph with CRLF: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with CRLF: profileGroup.name 1`] = `"simple-crlf.txt"`;
exports[`importFromBGFlameGraph with UTF-16, Big Endian 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "a",
"line": undefined,
"name": "a",
"selfWeight": 0,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "b",
"line": undefined,
"name": "b",
"selfWeight": 5,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "c",
"line": undefined,
"name": "c",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "d",
"line": undefined,
"name": "d",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "simple-utf16-be.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
"a;b;c 3",
"a;b 5",
],
}
`;
exports[`importFromBGFlameGraph with UTF-16, Big Endian: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with UTF-16, Big Endian: profileGroup.name 1`] = `"simple-utf16-be.txt"`;
exports[`importFromBGFlameGraph with UTF-16, Little Endian 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "a",
"line": undefined,
"name": "a",
"selfWeight": 0,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "b",
"line": undefined,
"name": "b",
"selfWeight": 5,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "c",
"line": undefined,
"name": "c",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "d",
"line": undefined,
"name": "d",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "simple-utf16-le.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
"a;b;c 3",
"a;b 5",
],
}
`;
exports[`importFromBGFlameGraph with UTF-16, Little Endian: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with UTF-16, Little Endian: profileGroup.name 1`] = `"simple-utf16-le.txt"`;
exports[`importFromBGFlameGraph: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph: profileGroup.name 1`] = `"simple.txt"`;
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -64,17 +64,17 @@ exports[`importFromFirefox ignore self-hosted 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:1",
"col": 15,
"file": "http://localhost:8000/simple.js",
"key": "alpha (http://localhost:8000/simple.js:1:14)",
"line": 14,
"line": 1,
"name": "alpha",
"selfWeight": 0,
"totalWeight": 26.983816999942064,
},
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:14",
"col": 15,
"file": "http://localhost:8000/simple.js",
"key": "delta (http://localhost:8000/simple.js:14:14)",
"line": 14,
"name": "delta",
@@ -82,19 +82,19 @@ Object {
"totalWeight": 11.946324001066387,
},
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:20",
"col": 15,
"file": "http://localhost:8000/simple.js",
"key": "gamma (http://localhost:8000/simple.js:20:14)",
"line": 14,
"line": 20,
"name": "gamma",
"selfWeight": 26.983816999942064,
"totalWeight": 26.983816999942064,
},
Frame {
"col": undefined,
"file": "http://localhost:8000/simple.js:8",
"col": 14,
"file": "http://localhost:8000/simple.js",
"key": "beta (http://localhost:8000/simple.js:8:13)",
"line": 13,
"line": 8,
"name": "beta",
"selfWeight": 0,
"totalWeight": 15.037492998875678,
@@ -0,0 +1,101 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importFromSafari 1`] = `
Object {
"frames": Array [
Frame {
"col": 13,
"file": "__InjectedScript_InjectedScriptSource.js",
"key": "injectModule:__InjectedScript_InjectedScriptSource.js:109:13",
"line": 109,
"name": "injectModule",
"selfWeight": 0,
"totalWeight": 0.001,
},
Frame {
"col": 10,
"file": "__InjectedScript_CommandLineAPIModuleSource.js",
"key": ":__InjectedScript_CommandLineAPIModuleSource.js:2:10",
"line": 2,
"name": "(anonymous)",
"selfWeight": 0.001,
"totalWeight": 0.001,
},
Frame {
"col": 1,
"file": "file:///speedscope/sample/programs/javascript/simple.js",
"key": "(program):file:///speedscope/sample/programs/javascript/simple.js:1:1",
"line": 1,
"name": "(program)",
"selfWeight": 0,
"totalWeight": 0.03248933597933502,
},
Frame {
"col": 15,
"file": "file:///speedscope/sample/programs/javascript/simple.js",
"key": "alpha:file:///speedscope/sample/programs/javascript/simple.js:1:15",
"line": 1,
"name": "alpha",
"selfWeight": 0,
"totalWeight": 0.03248933597933502,
},
Frame {
"col": 15,
"file": "file:///speedscope/sample/programs/javascript/simple.js",
"key": "delta:file:///speedscope/sample/programs/javascript/simple.js:14:15",
"line": 14,
"name": "delta",
"selfWeight": 0.003094222474222382,
"totalWeight": 0.020112446082445484,
},
Frame {
"col": 15,
"file": "file:///speedscope/sample/programs/javascript/simple.js",
"key": "gamma:file:///speedscope/sample/programs/javascript/simple.js:20:15",
"line": 20,
"name": "gamma",
"selfWeight": 0.029395113505112636,
"totalWeight": 0.029395113505112636,
},
Frame {
"col": 14,
"file": "file:///speedscope/sample/programs/javascript/simple.js",
"key": "beta:file:///speedscope/sample/programs/javascript/simple.js:8:14",
"line": 8,
"name": "beta",
"selfWeight": 0,
"totalWeight": 0.012376889896889526,
},
Frame {
"col": 102,
"file": "",
"key": "firstOpenSearchURLString::4:102",
"line": 4,
"name": "firstOpenSearchURLString",
"selfWeight": 0.0005174240213818848,
"totalWeight": 0.0005174240213818848,
},
],
"name": "Grabación de Control temporal 1",
"stacks": Array [
"injectModule;(anonymous) 1.00ms",
" 39.93ms",
"(program);alpha;delta;gamma 10.83ms",
" 2.46ms",
"(program);alpha;delta 3.09ms",
"(program);alpha;beta;gamma 4.64ms",
"(program);alpha;delta;gamma 1.55ms",
"(program);alpha;beta;gamma 1.55ms",
"(program);alpha;delta;gamma 3.09ms",
"(program);alpha;beta;gamma 4.64ms",
"(program);alpha;delta;gamma 1.55ms",
"(program);alpha;beta;gamma 1.55ms",
" 253.50ms",
"firstOpenSearchURLString 517.42µs",
],
}
`;
exports[`importFromSafari: indexToView 1`] = `0`;
exports[`importFromSafari: profileGroup.name 1`] = `"Grabación de Control temporal 1"`;
@@ -1,5 +1,142 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importTraceEvents BEX interaction 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "A",
"line": undefined,
"name": "A",
"selfWeight": 0,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "B",
"line": undefined,
"name": "B",
"selfWeight": 2,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "D",
"line": undefined,
"name": "D",
"selfWeight": 0,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "C",
"line": undefined,
"name": "C",
"selfWeight": 2,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "F",
"line": undefined,
"name": "F",
"selfWeight": 1,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "E",
"line": undefined,
"name": "E",
"selfWeight": 1,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": undefined,
"key": "G",
"line": undefined,
"name": "G",
"selfWeight": 1,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "H",
"line": undefined,
"name": "H",
"selfWeight": 1,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": undefined,
"key": "J",
"line": undefined,
"name": "J",
"selfWeight": 1,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "I",
"line": undefined,
"name": "I",
"selfWeight": 1,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": undefined,
"key": "K",
"line": undefined,
"name": "K",
"selfWeight": 1,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "L",
"line": undefined,
"name": "L",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"A;B 2.00µs",
" 8.00µs",
"D;C 2.00µs",
" 8.00µs",
"F;E 1.00µs",
"F 1.00µs",
" 8.00µs",
"G;H 1.00µs",
"G 1.00µs",
" 8.00µs",
"J 1.00µs",
"J;I 1.00µs",
" 8.00µs",
"K 1.00µs",
"K;L 1.00µs",
],
}
`;
exports[`importTraceEvents BEX interaction: indexToView 1`] = `0`;
exports[`importTraceEvents BEX interaction: profileGroup.name 1`] = `"bex-interaction.json"`;
exports[`importTraceEvents bad E events 1`] = `
Object {
"frames": Array [
@@ -35,6 +172,74 @@ exports[`importTraceEvents bad E events: indexToView 1`] = `0`;
exports[`importTraceEvents bad E events: profileGroup.name 1`] = `"too-many-end-events.json"`;
exports[`importTraceEvents end event with empty stack 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents end event with empty stack: indexToView 1`] = `0`;
exports[`importTraceEvents end event with empty stack: profileGroup.name 1`] = `"end-event-with-empty-stack.json"`;
exports[`importTraceEvents end-non-top-of-stack 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "A {\\"x\\":1}",
"line": undefined,
"name": "A {\\"x\\":1}",
"selfWeight": 1,
"totalWeight": 11,
},
Frame {
"col": undefined,
"file": undefined,
"key": "B {\\"x\\":2}",
"line": undefined,
"name": "B {\\"x\\":2}",
"selfWeight": 9,
"totalWeight": 10,
},
Frame {
"col": undefined,
"file": undefined,
"key": "Z {\\"x\\":1}",
"line": undefined,
"name": "Z {\\"x\\":1}",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"A {\\"x\\":1} 1.00µs",
"A {\\"x\\":1};B {\\"x\\":2} 9.00µs",
"A {\\"x\\":1};B {\\"x\\":2};Z {\\"x\\":1} 1.00µs",
],
}
`;
exports[`importTraceEvents end-non-top-of-stack: indexToView 1`] = `0`;
exports[`importTraceEvents end-non-top-of-stack: profileGroup.name 1`] = `"end-non-top-of-stack.json"`;
exports[`importTraceEvents event re-ordering 1`] = `
Object {
"frames": Array [
@@ -93,6 +298,176 @@ exports[`importTraceEvents event re-ordering: indexToView 1`] = `0`;
exports[`importTraceEvents event re-ordering: profileGroup.name 1`] = `"must-retain-original-order.json"`;
exports[`importTraceEvents event reordering name match 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha {\\"x\\":0}",
"line": undefined,
"name": "alpha {\\"x\\":0}",
"selfWeight": 9,
"totalWeight": 10,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta {\\"x\\":0}",
"line": undefined,
"name": "beta {\\"x\\":0}",
"selfWeight": 0,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma {\\"x\\":0}",
"line": undefined,
"name": "gamma {\\"x\\":0}",
"selfWeight": 1,
"totalWeight": 1,
},
],
"name": "pid 0, tid 1",
"stacks": Array [
"alpha {\\"x\\":0} 1.00µs",
"alpha {\\"x\\":0};beta {\\"x\\":0};gamma {\\"x\\":0} 1.00µs",
"alpha {\\"x\\":0} 8.00µs",
],
}
`;
exports[`importTraceEvents event reordering name match: indexToView 1`] = `0`;
exports[`importTraceEvents event reordering name match: profileGroup.name 1`] = `"event-reordering-name-match.json"`;
exports[`importTraceEvents invalid x nesting 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 5,
"totalWeight": 15,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 10,
"totalWeight": 10,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 5.00µs",
"alpha;beta 10.00µs",
],
}
`;
exports[`importTraceEvents invalid x nesting: indexToView 1`] = `0`;
exports[`importTraceEvents invalid x nesting: profileGroup.name 1`] = `"invalid-x-nesting.json"`;
exports[`importTraceEvents matching x 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 8,
"totalWeight": 10,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 1,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma",
"line": undefined,
"name": "gamma",
"selfWeight": 1,
"totalWeight": 2,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta;gamma 1.00µs",
"alpha 3.00µs",
"alpha;gamma;beta 1.00µs",
"alpha 4.00µs",
],
}
`;
exports[`importTraceEvents matching x: indexToView 1`] = `0`;
exports[`importTraceEvents matching x: profileGroup.name 1`] = `"matching-x.json"`;
exports[`importTraceEvents mismatched args 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "A {\\"x\\":1}",
"line": undefined,
"name": "A {\\"x\\":1}",
"selfWeight": 10,
"totalWeight": 10,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"A {\\"x\\":1} 10.00µs",
],
}
`;
exports[`importTraceEvents mismatched args: indexToView 1`] = `0`;
exports[`importTraceEvents mismatched args: profileGroup.name 1`] = `"mismatched-args.json"`;
exports[`importTraceEvents mismatched name 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 0,
"totalWeight": 0,
},
],
"name": "pid 0, tid 0",
"stacks": Array [],
}
`;
exports[`importTraceEvents mismatched name: indexToView 1`] = `0`;
exports[`importTraceEvents mismatched name: profileGroup.name 1`] = `"mismatched-name.json"`;
exports[`importTraceEvents multiprocess 1`] = `
Object {
"frames": Array [
@@ -257,6 +632,164 @@ exports[`importTraceEvents multiprocess: indexToView 1`] = `0`;
exports[`importTraceEvents multiprocess: profileGroup.name 1`] = `"multiprocess.json"`;
exports[`importTraceEvents not enough end events 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "A {\\"x\\":1}",
"line": undefined,
"name": "A {\\"x\\":1}",
"selfWeight": 2,
"totalWeight": 11,
},
Frame {
"col": undefined,
"file": undefined,
"key": "A {\\"x\\":2}",
"line": undefined,
"name": "A {\\"x\\":2}",
"selfWeight": 9,
"totalWeight": 10,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"A {\\"x\\":1} 1.00µs",
"A {\\"x\\":1};A {\\"x\\":2} 9.00µs",
"A {\\"x\\":1};A {\\"x\\":2};A {\\"x\\":1} 1.00µs",
],
}
`;
exports[`importTraceEvents not enough end events: indexToView 1`] = `0`;
exports[`importTraceEvents not enough end events: profileGroup.name 1`] = `"not-enough-end-events.json"`;
exports[`importTraceEvents not out-of-order 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "A",
"line": undefined,
"name": "A",
"selfWeight": 2,
"totalWeight": 10,
},
Frame {
"col": undefined,
"file": undefined,
"key": "B",
"line": undefined,
"name": "B",
"selfWeight": 2,
"totalWeight": 8,
},
Frame {
"col": undefined,
"file": undefined,
"key": "C",
"line": undefined,
"name": "C",
"selfWeight": 6,
"totalWeight": 6,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"A 1.00µs",
"A;B 1.00µs",
"A;B;C 6.00µs",
"A;B 1.00µs",
"A 1.00µs",
],
}
`;
exports[`importTraceEvents not out-of-order unbalanced name 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 1,
"totalWeight": 10,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 9,
"totalWeight": 9,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 9.00µs",
],
}
`;
exports[`importTraceEvents not out-of-order unbalanced name: indexToView 1`] = `0`;
exports[`importTraceEvents not out-of-order unbalanced name: profileGroup.name 1`] = `"out-of-order-unbalanced-name.json"`;
exports[`importTraceEvents not out-of-order: indexToView 1`] = `0`;
exports[`importTraceEvents not out-of-order: profileGroup.name 1`] = `"out-of-order.json"`;
exports[`importTraceEvents only begin events 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "A {\\"x\\":1}",
"line": undefined,
"name": "A {\\"x\\":1}",
"selfWeight": 1,
"totalWeight": 2,
},
Frame {
"col": undefined,
"file": undefined,
"key": "B {\\"x\\":2}",
"line": undefined,
"name": "B {\\"x\\":2}",
"selfWeight": 1,
"totalWeight": 1,
},
Frame {
"col": undefined,
"file": undefined,
"key": "C {\\"x\\":2}",
"line": undefined,
"name": "C {\\"x\\":2}",
"selfWeight": 0,
"totalWeight": 0,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"A {\\"x\\":1} 1.00µs",
"A {\\"x\\":1};B {\\"x\\":2} 1.00µs",
],
}
`;
exports[`importTraceEvents only begin events: indexToView 1`] = `0`;
exports[`importTraceEvents only begin events: profileGroup.name 1`] = `"only-begin-events.json"`;
exports[`importTraceEvents partial json import 1`] = `
Object {
"frames": Array [
@@ -526,3 +1059,231 @@ exports[`importTraceEvents simple object: profileGroup.name 1`] = `"simple-objec
exports[`importTraceEvents simple: indexToView 1`] = `0`;
exports[`importTraceEvents simple: profileGroup.name 1`] = `"simple.json"`;
exports[`importTraceEvents unbalanced args 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "A {\\"x\\":1}",
"line": undefined,
"name": "A {\\"x\\":1}",
"selfWeight": 2,
"totalWeight": 10,
},
Frame {
"col": undefined,
"file": undefined,
"key": "A {\\"x\\":2}",
"line": undefined,
"name": "A {\\"x\\":2}",
"selfWeight": 8,
"totalWeight": 8,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"A {\\"x\\":1} 1.00µs",
"A {\\"x\\":1};A {\\"x\\":2} 8.00µs",
"A {\\"x\\":1} 1.00µs",
],
}
`;
exports[`importTraceEvents unbalanced args: indexToView 1`] = `0`;
exports[`importTraceEvents unbalanced args: profileGroup.name 1`] = `"unbalanced-args.json"`;
exports[`importTraceEvents unbalanced name 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 1,
"totalWeight": 10,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 9,
"totalWeight": 9,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 9.00µs",
],
}
`;
exports[`importTraceEvents unbalanced name: indexToView 1`] = `0`;
exports[`importTraceEvents unbalanced name: profileGroup.name 1`] = `"unbalanced-name.json"`;
exports[`importTraceEvents x events matching end 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 12,
"totalWeight": 20,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 4,
"totalWeight": 6,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma",
"line": undefined,
"name": "gamma",
"selfWeight": 4,
"totalWeight": 6,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma 1.00µs",
"alpha 2.00µs",
"alpha;gamma 1.00µs",
"alpha;gamma;beta 1.00µs",
"alpha 2.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma 1.00µs",
"alpha 3.00µs",
"alpha;gamma 1.00µs",
"alpha;gamma;beta 1.00µs",
"alpha 4.00µs",
],
}
`;
exports[`importTraceEvents x events matching end: indexToView 1`] = `0`;
exports[`importTraceEvents x events matching end: profileGroup.name 1`] = `"x-events-matching-end.json"`;
exports[`importTraceEvents x events matching start 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 12,
"totalWeight": 20,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 4,
"totalWeight": 6,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma",
"line": undefined,
"name": "gamma",
"selfWeight": 4,
"totalWeight": 6,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta;gamma 1.00µs",
"alpha;beta 1.00µs",
"alpha 2.00µs",
"alpha;gamma;beta 1.00µs",
"alpha;gamma 1.00µs",
"alpha 2.00µs",
"alpha;gamma;beta 1.00µs",
"alpha;gamma 1.00µs",
"alpha 2.00µs",
"alpha;beta;gamma 1.00µs",
"alpha;beta 1.00µs",
"alpha 5.00µs",
],
}
`;
exports[`importTraceEvents x events matching start: indexToView 1`] = `0`;
exports[`importTraceEvents x events matching start: profileGroup.name 1`] = `"x-events-matching-start.json"`;
exports[`importTraceEvents zero duration events 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "A",
"line": undefined,
"name": "A",
"selfWeight": 10,
"totalWeight": 10,
},
Frame {
"col": undefined,
"file": undefined,
"key": "B",
"line": undefined,
"name": "B",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": "C",
"line": undefined,
"name": "C",
"selfWeight": 0,
"totalWeight": 0,
},
Frame {
"col": undefined,
"file": undefined,
"key": "D",
"line": undefined,
"name": "D",
"selfWeight": 0,
"totalWeight": 0,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"A 10.00µs",
],
}
`;
exports[`importTraceEvents zero duration events: indexToView 1`] = `0`;
exports[`importTraceEvents zero duration events: profileGroup.name 1`] = `"zero-duration-events.json"`;
+8
View File
@@ -7,3 +7,11 @@ test('importFromBGFlameGraph', async () => {
test('importFromBGFlameGraph with CRLF', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-crlf.txt')
})
test('importFromBGFlameGraph with UTF-16, Little Endian', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-utf16-le.txt')
})
test('importFromBGFlameGraph with UTF-16, Big Endian', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-utf16-be.txt')
})
+28 -11
View File
@@ -170,8 +170,17 @@ function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
return getOrInsert(callFrameToFrameInfo, callFrame, callFrame => {
const name = callFrame.functionName || '(anonymous)'
const file = callFrame.url
const line = callFrame.lineNumber
const col = callFrame.columnNumber
// In Chrome profiles, line numbers & column numbers are both 0-indexed.
//
// We're going to normalize these to be 1-based to avoid needing to normalize
// these at the presentation layer.
let line = callFrame.lineNumber
if (line != null) line++
let col = callFrame.columnNumber
if (col != null) col++
return {
key: `${name}:${file}:${line}:${col}`,
name,
@@ -224,6 +233,10 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
// Ref: https://github.com/v8/v8/blob/44bd8fd7/src/inspector/js_protocol.json#L1485
let elapsed = chromeProfile.timeDeltas[0]
// Prevents negative time deltas from causing bad data. See
// https://github.com/jlfwong/speedscope/pull/305 for details.
let lastValidElapsed = elapsed
let lastNodeId = NaN
// The chrome CPU profile format doesn't collapse identical samples. We'll do that
@@ -232,22 +245,26 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
const nodeId = chromeProfile.samples[i]
if (nodeId != lastNodeId) {
samples.push(nodeId)
sampleTimes.push(elapsed)
if (elapsed < lastValidElapsed) {
sampleTimes.push(lastValidElapsed)
} else {
sampleTimes.push(elapsed)
lastValidElapsed = elapsed
}
}
if (i === chromeProfile.samples.length - 1) {
if (!isNaN(lastNodeId)) {
samples.push(lastNodeId)
sampleTimes.push(elapsed)
if (elapsed < lastValidElapsed) {
sampleTimes.push(lastValidElapsed)
} else {
sampleTimes.push(elapsed)
lastValidElapsed = elapsed
}
}
} else {
let timeDelta = chromeProfile.timeDeltas[i + 1]
if (timeDelta < 0) {
// This is super noisy, but can be helpful when debugging strange data
// console.warn('Substituting zero for unexpected time delta:', timeDelta, 'at index', i)
timeDelta = 0
}
const timeDelta = chromeProfile.timeDeltas[i + 1]
elapsed += timeDelta
lastNodeId = nodeId
}
+5 -1
View File
@@ -176,7 +176,7 @@ export function importFromFirefox(firefoxProfile: FirefoxProfile): Profile {
const frameData = thread.frameTable.data[f]
const location = thread.stringTable[frameData[0]]
const match = /(.*)\s+\((.*?):?(\d+)?\)$/.exec(location)
const match = /(.*)\s+\((.*?)(?::(\d+))?(?::(\d+))?\)$/.exec(location)
if (!match) return null
@@ -193,7 +193,11 @@ export function importFromFirefox(firefoxProfile: FirefoxProfile): Profile {
key: location,
name: match[1]!,
file: match[2]!,
// In Firefox profiles, line numbers are 1-based, but columns are
// 0-based. Let's normalize both to be 1-based.
line: match[3] ? parseInt(match[3]) : undefined,
col: match[4] ? parseInt(match[4]) + 1 : undefined,
}))
})
.filter(f => f != null) as FrameInfo[]
+7
View File
@@ -15,6 +15,7 @@ import {importSpeedscopeProfiles} from '../lib/file-format'
import {importFromV8ProfLog} from './v8proflog'
import {importFromLinuxPerf} from './linux-tools-perf'
import {importFromHaskell} from './haskell'
import {importFromSafari} from './safari'
import {ProfileDataSource, TextProfileDataSource, MaybeCompressedDataReader} from './utils'
import {importAsPprofProfile} from './pprof'
import {decodeBase64} from '../lib/utils'
@@ -131,6 +132,9 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
} else if (fileName.endsWith('.heapprofile')) {
console.log('Importing as Chrome Heap Profile')
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
} else if (fileName.endsWith('-recording.json')) {
console.log('Importing as Safari profile')
return toGroup(importFromSafari(JSON.parse(contents)))
}
// Second pass: Try to guess what file format it is based on structure
@@ -169,6 +173,9 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
} else if ('rts_arguments' in parsed && 'initial_capabilities' in parsed) {
console.log('Importing as Haskell GHC JSON Profile')
return importFromHaskell(parsed)
} else if ('recording' in parsed && 'sampleStackTraces' in parsed.recording) {
console.log('Importing as Safari profile')
return toGroup(importFromSafari(JSON.parse(contents)))
}
} else {
// Format is not JSON
+5
View File
@@ -0,0 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importFromSafari', async () => {
await checkProfileSnapshot('./sample/profiles/Safari/13.1/simple.html-recording.json')
})
+120
View File
@@ -0,0 +1,120 @@
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
import {TimeFormatter} from '../lib/value-formatters'
interface Record {
type: string
eventType?: string
startTime?: number
endTime?: number
// timeline-record-type-cpu
timestamp?: number
usage?: number
threads?: any[]
// timeline-record-type-script
details?: number | string | any
extraDetails?: null | any
// timeline-record-type-network
archiveStartTime?: number
entry?: any
// timeline-record-type-layout
quad?: number[]
}
interface ExprLocation {
line: number
column: number
}
interface StackFrame {
sourceID: string
name: string
line: number
column: number
url: string
expressionLocation?: ExprLocation
}
interface Sample {
timestamp: number
stackFrames: StackFrame[]
}
interface Recording {
displayName: string
startTime: number
endTime: number
discontinuities: any[]
instrumentTypes: string[]
records: Record[]
markers: any[]
memoryPressureEvents: any[]
sampleStackTraces: Sample[]
sampleDurations: number[]
}
interface Overview {
secondsPerPixel: number
scrollStartTime: number
selectionStartTime: number
selectionDuration: number
}
interface SafariProfile {
version: number
recording: Recording
overview: Overview
}
function makeStack(frames: StackFrame[]): FrameInfo[] {
return frames
.map(({name, url, line, column}) => ({
key: `${name}:${url}:${line}:${column}`,
file: url,
line,
col: column,
name: name || '(anonymous)',
}))
.reverse()
}
export function importFromSafari(contents: SafariProfile): Profile | null {
if (contents.version !== 1) {
console.warn(`Unknown Safari profile version ${contents.version}... Might be incompatible.`)
}
const {recording} = contents
const {sampleStackTraces, sampleDurations} = recording
const count = sampleStackTraces.length
if (count < 1) {
console.warn('Empty profile')
return null
}
const profileDuration =
sampleStackTraces[count - 1].timestamp - sampleStackTraces[0].timestamp + sampleDurations[0]
const profile = new StackListProfileBuilder(profileDuration)
let previousEndTime = Number.MAX_VALUE
sampleStackTraces.forEach((sample, i) => {
const endTime = sample.timestamp
const duration = sampleDurations[i]
const startTime = endTime - duration
const idleDurationBefore = startTime - previousEndTime
// FIXME: 2ms is a lot, but Safari's timestamps and durations don't line up very well and will create
// phantom idle time
if (idleDurationBefore > 0.002) {
profile.appendSampleWithWeight([], idleDurationBefore)
}
profile.appendSampleWithWeight(makeStack(sample.stackFrames), duration)
previousEndTime = endTime
})
profile.setValueFormatter(new TimeFormatter('seconds'))
profile.setName(recording.displayName)
return profile.build()
}
+68
View File
@@ -31,3 +31,71 @@ test('importTraceEvents bad E events', async () => {
test('importTraceEvents event re-ordering', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/must-retain-original-order.json')
})
test('importTraceEvents end-non-top-of-stack', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/end-non-top-of-stack.json')
})
test('importTraceEvents mismatched args', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/mismatched-args.json')
})
test('importTraceEvents mismatched name', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/mismatched-name.json')
})
test('importTraceEvents not enough end events', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/not-enough-end-events.json')
})
test('importTraceEvents not out-of-order unbalanced name', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/out-of-order-unbalanced-name.json')
})
test('importTraceEvents not out-of-order', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/out-of-order.json')
})
test('importTraceEvents unbalanced name', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/unbalanced-name.json')
})
test('importTraceEvents unbalanced args', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/unbalanced-args.json')
})
test('importTraceEvents end event with empty stack', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/end-event-with-empty-stack.json')
})
test('importTraceEvents only begin events', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/only-begin-events.json')
})
test('importTraceEvents zero duration events', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/zero-duration-events.json')
})
test('importTraceEvents matching x', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/matching-x.json')
})
test('importTraceEvents x events matching start', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/x-events-matching-start.json')
})
test('importTraceEvents x events matching end', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/x-events-matching-end.json')
})
test('importTraceEvents BEX interaction', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/bex-interaction.json')
})
test('importTraceEvents invalid x nesting', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/invalid-x-nesting.json')
})
test('importTraceEvents event reordering name match', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/event-reordering-name-match.json')
})
+281 -118
View File
@@ -1,5 +1,5 @@
import {sortBy, zeroPad, lastOf} from '../lib/utils'
import {ProfileGroup, CallTreeProfileBuilder, FrameInfo} from '../lib/profile'
import {sortBy, zeroPad, getOrInsert, lastOf} from '../lib/utils'
import {ProfileGroup, CallTreeProfileBuilder, FrameInfo, Profile} from '../lib/profile'
import {TimeFormatter} from '../lib/value-formatters'
// This file concerns import from the "Trace Event Format", authored by Google
@@ -64,7 +64,135 @@ interface XTraceEvent extends TraceEvent {
// The trace format supports a number of event types that we ignore.
type ImportableTraceEvent = BTraceEvent | ETraceEvent | XTraceEvent
type DurationEvent = BTraceEvent | ETraceEvent
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[]>()
for (let ev of events) {
const list = getOrInsert(map, pidTidKey(ev.pid, ev.tid), () => [])
list.push(ev)
}
return map
}
function selectQueueToTakeFromNext(
bEventQueue: BTraceEvent[],
eEventQueue: ETraceEvent[],
): 'B' | 'E' {
if (bEventQueue.length === 0 && eEventQueue.length === 0) {
throw new Error('This method should not be given both queues empty')
}
if (eEventQueue.length === 0) return 'B'
if (bEventQueue.length === 0) return 'E'
const bFront = bEventQueue[0]
const eFront = eEventQueue[0]
const bts = bFront.ts
const ets = eFront.ts
if (bts < ets) return 'B'
if (ets < bts) return 'E'
// 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,
// 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'
}
function convertToEventQueues(events: ImportableTraceEvent[]): [BTraceEvent[], ETraceEvent[]] {
const beginEvents: BTraceEvent[] = []
const endEvents: ETraceEvent[] = []
// Rebase all of the timestamps on the lowest timestamp
if (events.length > 0) {
let firstTs = Number.MAX_SAFE_INTEGER
for (let ev of events) {
firstTs = Math.min(firstTs, ev.ts)
}
for (let ev of events) {
ev.ts -= firstTs
}
}
// Next, combine B, E, and X events into two timestamp ordered queues.
const xEvents: XTraceEvent[] = []
for (let ev of events) {
switch (ev.ph) {
case 'B': {
beginEvents.push(ev)
break
}
case 'E': {
endEvents.push(ev)
break
}
case 'X': {
xEvents.push(ev)
break
}
default: {
const _exhaustiveCheck: never = ev
return _exhaustiveCheck
}
}
}
function dur(x: XTraceEvent): number {
return x.dur ?? x.tdur ?? 0
}
xEvents.sort((a, b) => {
if (a.ts < b.ts) return -1
if (a.ts > b.ts) return 1
// Super weird special case: if we have two 'X' events with the same 'ts'
// but different 'dur' the only valid interpretation is to put the one with
// the longer 'dur' first, because you can't nest longer things in shorter
// things.
const aDur = dur(a)
const bDur = dur(b)
if (aDur > bDur) return -1
if (aDur < bDur) return 1
// Otherwise, retain the original order by relying upon a stable sort here.
return 0
})
for (let x of xEvents) {
const xDur = dur(x)
beginEvents.push({...x, ph: 'B'} as BTraceEvent)
endEvents.push({...x, ph: 'E', ts: x.ts + xDur} as ETraceEvent)
}
function compareTimestamps(a: TraceEvent, b: TraceEvent) {
if (a.ts < b.ts) return -1
if (a.ts > b.ts) return 1
// Important: if the timestamps are the same, return zero. We're going to
// rely upon a stable sort here.
return 0
}
beginEvents.sort(compareTimestamps)
endEvents.sort(compareTimestamps)
return [beginEvents, endEvents]
}
function filterIgnoredEventTypes(events: TraceEvent[]): ImportableTraceEvent[] {
const ret: ImportableTraceEvent[] = []
@@ -79,40 +207,6 @@ function filterIgnoredEventTypes(events: TraceEvent[]): ImportableTraceEvent[] {
return ret
}
function convertToDurationEvents(events: ImportableTraceEvent[]): DurationEvent[] {
const ret: DurationEvent[] = []
for (let ev of events) {
switch (ev.ph) {
case 'B':
ret.push(ev)
break
case 'E':
ret.push(ev)
break
case 'X':
let dur: number | null = null
if (ev.dur != null) dur = ev.dur
else if (ev.tdur != null) dur = ev.tdur
if (dur == null) {
console.warn('Found a complete event (X) with no duration. Skipping: ', ev)
continue
}
ret.push({...ev, ph: 'B'} as BTraceEvent)
ret.push({...ev, ph: 'E', ts: ev.ts + dur} as ETraceEvent)
break
default:
const _exhaustiveCheck: never = ev
return _exhaustiveCheck
}
}
return ret
}
function getProcessNamesByPid(events: TraceEvent[]): Map<number, string> {
const processNamesByPid = new Map<number, string>()
for (let ev of events) {
@@ -128,8 +222,7 @@ function getThreadNamesByPidTid(events: TraceEvent[]): Map<string, string> {
for (let ev of events) {
if (ev.ph === 'M' && ev.name === 'thread_name' && ev.args && ev.args.name) {
const key = `${ev.pid}:${ev.tid}`
threadNameByPidTid.set(key, ev.args.name)
threadNameByPidTid.set(pidTidKey(ev.pid, ev.tid), ev.args.name)
}
}
return threadNameByPidTid
@@ -143,66 +236,33 @@ function keyForEvent(event: TraceEvent): string {
return name
}
type TraceEventProfileState = {profile: CallTreeProfileBuilder; eventStack: BTraceEvent[]}
function frameInfoForEvent(event: TraceEvent): FrameInfo {
const key = keyForEvent(event)
return {
name: key,
key: key,
}
}
function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
const stateByPidTid = new Map<string, TraceEventProfileState>()
const importableEvents = filterIgnoredEventTypes(events)
const durationEvents = convertToDurationEvents(importableEvents)
const partitioned = partitionByPidTid(importableEvents)
const processNamesByPid = getProcessNamesByPid(events)
const threadNamesByPidTid = getThreadNamesByPidTid(events)
durationEvents.sort((a, b) => {
if (a.ts < b.ts) return -1
if (a.ts > b.ts) return 1
if (a.pid < b.pid) return -1
if (a.pid > b.pid) return 1
if (a.tid < b.tid) return -1
if (a.tid > b.tid) return 1
const profilePairs: [string, Profile][] = []
// We have to be careful with events that have the same timestamp
// and the same pid/tid
const aKey = keyForEvent(a)
const bKey = keyForEvent(b)
if (aKey === bKey) {
// If the two elements have the same key, we need to process the begin
// event before the end event. This will be a zero-duration event.
if (a.ph === 'B' && b.ph === 'E') return -1
if (a.ph === 'E' && b.ph === 'B') return 1
} else {
// If the two elements have *different* keys, we want to process
// the end of an event before the beginning of the event to prevent
// out-of-order push/pops from the call-stack.
if (a.ph === 'B' && b.ph === 'E') return 1
if (a.ph === 'E' && b.ph === 'B') return -1
}
partitioned.forEach(eventsForThread => {
if (eventsForThread.length === 0) return
// In all other cases, retain the original sort order.
return 0
})
const {pid, tid} = eventsForThread[0]
if (durationEvents.length > 0) {
const firstTs = durationEvents[0].ts
for (let ev of durationEvents) {
ev.ts -= firstTs
}
}
function getOrCreateProfileState(pid: number, tid: number): TraceEventProfileState {
// We zero-pad the PID and TID to make sorting them by pid/tid pair later easier.
const pidTid = `${zeroPad('' + pid, 10)}:${zeroPad('' + tid, 10)}`
let state = stateByPidTid.get(pidTid)
if (state != null) return state
let profile = new CallTreeProfileBuilder()
state = {profile, eventStack: []}
const profile = new CallTreeProfileBuilder()
profile.setValueFormatter(new TimeFormatter('microseconds'))
stateByPidTid.set(pidTid, state)
const processName = processNamesByPid.get(pid)
const threadName = threadNamesByPidTid.get(`${pid}:${tid}`)
const threadName = threadNamesByPidTid.get(pidTidKey(pid, tid))
if (processName != null && threadName != null) {
profile.setName(`${processName} (pid ${pid}), ${threadName} (tid ${tid})`)
@@ -214,51 +274,154 @@ function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
profile.setName(`pid ${pid}, tid ${tid}`)
}
return state
}
// 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)
for (let ev of durationEvents) {
const {profile, eventStack} = getOrCreateProfileState(ev.pid, ev.tid)
const key = keyForEvent(ev)
const frameInfo: FrameInfo = {
key: key,
name: key,
const frameStack: BTraceEvent[] = []
const enterFrame = (b: BTraceEvent) => {
frameStack.push(b)
profile.enterFrame(frameInfoForEvent(b), b.ts)
}
switch (ev.ph) {
case 'B':
eventStack.push(ev)
profile.enterFrame(frameInfo, ev.ts)
break
case 'E':
const lastEvent = lastOf(eventStack)
if (lastEvent != null && lastEvent.name === ev.name) {
profile.leaveFrame(frameInfo, ev.ts)
eventStack.pop()
} else {
console.warn(
'Event discarded because it did not match top-of-stack. Discarded event:',
ev,
'Top of stack:',
lastEvent,
)
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)
}
while (bEventQueue.length > 0 || eEventQueue.length > 0) {
const queueName = selectQueueToTakeFromNext(bEventQueue, eEventQueue)
switch (queueName) {
case 'B': {
enterFrame(bEventQueue.shift()!)
break
}
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)
default:
const _exhaustiveCheck: never = ev
return _exhaustiveCheck
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) {
// 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
}
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.`)
profile.leaveFrame(frame, profile.getTotalWeight())
}
profilePairs.push([pidTidKey(pid, tid), profile.build()])
})
// For now, we just sort processes by pid & tid.
// TODO: The standard specifies that metadata events with the name
// "process_sort_index" and "thread_sort_index" can be used to influence the
// order, but for simplicity we'll ignore that until someone complains :)
const profilePairs = Array.from(stateByPidTid.entries())
sortBy(profilePairs, p => p[0])
return {name: '', indexToView: 0, profiles: profilePairs.map(p => p[1].profile)}
return {
name: '',
indexToView: 0,
profiles: profilePairs.map(p => p[1]),
}
}
function isTraceEventList(maybeEventList: any): maybeEventList is TraceEvent[] {
+20 -5
View File
@@ -51,15 +51,30 @@ export class MaybeCompressedDataReader implements ProfileDataSource {
async readAsText(): Promise<string> {
const buffer = await this.readAsArrayBuffer()
let ret: string = ''
// 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()
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 asusme is UTF-8 encoded.
const array = new Uint8Array(buffer)
// 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])
}
+8 -2
View File
@@ -16,7 +16,7 @@
// Because we're not going to use this in our actual build, it's okay for this
// to be inefficient.
(function () {
;(function () {
const nodeVersion = process.versions.node
const versionParts = nodeVersion.split('.')
const majorVersion = parseInt(versionParts[0], 10)
@@ -48,4 +48,10 @@
this.splice(0, this.length, ...arrayWithIndices.map(x => x[0]))
return this
}
})()
})()
;(function () {
// TextDecoder is a global API in browsers, but an imported API in node.
//
// Let's emulate it being a global API during tests.
global.TextDecoder = require('util').TextDecoder
})()
@@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
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)",
"(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)",
"(m @ delta.ts:3:22) <- (m @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:119)",
"(r @ alpha.ts:4:8) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:158)",
"(gamma @ gamma.ts:1:14) <- (t @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:15)",
"(zap @ kludge.ts:9:3) <- (zap @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:260)",
]
`;
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)",
"(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)",
"(o constructor @ ../kludge.ts:4:3) <- (o @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:10:139)",
"(parcelRequire.CIJJ../alpha @ ../alpha.ts:5:5) <- (parcelRequire.CIJJ../alpha @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:9:55)",
"(parcelRequire.EJAe../gamma @ ../gamma.ts:1:8) <- (parcelRequire.EJAe../gamma @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:3:23)",
"(parcelRequire.NDR3../beta @ ../delta.ts:3:8) <- (parcelRequire.NDR3../beta @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:7:39)",
"(parcelRequire.xEzo../kludge @ ../kludge.ts:3:1) <- (parcelRequire.xEzo../kludge @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:11:39)",
"(beta @ ../beta.ts:3:17) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:4:119)",
"(r @ ../delta.ts:3:22) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:6:125)",
"(alpha @ ../alpha.ts:4:17) <- (t @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:8:140)",
]
`;
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)",
"(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)",
"(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)",
"(beta @ webpack://speedscope-sourcemap-test-project/beta.ts:3:17) <- (o @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:88)",
"(r @ webpack://speedscope-sourcemap-test-project/delta.ts:3:22) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:130)",
"(alpha @ webpack://speedscope-sourcemap-test-project/alpha.ts:4:17) <- (t @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:169)",
]
`;
exports[`source-map remapping of firefox-79-webpack 1`] = `
Array [
"(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)",
"(n.prototype.zap @ webpack://speedscope-sourcemap-test-project/kludge.ts:9:3) <- (n.prototype.zap @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:305)",
"(beta @ webpack://speedscope-sourcemap-test-project/beta.ts:3:17) <- (o @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:88)",
"(r @ webpack://speedscope-sourcemap-test-project/delta.ts:3:22) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:130)",
"(alpha @ webpack://speedscope-sourcemap-test-project/alpha.ts:4:17) <- (t @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:169)",
"(t/< @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- (t/< @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
]
`;
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)",
"(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)",
"(beta @ webpack://speedscope-sourcemap-test-project/beta.ts:3:17) <- (o @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:88)",
"(r @ webpack://speedscope-sourcemap-test-project/delta.ts:3:22) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:130)",
"(alpha @ webpack://speedscope-sourcemap-test-project/alpha.ts:4:17) <- (t @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:169)",
]
`;
+69
View File
@@ -0,0 +1,69 @@
// This file contains a collection of classes which make it easier to perform
// batch rendering of Canvas2D primitives. The advantage of this over just doing
// ctx.beginPath() ... ctx.rect(...) ... ctx.endPath() is that you can construct
// several different batch renderers are the same time, then decide on their
// paint order at the end.
//
// See FlamechartPanZoomView.renderOverlays for an example of how this is used.
export interface TextArgs {
text: string
x: number
y: number
}
export class BatchCanvasTextRenderer {
private argsBatch: TextArgs[] = []
text(args: TextArgs) {
this.argsBatch.push(args)
}
fill(ctx: CanvasRenderingContext2D, color: string) {
if (this.argsBatch.length === 0) return
ctx.fillStyle = color
for (let args of this.argsBatch) {
ctx.fillText(args.text, args.x, args.y)
}
this.argsBatch = []
}
}
export interface RectArgs {
x: number
y: number
w: number
h: number
}
export class BatchCanvasRectRenderer {
private argsBatch: RectArgs[] = []
rect(args: RectArgs) {
this.argsBatch.push(args)
}
private drawPath(ctx: CanvasRenderingContext2D) {
ctx.beginPath()
for (let args of this.argsBatch) {
ctx.rect(args.x, args.y, args.w, args.h)
}
ctx.closePath()
this.argsBatch = []
}
fill(ctx: CanvasRenderingContext2D, color: string) {
if (this.argsBatch.length === 0) return
ctx.fillStyle = color
this.drawPath(ctx)
ctx.fill()
}
stroke(ctx: CanvasRenderingContext2D, color: string, lineWidth: number) {
if (this.argsBatch.length === 0) return
ctx.strokeStyle = color
ctx.lineWidth = lineWidth
this.drawPath(ctx)
ctx.stroke()
}
}
+17
View File
@@ -34,6 +34,23 @@ export class Color {
return new Color(clamp(R1 + m, 0, 1), clamp(G1 + m, 0, 1), clamp(B1 + m, 0, 1), 1.0)
}
static fromCSSHex(hex: string) {
if (hex.length !== 7 || hex[0] !== '#') {
throw new Error(`Invalid color input ${hex}`)
}
const r = parseInt(hex.substr(1, 2), 16) / 255
const g = parseInt(hex.substr(3, 2), 16) / 255
const b = parseInt(hex.substr(5, 2), 16) / 255
if (r < 0 || r > 1 || g < 0 || g > 1 || b < 0 || b > 1) {
throw new Error(`Invalid color input ${hex}`)
}
return new Color(r, g, b)
}
withAlpha(a: number): Color {
return new Color(this.r, this.g, this.b, a)
}
toCSS(): string {
return `rgba(${(255 * this.r).toFixed()}, ${(255 * this.g).toFixed()}, ${(
255 * this.b
+51 -46
View File
@@ -1,67 +1,72 @@
import {importEmscriptenSymbolMap} from './emscripten'
import {Frame} from './profile'
import {KeyedSet} from './utils'
test('importEmscriptenSymbolMap', () => {
function checkMap(input: string, expectedMapping: [string, string][]) {
const mapping = importEmscriptenSymbolMap(input)
if (mapping == null) {
fail('Mapping failed to parse')
return
}
const frames = new KeyedSet<Frame>()
for (let [key, value] of expectedMapping) {
const frame = Frame.getOrInsert(frames, {key, name: key})
expect(mapping(frame)?.name).toBe(value)
}
}
// Valid symbol map
expect(
importEmscriptenSymbolMap(
[
/* prettier: ignore */
'a:A',
'b:B',
'c:C',
].join('\n'),
),
).toEqual(
new Map([
checkMap(
[
/* prettier: ignore */
'a:A',
'b:B',
'c:C',
].join('\n'),
[
['a', 'A'],
['b', 'B'],
['c', 'C'],
]),
],
)
// Valid symbol map with trailing newline
expect(
importEmscriptenSymbolMap(
[
/* prettier: ignore */
'a:A',
'b:B',
'c:C',
'd:D-D',
'',
].join('\n'),
),
).toEqual(
new Map([
checkMap(
[
/* prettier: ignore */
'a:A',
'b:B',
'c:C',
'd:D-D',
'',
].join('\n'),
[
['a', 'A'],
['b', 'B'],
['c', 'C'],
['d', 'D-D'],
]),
],
)
// Valid symbol map with non-alpha characters
expect(importEmscriptenSymbolMap('u6:__ZN8tinyxml210XMLCommentD0Ev\n')).toEqual(
new Map([['u6', '__ZN8tinyxml210XMLCommentD0Ev']]),
)
checkMap('u6:__ZN8tinyxml210XMLCommentD0Ev\n', [['u6', '__ZN8tinyxml210XMLCommentD0Ev']])
// WebAssembly symbol map
expect(
importEmscriptenSymbolMap(
[
/* prettier: ignore */
'0:A',
'1:B',
'2:C',
'3:D-D',
'4:a\\20b',
'5:a\\2',
'6:a\\3z',
'7:a\\20b\\20c',
].join('\n'),
),
).toEqual(
new Map([
checkMap(
[
/* prettier: ignore */
'0:A',
'1:B',
'2:C',
'3:D-D',
'4:a\\20b',
'5:a\\2',
'6:a\\3z',
'7:a\\20b\\20c',
].join('\n'),
[
['wasm-function[0]', 'A'],
['wasm-function[1]', 'B'],
['wasm-function[2]', 'C'],
@@ -70,7 +75,7 @@ test('importEmscriptenSymbolMap', () => {
['wasm-function[5]', 'a\\2'],
['wasm-function[6]', 'a\\3z'],
['wasm-function[7]', 'a b c'],
]),
],
)
// Invalid symbol map
+12 -5
View File
@@ -1,7 +1,8 @@
type EmscriptenSymbolMap = Map<string, string>
// Returns `input` with hex escapes expanded (e.g. `\20` becomes ` `.)
//
import {Frame, SymbolRemapper} from './profile'
// NOTE: This will fail to ignore escaped backslahes (e.g. `\\20`).
function unescapeHex(input: string): string {
return input.replace(/\\([a-fA-F0-9]{2})/g, (_match, group) => {
@@ -15,7 +16,7 @@ function unescapeHex(input: string): string {
// have the associated symbol map. To do this, first drop the profile into speedscope
// and then drop the symbol map. After the second drop, the symbols will be remapped to
// their original names.
export function importEmscriptenSymbolMap(contents: string): EmscriptenSymbolMap | null {
export function importEmscriptenSymbolMap(contents: string): SymbolRemapper | null {
const lines = contents.split('\n')
if (!lines.length) return null
@@ -23,7 +24,7 @@ export function importEmscriptenSymbolMap(contents: string): EmscriptenSymbolMap
if (lines[lines.length - 1] === '') lines.pop()
if (!lines.length) return null
const map: EmscriptenSymbolMap = new Map()
const map = new Map<string, string>()
const intRegex = /^(\d+):(.+)$/
const idRegex = /^([\$\w]+):([\$\w-]+)$/
@@ -45,5 +46,11 @@ export function importEmscriptenSymbolMap(contents: string): EmscriptenSymbolMap
return null
}
return map
return (frame: Frame) => {
if (!map.has(frame.name)) {
return null
}
return {name: map.get(frame.name)}
}
}
+22 -1
View File
@@ -1,7 +1,7 @@
import {Frame, CallTreeNode} from './profile'
import {lastOf} from './utils'
import {clamp} from './math'
import {clamp, Rect, Vec2} from './math'
export interface FlamechartFrame {
node: CallTreeNode
@@ -90,6 +90,27 @@ export class Flamechart {
return clamp(viewportWidth, minWidth, maxWidth)
}
// Given a desired config-space viewport rectangle, clamp the rectangle so
// that it fits within the given flamechart. This prevents the viewport from
// extending past the bounds of the flamechart or zooming in too far.
getClampedConfigSpaceViewportRect({
configSpaceViewportRect,
renderInverted,
}: {
configSpaceViewportRect: Rect
renderInverted?: boolean
}) {
const configSpaceSize = new Vec2(this.getTotalWeight(), this.getLayers().length)
const width = this.getClampedViewportWidth(configSpaceViewportRect.size.x)
const size = configSpaceViewportRect.size.withX(width)
const origin = Vec2.clamp(
configSpaceViewportRect.origin,
new Vec2(0, renderInverted ? 0 : -1),
Vec2.max(Vec2.zero, configSpaceSize.minus(size).plus(new Vec2(0, 1))),
)
return new Rect(origin, configSpaceViewportRect.size.withX(width))
}
constructor(private source: FlamechartDataSource) {
const stack: FlamechartFrame[] = []
const openFrame = (node: CallTreeNode, value: number) => {
+86
View File
@@ -0,0 +1,86 @@
import * as fs from 'fs'
import * as path from 'path'
import {importProfilesFromArrayBuffer} from '../import'
import {importJavaScriptSourceMapSymbolRemapper} from './js-source-map'
import {Frame} from './profile'
async function checkSourceMapApplication(pathToProfile: string, pathToSourceMap: string) {
const buffer = fs.readFileSync(pathToProfile)
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
const profileGroup = await importProfilesFromArrayBuffer(
path.basename(pathToProfile),
arrayBuffer,
)
if (!profileGroup) {
fail('Failed to extract profile')
return
}
const sourceMapFileName = path.basename(pathToSourceMap)
const remapper = await importJavaScriptSourceMapSymbolRemapper(
fs.readFileSync(pathToSourceMap, 'utf-8'),
sourceMapFileName,
)
if (!remapper) {
fail('Failed to extract sourcemap')
return
}
const key: (f: {name?: string; file?: string; line?: number; col?: number}) => string = f => {
return `${f.name} @ ${f.file}:${f.line}:${f.col}`
}
const frames: Frame[] = []
profileGroup.profiles[profileGroup.indexToView].forEachFrame(f => {
frames.push(f)
})
frames.sort((a, b) => (a.key < b.key ? -1 : 1))
const remappedFrames: string[] = []
frames.forEach(f => {
const remapped = remapper(f)
if (!remapped) return
remappedFrames.push(`(${key({...f, ...remapped})}) <- (${key(f)})`)
})
expect(remappedFrames).toMatchSnapshot()
}
test('source-map remapping of chrome-85-webpack', async () => {
await checkSourceMapApplication(
'./sample/profiles/source-maps/chrome-85-webpack.json',
'./sample/profiles/source-maps/webpack/typescript-source-map-test.js.map',
)
})
test('source-map remapping of firefox-79-webpack', async () => {
await checkSourceMapApplication(
'./sample/profiles/source-maps/firefox-79-webpack.json',
'./sample/profiles/source-maps/webpack/typescript-source-map-test.js.map',
)
})
test('source-map remapping of safari-13-webpack', async () => {
await checkSourceMapApplication(
'./sample/profiles/source-maps/safari-13-webpack.json',
'./sample/profiles/source-maps/webpack/typescript-source-map-test.js.map',
)
})
test('source-map remapping of chrome-85-esbuild', async () => {
await checkSourceMapApplication(
'./sample/profiles/source-maps/chrome-85-esbuild.json',
'./sample/profiles/source-maps/esbuild/typescript-source-map-test.js.map',
)
})
test('source-map remapping of chrome-85-parcel', async () => {
await checkSourceMapApplication(
'./sample/profiles/source-maps/chrome-85-parcel.json',
'./sample/profiles/source-maps/parcel/typescript-source-map-test.js.map',
)
})
+227
View File
@@ -0,0 +1,227 @@
// This file contains code to allow profiles to be remapped by JavaScript source maps.
//
// As of writing, this is using an out-of-date version of source-map, because the
// source-map library migrated to using web-assembly. This requires loading the
// web-assembly ball. The easiest way to do this is to load it from a third-party
// URL, but I want speedscope to work standalone offline. This means that the remaining
// options require some way of having a local URL that corresponds the .wasm file.
//
// Also as of writing, speedscope is bundled with Parcel v1. Trying to import
// a .wasm file in Parcel v1 tries to load the wasm module itself, which is not
// what I'm trying to do -- I want SourceMapConsumer.initialize to be the thing
// booting the WebAssembly, not Parcel itself.
//
// One way of getting around this problem is to modify the build system to
// copy the .wasm file from node_modules/source-map/lib/mappings.wasm. I could do
// this, but it's a bit of a pain.
//
// Another would be to use something like
// import("url:../node_modules/source-map/lib/mappings.wasm"), and then pass the
// resulting URL to SourceMapConsumer.initialize. This is also kind of a pain,
// because I can only do that if I upgrade to Parcel v2. Ultimately, I'd like to
// use esbuild rather than parcel at all, so for now I'm just punting on this by
// using an old-version of source-map which doesn't depend on wasm.
// This is rarely used, so let's load it async to avoid bloating the initial
// bundle.
import type {MappingItem, RawSourceMap, SourceMapConsumer} from 'source-map'
const sourceMapModule = import('source-map')
import {Frame, SymbolRemapper} from './profile'
import {findIndexBisect} from './utils'
const DEBUG = false
export async function importJavaScriptSourceMapSymbolRemapper(
contentsString: string,
sourceMapFileName: string,
): Promise<SymbolRemapper | null> {
const sourceMap = await sourceMapModule
let consumer: SourceMapConsumer | null = null
let contents: RawSourceMap | null = null
try {
contents = JSON.parse(contentsString)
consumer = new sourceMap.SourceMapConsumer(contents!)
} catch (e) {
return null
}
const mappingItems: MappingItem[] = []
consumer.eachMapping(
function (m: MappingItem) {
// The sourcemap library uses 1-based line numbers, and 0-based column
// numbers. speedscope uses 1-based line-numbers, and 1-based column
// numbers for its in-memory representation, so we'll normalize that
// here too.
mappingItems.push({
...m,
generatedColumn: m.generatedColumn + 1,
originalColumn: m.originalColumn + 1,
})
},
{},
// We're going to binary search through these later, so make sure they're
// sorted by their order in the generated file.
sourceMap.SourceMapConsumer.GENERATED_ORDER,
)
const sourceMapFileNameWithoutExt = sourceMapFileName.replace(/\.[^/]*$/, '')
return (frame: Frame) => {
let fileMatches = false
if (contents?.file && contents?.file === frame.file) {
fileMatches = true
} else if (
('/' + frame.file?.replace(/\.[^/]*$/, '')).endsWith('/' + sourceMapFileNameWithoutExt)
) {
fileMatches = true
}
if (!fileMatches) {
// The source-map doesn't apply to the file this frame is defined in.
return null
}
if (frame.line == null || frame.col == null) {
// If we don't have a line & column number for the frame, we can't
// remap it.
return null
}
// If we got here, then we hopefully have an remapping.
//
// Ideally, we'd look up a symbol whose generatedLine & generatedColumn
// match what we have in our profile, but unfortunately browsers don't do
// this.
//
// Browsers set the column number for a function to the index of the
// opening paren for the argument list, rather than the beginning of the
// index of the name.
//
// function alpha() { ... }
// ^
//
// const beta = function() { ... }
// ^
//
// const gamma = () => { ... }
// ^
//
// Since we don't have the source code being profiled, we unfortunately
// can't normalize this to set the column to the first character of the
// actual name.
//
// To work around this limitation, we'll search backwards from the first
// mapping whose generatedLine & generatedColumn are beyond the location
// in the profile.
let mappingIndex = findIndexBisect(mappingItems, m => {
if (m.generatedLine > frame.line!) return true
if (m.generatedLine < frame.line!) return false
if (m.generatedColumn >= frame.col!) return true
return false
})
if (mappingIndex === -1) {
// There are no symbols following the given profile frame symbol, so try
// to apply the very last mapping.
mappingIndex = mappingItems.length - 1
} else if (mappingIndex === 0) {
// If the very first index in mappingItems is beyond the location in the
// profile, it means the name we're looking for doesn't have a
// corresponding entry in the source-map (this can happen if the
// source-map isn't the right source-map)
return null
} else {
mappingIndex--
}
const sourceMapItem = mappingItems[mappingIndex]
const remappedFrameInfo: {name?: string; file?: string; line?: number; col?: number} = {}
if (sourceMapItem.name != null) {
remappedFrameInfo.name = sourceMapItem.name
} else if (sourceMapItem.source != null) {
// HACK: If the item name isn't specified, but the source is present, then
// we're going to try to guess what the name is by using the originalLine
// and originalColumn.
// The second argument here is "returnNullOnMissing". Without this, it
// throws instead of returning null.
const content = consumer?.sourceContentFor(sourceMapItem.source, true)
if (content) {
const lines = content.split('\n')
const line = lines[sourceMapItem.originalLine - 1]
if (line) {
// It's possible this source map entry will contain stuff other than
// the name, so let's only consider word-ish characters that are part
// of the prefix.
const identifierMatch = /\w+/.exec(line.substr(sourceMapItem.originalColumn - 1))
if (identifierMatch) {
remappedFrameInfo.name = identifierMatch[0]
}
}
}
}
switch (remappedFrameInfo.name) {
case 'constructor': {
// If the name was remapped to "constructor", then let's use the
// original name, since "constructor" isn't very helpful.
//
// TODO(jlfwong): Search backwards for the class keyword and see if we
// can guess the right name.
remappedFrameInfo.name = frame.name + ' constructor'
break
}
case 'function': {
// If the name is just "function", it probably means we either messed up
// the remapping, or that we matched an anonymous function. In either
// case, this isn't helpful, so put this back.
remappedFrameInfo.name = frame.name
break
}
case 'const':
case 'export': {
// If we got this, we probably just did a bad job leveraging the hack
// looking through the source code. Let's fall-back to whatever the
// original name was.
remappedFrameInfo.name = frame.name
break
}
}
if (remappedFrameInfo.name && frame.name.includes(remappedFrameInfo.name)) {
// If the remapped name is a substring of the original name, the original
// name probably contains more useful information. In that case, just use
// the original name instead.
//
// This can happen, for example, when remapping method names. If a
// call stack says the symbol name is "n.zap" and we remapped it to a
// function just called "zap", we might as well use the original name
// instead.
remappedFrameInfo.name = frame.name
}
if (sourceMapItem.source != null) {
remappedFrameInfo.file = sourceMapItem.source
remappedFrameInfo.line = sourceMapItem.originalLine
remappedFrameInfo.col = sourceMapItem.originalColumn
}
if (DEBUG) {
console.groupCollapsed(`Remapping "${frame.name}" -> "${remappedFrameInfo.name}"`)
console.log('before', {...frame})
console.log('item @ index', sourceMapItem)
console.log('item @ index + 1', mappingItems[mappingIndex + 1])
console.log('after', remappedFrameInfo)
console.groupEnd()
}
return remappedFrameInfo
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* 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 (intentinoally) unmet peer dependencies.
* 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
+90
View File
@@ -0,0 +1,90 @@
import {Profile, Frame, CallTreeNode} from './profile'
import {FuzzyMatch, fuzzyMatchStrings} from './fuzzy-find'
import {Flamechart, FlamechartFrame} from './flamechart'
import {Rect, Vec2} from './math'
export enum FlamechartType {
CHRONO_FLAME_CHART,
LEFT_HEAVY_FLAME_GRAPH,
}
// A utility class for storing cached search results to avoid recomputation when
// the search results & profile did not change.
export class ProfileSearchResults {
constructor(readonly profile: Profile, readonly searchQuery: string) {}
private matches: Map<Frame, FuzzyMatch> | null = null
getMatchForFrame(frame: Frame): FuzzyMatch | null {
if (!this.matches) {
this.matches = new Map()
this.profile.forEachFrame(frame => {
const match = fuzzyMatchStrings(frame.name, this.searchQuery)
if (match == null) return
this.matches!.set(frame, match)
})
}
return this.matches.get(frame) || null
}
}
export interface FlamechartSearchMatch {
configSpaceBounds: Rect
node: CallTreeNode
}
interface CachedFlamechartResult {
matches: FlamechartSearchMatch[]
indexForNode: Map<CallTreeNode, number>
}
export class FlamechartSearchResults {
constructor(readonly flamechart: Flamechart, readonly profileResults: ProfileSearchResults) {}
private matches: CachedFlamechartResult | null = null
private getResults(): CachedFlamechartResult {
if (this.matches == null) {
const matches: FlamechartSearchMatch[] = []
const indexForNode = new Map<CallTreeNode, number>()
const visit = (frame: FlamechartFrame, depth: number) => {
const {node} = frame
if (this.profileResults.getMatchForFrame(node.frame)) {
const configSpaceBounds = new Rect(
new Vec2(frame.start, depth),
new Vec2(frame.end - frame.start, 1),
)
indexForNode.set(node, matches.length)
matches.push({configSpaceBounds, node})
}
frame.children.forEach(child => {
visit(child, depth + 1)
})
}
const layers = this.flamechart.getLayers()
if (layers.length > 0) {
layers[0].forEach(frame => visit(frame, 0))
}
this.matches = {matches, indexForNode}
}
return this.matches
}
count(): number {
return this.getResults().matches.length
}
indexOf(node: CallTreeNode): number | null {
const result = this.getResults().indexForNode.get(node)
return result === undefined ? null : result
}
at(index: number): FlamechartSearchMatch {
const matches = this.getResults().matches
if (index < 0 || index >= matches.length) {
throw new Error(`Index ${index} out of bounds in list of ${matches.length} matches.`)
}
return matches[index]
}
}
+52 -17
View File
@@ -3,9 +3,6 @@ import {ValueFormatter, RawValueFormatter} from './value-formatters'
import {FileFormat} from './file-format-spec'
const demangleCppModule = import('./demangle-cpp')
// Force eager loading of the module
demangleCppModule.then(() => {})
export interface FrameInfo {
key: string | number
@@ -17,13 +14,17 @@ export interface FrameInfo {
// call stack frame.
file?: string
// Line in the given file where this frame occurs
// Line in the given file where this frame occurs, 1-based.
line?: number
// Column in the file
// Column in the file, 1-based.
col?: number
}
export type SymbolRemapper = (
frame: Frame,
) => {name?: string; file?: string; line?: number; col?: number} | null
export class HasWeights {
private selfWeight = 0
private totalWeight = 0
@@ -116,6 +117,13 @@ export class Profile {
protected frames = new KeyedSet<Frame>()
// Profiles store two call-trees.
//
// The "append order" call tree is the one in which nodes are ordered in
// whatever order they were appended to their parent.
//
// The "grouped" call tree is one in which each node has at most one child per
// frame. Nodes are ordered in decreasing order of weight
protected appendOrderCalltreeRoot = new CallTreeNode(Frame.root, null)
protected groupedCalltreeRoot = new CallTreeNode(Frame.root, null)
@@ -137,6 +145,12 @@ export class Profile {
this.totalWeight = totalWeight
}
shallowClone(): Profile {
const profile = new Profile(this.totalWeight)
Object.assign(profile, this)
return profile
}
formatValue(v: number) {
return this.valueFormatter.format(v)
}
@@ -169,6 +183,17 @@ export class Profile {
return this.totalNonIdleWeight
}
// This is private because it should only be called in the ProfileBuilder
// classes. Once a Profile instance has been constructed, it should be treated
// as immutable.
protected sortGroupedCallTree() {
function visit(node: CallTreeNode) {
node.children.sort((a, b) => -(a.getTotalWeight() - b.getTotalWeight()))
node.children.forEach(visit)
}
visit(this.groupedCalltreeRoot)
}
forEachCallGrouped(
openFrame: (node: CallTreeNode, value: number) => void,
closeFrame: (node: CallTreeNode, value: number) => void,
@@ -180,10 +205,7 @@ export class Profile {
let childTime = 0
const children = [...node.children]
children.sort((a, b) => -(a.getTotalWeight() - b.getTotalWeight()))
children.forEach(function (child) {
node.children.forEach(function (child) {
visit(child, start + childTime)
childTime += child.getTotalWeight()
})
@@ -250,12 +272,6 @@ export class Profile {
this.frames.forEach(fn)
}
forEachSample(fn: (sample: CallTreeNode, weight: number) => void) {
for (let i = 0; i < this.samples.length; i++) {
fn(this.samples[i], this.weights[i])
}
}
getProfileWithRecursionFlattened(): Profile {
const builder = new CallTreeProfileBuilder()
@@ -399,9 +415,25 @@ export class Profile {
}
}
remapNames(callback: (name: string) => string) {
remapSymbols(callback: SymbolRemapper) {
for (let frame of this.frames) {
frame.name = callback(frame.name)
const remapped = callback(frame)
if (remapped == null) {
continue
}
const {name, file, line, col} = remapped
if (name != null) {
frame.name = name
}
if (file != null) {
frame.file = file
}
if (line != null) {
frame.line = line
}
if (col != null) {
frame.col = col
}
}
}
}
@@ -511,6 +543,7 @@ export class StackListProfileBuilder extends Profile {
this.totalWeight,
this.weights.reduce((a, b) => a + b, 0),
)
this.sortGroupedCallTree()
return this
}
}
@@ -588,6 +621,7 @@ export class CallTreeProfileBuilder extends Profile {
const frameCount = this.framesInStack.get(frame) || 0
this.framesInStack.set(frame, frameCount + 1)
this.lastValue = value
this.totalWeight = Math.max(this.totalWeight, this.lastValue)
}
private _leaveFrame(frame: Frame, value: number, useAppendOrder: boolean) {
@@ -651,6 +685,7 @@ export class CallTreeProfileBuilder extends Profile {
if (this.appendOrderStack.length > 1 || this.groupedOrderStack.length > 1) {
throw new Error('Tried to complete profile construction with a non-empty stack')
}
this.sortGroupedCallTree()
return this
}
}
+124
View File
@@ -0,0 +1,124 @@
import {buildTrimmedText, ELLIPSIS, remapRangesToTrimmedText} from './text-utils'
import {fuzzyMatchStrings} from './fuzzy-find'
function assertTrimmed(text: string, length: number, expectedTrimmed: string) {
expect(buildTrimmedText(text, length).trimmedString).toEqual(
expectedTrimmed.replace('...', ELLIPSIS),
)
}
test('buildTrimmedText', () => {
assertTrimmed('hello world', 1, '...')
assertTrimmed('hello world', 2, 'h...')
assertTrimmed('hello world', 3, 'h...d')
assertTrimmed('hello world', 4, 'he...d')
assertTrimmed('hello world', 10, 'hello...orld')
assertTrimmed('hello world', 11, 'hello world')
assertTrimmed('hello world', 100, 'hello world')
})
function highlightText(text: string, highlightedRanges: [number, number][]): string {
let last = 0
let highlighted = ''
for (let range of highlightedRanges) {
highlighted += `${text.slice(last, range[0])}[${text.slice(range[0], range[1])}]`
last = range[1]
}
highlighted += text.slice(last)
return highlighted
}
function assertTrimmedHighlight({
text,
pattern,
expectedHighlighted,
length,
expectedHighlightedTrimmed,
}: {
text: string
pattern: string
expectedHighlighted: string
length: number
expectedHighlightedTrimmed: string
}) {
const match = fuzzyMatchStrings(text, pattern)
const trimmed = buildTrimmedText(text, length)
if (!match) {
fail()
return
}
const matchedRangesForTrimmedText = remapRangesToTrimmedText(trimmed, match.matchedRanges)
const highlighted = highlightText(text, match.matchedRanges)
const highlightedTrimmed = highlightText(trimmed.trimmedString, matchedRangesForTrimmedText)
expect(highlighted).toEqual(expectedHighlighted)
expect(highlightedTrimmed).toEqual(expectedHighlightedTrimmed.replace('...', ELLIPSIS))
}
test('remapRangesToTrimmedText', () => {
assertTrimmedHighlight({
text: 'hello world',
pattern: 'he',
length: 4,
expectedHighlighted: '[he]llo world',
expectedHighlightedTrimmed: `[he]...d`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'o w',
length: 4,
expectedHighlighted: 'hell[o w]orld',
expectedHighlightedTrimmed: `he[...]d`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'ow',
length: 4,
expectedHighlighted: 'hell[o] [w]orld',
expectedHighlightedTrimmed: `he[...]d`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'hello',
length: 4,
expectedHighlighted: '[hello] world',
expectedHighlightedTrimmed: `[he...]d`,
})
assertTrimmedHighlight({
text: 'xxhello world',
pattern: 'hello',
length: 6,
expectedHighlighted: 'xx[hello] world',
expectedHighlightedTrimmed: `xx[h...]ld`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'hello world',
length: 4,
expectedHighlighted: '[hello world]',
expectedHighlightedTrimmed: `[he...d]`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'helloworld',
length: 4,
expectedHighlighted: '[hello] [world]',
expectedHighlightedTrimmed: `[he...][d]`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'world',
length: 4,
expectedHighlighted: 'hello [world]',
expectedHighlightedTrimmed: `he[...d]`,
})
})
+173 -10
View File
@@ -1,4 +1,4 @@
import {binarySearch} from './utils'
import {findValueBisect} from './utils'
export const ELLIPSIS = '\u2026'
@@ -18,22 +18,185 @@ export function cachedMeasureTextWidth(ctx: CanvasRenderingContext2D, text: stri
return measureTextCache.get(text)!
}
function buildTrimmedText(text: string, length: number) {
const prefixLength = Math.floor(length / 2)
const prefix = text.substr(0, prefixLength)
const suffix = text.substr(text.length - prefixLength, prefixLength)
return prefix + ELLIPSIS + suffix
interface TrimmedTextResult {
trimmedString: string
trimmedLength: number
prefixLength: number
suffixLength: number
originalLength: number
originalString: string
}
export function trimTextMid(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) return text
const [lo] = binarySearch(
// Trim text, placing an ellipsis in the middle, with a slight bias towards
// keeping text from the beginning rather than the end
export function buildTrimmedText(text: string, length: number): TrimmedTextResult {
if (text.length <= length) {
return {
trimmedString: text,
trimmedLength: text.length,
prefixLength: text.length,
suffixLength: 0,
originalString: text,
originalLength: text.length,
}
}
let prefixLength = Math.floor(length / 2)
const suffixLength = length - prefixLength - 1
const prefix = text.substr(0, prefixLength)
const suffix = text.substr(text.length - suffixLength, suffixLength)
const trimmedString = prefix + ELLIPSIS + suffix
return {
trimmedString,
trimmedLength: trimmedString.length,
prefixLength: prefix.length,
suffixLength: suffix.length,
originalString: text,
originalLength: text.length,
}
}
// Trim text to fit within the given number of pixels on the canvas
export function trimTextMid(
ctx: CanvasRenderingContext2D,
text: string,
maxWidth: number,
): TrimmedTextResult {
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) {
return buildTrimmedText(text, text.length)
}
const [lo] = findValueBisect(
0,
text.length,
n => {
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n))
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n).trimmedString)
},
maxWidth,
)
return buildTrimmedText(text, lo)
}
enum IndexTypeInTrimmed {
IN_PREFIX,
IN_SUFFIX,
ELIDED,
}
function getIndexTypeInTrimmed(result: TrimmedTextResult, index: number): IndexTypeInTrimmed {
if (index < result.prefixLength) {
return IndexTypeInTrimmed.IN_PREFIX
} else if (index < result.originalLength - result.suffixLength) {
return IndexTypeInTrimmed.ELIDED
} else {
return IndexTypeInTrimmed.IN_SUFFIX
}
}
export function remapRangesToTrimmedText(
trimmedText: TrimmedTextResult,
ranges: [number, number][],
): [number, number][] {
// We intentionally don't just re-run fuzzy matching on the trimmed
// text, beacuse if the search query is "helloWorld", the frame name
// is "application::helloWorld", and that gets trimmed down to
// "appl...oWorld", we still want "oWorld" to be highlighted, even
// though the string "appl...oWorld" is not matched by the query
// "helloWorld".
//
// There's a weird case to consider here: what if the trimmedText is
// also matched by the query, but results in a different match than
// the original query? Consider, e.g. the search string of "ab". The
// string "hello ab shabby" will be matched at the first "ab", but
// may be trimmed to "hello...shabby". In this case, should we
// highlight the "ab" hidden by the ellipsis, or the "ab" in
// "shabby"? The code below highlights the ellipsis so that the
// matched characters don't change as you zoom in and out.
const rangesToHighlightInTrimmedText: [number, number][] = []
const lengthLoss = trimmedText.originalLength - trimmedText.trimmedLength
let highlightedEllipsis = false
for (let [origStart, origEnd] of ranges) {
let startPosType = getIndexTypeInTrimmed(trimmedText, origStart)
let endPosType = getIndexTypeInTrimmed(trimmedText, origEnd - 1)
switch (startPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
switch (endPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
// The entire range fits in the prefix. Add it unmodified.
rangesToHighlightInTrimmedText.push([origStart, origEnd])
break
}
case IndexTypeInTrimmed.ELIDED: {
// The range starts in the prefix, but ends in the elided
// section. Add just the prefix + one char for the ellipsis.
rangesToHighlightInTrimmedText.push([origStart, trimmedText.prefixLength + 1])
highlightedEllipsis = true
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
// The range crosses from the prefix to the suffix.
// Highlight everything including the ellipsis.
rangesToHighlightInTrimmedText.push([origStart, origEnd - lengthLoss])
break
}
}
break
}
case IndexTypeInTrimmed.ELIDED: {
switch (endPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
// This should be impossible
throw new Error('Unexpected highlight range starts in elided and ends in prefix')
}
case IndexTypeInTrimmed.ELIDED: {
// The match starts & ends within the elided section.
if (!highlightedEllipsis) {
rangesToHighlightInTrimmedText.push([
trimmedText.prefixLength,
trimmedText.prefixLength + 1,
])
highlightedEllipsis = true
}
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
// The match starts in elided, but ends in suffix.
if (highlightedEllipsis) {
rangesToHighlightInTrimmedText.push([
trimmedText.trimmedLength - trimmedText.suffixLength,
origEnd - lengthLoss,
])
} else {
rangesToHighlightInTrimmedText.push([trimmedText.prefixLength, origEnd - lengthLoss])
highlightedEllipsis = true
}
break
}
}
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
switch (endPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
// This should be impossible
throw new Error('Unexpected highlight range starts in suffix and ends in prefix')
}
case IndexTypeInTrimmed.ELIDED: {
// This should be impossible
throw new Error('Unexpected highlight range starts in suffix and ends in elided')
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
// Match starts & ends in suffix
rangesToHighlightInTrimmedText.push([origStart - lengthLoss, origEnd - lengthLoss])
break
}
}
break
}
}
}
return rangesToHighlightInTrimmedText
}
+40 -3
View File
@@ -9,11 +9,12 @@ import {
zeroPad,
formatPercent,
KeyedSet,
binarySearch,
findValueBisect,
memoizeByReference,
memoizeByShallowEquality,
objectsHaveShallowEquality,
decodeBase64,
findIndexBisect,
} from './utils'
import * as jsc from 'jsverify'
@@ -109,13 +110,49 @@ test('formatPercent', () => {
expect(formatPercent(100)).toBe('100%')
})
test('binarySearch', () => {
const [lo, hi] = binarySearch(0, 10, n => Math.log(n), 1, 0.0001)
test('findValueBisect', () => {
const [lo, hi] = findValueBisect(0, 10, n => Math.log(n), 1, 0.0001)
expect(lo).toBeCloseTo(Math.E, 4)
expect(lo).toBeLessThan(Math.E)
expect(hi).toBeGreaterThan(Math.E)
})
test('findIndexBisect', () => {
const check = (haystack: number[], needle: number) => {
const condition = (v: number) => v > needle
expect(findIndexBisect(haystack, condition)).toEqual(haystack.findIndex(condition))
}
check([], 0)
check([0], 0)
check([0], -1)
check([0], 1)
check([0, 1], 0)
check([0, 1], 1)
check([0, 1], 2)
check([0, 1, 2], 2)
check([0, 1, 2], 2)
check([0, 1, 2], 2)
check([0, 1, 2], 2)
check([3, 5, 5, 7], 1)
check([3, 5, 5, 7], 2)
check([3, 5, 5, 7], 5)
check([3, 5, 5, 7], 7)
check([3, 5, 5, 7], 11)
jsc.assertForall(jsc.array(jsc.int8), jsc.int8, (haystack: number[], needle: number) => {
haystack.sort((a, b) => a - b)
const fn = (v: number) => v > needle
expect(findIndexBisect(haystack, fn)).toEqual(haystack.findIndex(fn))
return true
})
})
test('memoizeByReference', () => {
let hitCount = 0
const identity = memoizeByReference((arg: number) => {
+34 -1
View File
@@ -105,7 +105,7 @@ export function triangle(x: number) {
return 2.0 * Math.abs(fract(x) - 0.5) - 1.0
}
export function binarySearch(
export function findValueBisect(
lo: number,
hi: number,
f: (val: number) => number,
@@ -122,6 +122,39 @@ export function binarySearch(
}
}
// Similar to Array.prototype.findIndex, except uses a binary search.
//
// This assumes that the condition transitions exactly once from false to true
// in the list, e.g. the following is a valid input:
//
// ls = [a, b, c, d]
// ls.map(f) = [false, false, true, true]
//
// The following is an invalid input:
//
// ls = [a, b, c, d]
// ls.map(f) = [false, true, false, true]
export function findIndexBisect<T>(ls: T[], f: (val: T) => boolean): number {
if (ls.length === 0) return -1
let lo = 0
let hi = ls.length - 1
while (hi !== lo) {
const mid = Math.floor((lo + hi) / 2)
if (f(ls[mid])) {
// The desired index is <= mid
hi = mid
} else {
// The desired index is > mid
lo = mid + 1
}
}
return f(ls[hi]) ? hi : -1
}
export function noop(...args: any[]) {}
export function objectsHaveShallowEquality<T extends object>(a: T, b: T): boolean {
+4 -1
View File
@@ -2,6 +2,7 @@ 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}`)
@@ -20,7 +21,9 @@ const store = lastStore ? createAppStore(lastStore.getState()) : createAppStore(
render(
<Provider store={store}>
<ApplicationContainer />
<ThemeProvider>
<ApplicationContainer />
</ThemeProvider>
</Provider>,
document.body,
document.body.lastElementChild || undefined,
+2 -1
View File
@@ -1,7 +1,7 @@
import {actionCreator} from '../lib/typed-redux'
import {CallTreeNode, Frame, ProfileGroup} from '../lib/profile'
import {SortMethod} from '../views/profile-table-view'
import {ViewMode} from '.'
import {ColorScheme, ViewMode} from '.'
import {FlamechartID} from './flamechart-view-state'
import {Rect, Vec2} from '../lib/math'
import {HashParams} from '../lib/hash-params'
@@ -19,6 +19,7 @@ export namespace actions {
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')
+16 -17
View File
@@ -1,9 +1,9 @@
import {Frame, Profile} from '../lib/profile'
import {triangle, memoizeByReference, memoizeByShallowEquality} from '../lib/utils'
import {memoizeByReference, memoizeByShallowEquality} from '../lib/utils'
import {RowAtlas} from '../gl/row-atlas'
import {CanvasContext} from '../gl/canvas-context'
import {Color} from '../lib/color'
import {FlamechartRowAtlasKey} from '../gl/flamechart-renderer'
import {Theme} from '../views/themes/theme'
export const createGetColorBucketForFrame = memoizeByReference(
(frameToColorBucket: Map<number | string, number>) => {
@@ -13,24 +13,27 @@ export const createGetColorBucketForFrame = memoizeByReference(
},
)
export const createGetCSSColorForFrame = memoizeByReference(
(frameToColorBucket: Map<number | string, number>) => {
export const createGetCSSColorForFrame = memoizeByShallowEquality(
({
theme,
frameToColorBucket,
}: {
theme: Theme
frameToColorBucket: Map<number | string, number>
}) => {
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
return (frame: Frame): string => {
const t = getColorBucketForFrame(frame) / 255
const x = triangle(30.0 * t)
const H = 360.0 * (0.9 * t)
const C = 0.25 + 0.2 * x
const L = 0.8 - 0.15 * x
return Color.fromLumaChromaHue(L, C, H).toCSS()
return theme.colorForBucket(t).toCSS()
}
},
)
export const getCanvasContext = memoizeByReference((canvas: HTMLCanvasElement) => {
return new CanvasContext(canvas)
})
export const getCanvasContext = memoizeByShallowEquality(
({theme, canvas}: {theme: Theme; canvas: HTMLCanvasElement}) => {
return new CanvasContext(canvas, theme)
},
)
export const getRowAtlas = memoizeByReference((canvasContext: CanvasContext) => {
return new RowAtlas<FlamechartRowAtlasKey>(
@@ -40,10 +43,6 @@ export const getRowAtlas = memoizeByReference((canvasContext: CanvasContext) =>
)
})
export const getProfileWithRecursionFlattened = memoizeByReference((profile: Profile) =>
profile.getProfileWithRecursionFlattened(),
)
export const getProfileToView = memoizeByShallowEquality(
({profile, flattenRecursion}: {profile: Profile; flattenRecursion: boolean}): Profile => {
return flattenRecursion ? profile.getProfileWithRecursionFlattened() : profile
+89 -1
View File
@@ -6,11 +6,15 @@ import {actions} from './actions'
*/
import * as redux from 'redux'
import {setter, Reducer} from '../lib/typed-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,
@@ -18,6 +22,17 @@ export const enum ViewMode {
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
@@ -56,6 +71,9 @@ export interface ApplicationState {
// 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
@@ -65,6 +83,47 @@ const protocol = window.location.protocol
// 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()
@@ -92,6 +151,8 @@ export function createAppStore(initialState?: ApplicationState): redux.Store<App
field: SortField.SELF,
direction: SortDirection.DESCENDING,
}),
colorScheme,
})
return redux.createStore(reducer, initialState)
@@ -101,3 +162,30 @@ export function useAppSelector<T>(selector: (t: ApplicationState) => T, cacheArg
/* 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,
}
}, [])
}
+24 -35
View File
@@ -1,10 +1,12 @@
import {h} from 'preact'
import {Application, ActiveProfileState} from './application'
import {getProfileToView, getCanvasContext} from '../store/getters'
import {Application} from './application'
import {getCanvasContext} from '../store/getters'
import {actions} from '../store/actions'
import {useActionCreator} from '../lib/preact-redux'
import {memo} from 'preact/compat'
import {useAppSelector} from '../store'
import {useAppSelector, useActiveProfileState} from '../store'
import {ProfileSearchContextProvider} from './search-view'
import {useTheme} from './themes/theme'
const {
setLoading,
@@ -19,41 +21,28 @@ const {
export const ApplicationContainer = memo(() => {
const appState = useAppSelector(state => state, [])
const theme = useTheme()
const canvasContext = useAppSelector(
state => (state.glCanvas ? getCanvasContext(state.glCanvas) : null),
[],
state => (state.glCanvas ? getCanvasContext({theme, canvas: state.glCanvas}) : null),
[theme],
)
const activeProfileState: ActiveProfileState | null = 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,
}
}, [])
return (
<Application
activeProfileState={activeProfileState}
canvasContext={canvasContext}
setGLCanvas={useActionCreator(setGLCanvas, [])}
setLoading={useActionCreator(setLoading, [])}
setError={useActionCreator(setError, [])}
setProfileGroup={useActionCreator(setProfileGroup, [])}
setDragActive={useActionCreator(setDragActive, [])}
setViewMode={useActionCreator(setViewMode, [])}
setFlattenRecursion={useActionCreator(setFlattenRecursion, [])}
setProfileIndexToView={useActionCreator(setProfileIndexToView, [])}
{...appState}
/>
<ProfileSearchContextProvider>
<Application
activeProfileState={useActiveProfileState()}
canvasContext={canvasContext}
setGLCanvas={useActionCreator(setGLCanvas, [])}
setLoading={useActionCreator(setLoading, [])}
setError={useActionCreator(setError, [])}
setProfileGroup={useActionCreator(setProfileGroup, [])}
setDragActive={useActionCreator(setDragActive, [])}
setViewMode={useActionCreator(setViewMode, [])}
setFlattenRecursion={useActionCreator(setFlattenRecursion, [])}
setProfileIndexToView={useActionCreator(setProfileIndexToView, [])}
theme={theme}
{...appState}
/>
</ProfileSearchContextProvider>
)
})
+168 -127
View File
@@ -2,23 +2,28 @@ import {h} from 'preact'
import {StyleSheet, css} from 'aphrodite'
import {FileSystemDirectoryEntry} from '../import/file-system-entry'
import {Profile, ProfileGroup} from '../lib/profile'
import {FontFamily, FontSize, Colors, Duration} from './style'
import {importEmscriptenSymbolMap} from '../lib/emscripten'
import {ProfileGroup, SymbolRemapper} from '../lib/profile'
import {FontFamily, FontSize, Duration} from './style'
import {importEmscriptenSymbolMap as importEmscriptenSymbolRemapper} from '../lib/emscripten'
import {SandwichViewContainer} from './sandwich-view'
import {saveToFile} from '../lib/file-format'
import {ApplicationState, ViewMode, canUseXHR} from '../store'
import {ApplicationState, ViewMode, canUseXHR, ActiveProfileState} from '../store'
import {StatelessComponent} from '../lib/typed-redux'
import {LeftHeavyFlamechartView, ChronoFlamechartView} from './flamechart-view-container'
import {SandwichViewState} from '../store/sandwich-view-state'
import {FlamechartViewState} from '../store/flamechart-view-state'
import {CanvasContext} from '../gl/canvas-context'
import {Graphics} from '../gl/graphics'
import {Toolbar} from './toolbar'
import {importJavaScriptSourceMapSymbolRemapper} from '../lib/js-source-map'
import {Theme, withTheme} from './themes/theme'
const importModule = import('../import')
// Force eager loading of the module
// Force eager loading of a few code-split modules.
//
// We put them all in one place so we can directly control the relative priority
// of these.
importModule.then(() => {})
import('../lib/demangle-cpp').then(() => {})
import('source-map').then(() => {})
async function importProfilesFromText(
fileName: string,
@@ -53,6 +58,7 @@ const exampleProfileURL = require('../../sample/profiles/stackcollapse/perf-vert
interface GLCanvasProps {
canvasContext: CanvasContext | null
theme: Theme
setGLCanvas: (canvas: HTMLCanvasElement | null) => void
}
export class GLCanvas extends StatelessComponent<GLCanvasProps> {
@@ -94,7 +100,6 @@ export class GLCanvas extends StatelessComponent<GLCanvasProps> {
widthInAppUnits,
heightInAppUnits,
)
this.props.canvasContext.gl.clear(new Graphics.Color(1, 1, 1, 1))
}
onWindowResize = () => {
@@ -123,6 +128,7 @@ export class GLCanvas extends StatelessComponent<GLCanvasProps> {
window.removeEventListener('resize', this.onWindowResize)
}
render() {
const style = getStyle(this.props.theme)
return (
<div ref={this.containerRef} className={css(style.glCanvasView)}>
<canvas ref={this.ref} width={1} height={1} />
@@ -131,14 +137,6 @@ export class GLCanvas extends StatelessComponent<GLCanvasProps> {
}
}
export interface ActiveProfileState {
profile: Profile
index: number
chronoViewState: FlamechartViewState
leftHeavyViewState: FlamechartViewState
sandwichViewState: SandwichViewState
}
export type ApplicationProps = ApplicationState & {
setGLCanvas: (canvas: HTMLCanvasElement | null) => void
setLoading: (loading: boolean) => void
@@ -150,6 +148,7 @@ export type ApplicationProps = ApplicationState & {
setProfileIndexToView: (profileIndex: number) => void
activeProfileState: ActiveProfileState | null
canvasContext: CanvasContext | null
theme: Theme
}
export class Application extends StatelessComponent<ApplicationProps> {
@@ -204,6 +203,10 @@ export class Application extends StatelessComponent<ApplicationProps> {
this.props.setLoading(false)
}
getStyle(): ReturnType<typeof getStyle> {
return getStyle(this.props.theme)
}
loadFromFile(file: File) {
this.loadProfile(async () => {
const profiles = await importProfilesFromFile(file)
@@ -233,15 +236,36 @@ export class Application extends StatelessComponent<ApplicationProps> {
reader.readAsText(file)
const fileContents = await fileContentsPromise
const map = importEmscriptenSymbolMap(fileContents)
if (map) {
const {profile, index} = this.props.activeProfileState
let symbolRemapper: SymbolRemapper | null = null
const emscriptenSymbolRemapper = importEmscriptenSymbolRemapper(fileContents)
if (emscriptenSymbolRemapper) {
console.log('Importing as emscripten symbol map')
profile.remapNames(name => map.get(name) || name)
symbolRemapper = emscriptenSymbolRemapper
}
const jsSourceMapRemapper = await importJavaScriptSourceMapSymbolRemapper(
fileContents,
file.name,
)
if (!symbolRemapper && jsSourceMapRemapper) {
console.log('Importing as JavaScript source map')
symbolRemapper = jsSourceMapRemapper
}
if (symbolRemapper != null) {
return {
name: this.props.profileGroup.name || 'profile',
indexToView: index,
profiles: [profile],
indexToView: this.props.profileGroup.indexToView,
profiles: this.props.profileGroup.profiles.map(profileState => {
// We do a shallow clone here to invalidate certain caches keyed
// on a reference to the profile group under the assumption that
// profiles are immutable. Symbol remapping is (at time of
// writing) the only exception to that immutability.
const p = profileState.profile.shallowClone()
p.remapSymbols(symbolRemapper!)
return p
}),
}
}
}
@@ -413,6 +437,8 @@ export class Application extends StatelessComponent<ApplicationProps> {
}
renderLanding() {
const style = this.getStyle()
return (
<div className={css(style.landingContainer)}>
<div className={css(style.landingMessage)}>
@@ -484,6 +510,8 @@ export class Application extends StatelessComponent<ApplicationProps> {
}
renderError() {
const style = this.getStyle()
return (
<div className={css(style.error)}>
<div>😿 Something went wrong.</div>
@@ -493,6 +521,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
}
renderLoadingBar() {
const style = this.getStyle()
return <div className={css(style.loading)} />
}
@@ -527,6 +556,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
}
render() {
const style = this.getStyle()
return (
<div
onDrop={this.onDrop}
@@ -534,7 +564,11 @@ export class Application extends StatelessComponent<ApplicationProps> {
onDragLeave={this.onDragLeave}
className={css(style.root, this.props.dragActive && style.dragTargetRoot)}
>
<GLCanvas setGLCanvas={this.props.setGLCanvas} canvasContext={this.props.canvasContext} />
<GLCanvas
setGLCanvas={this.props.setGLCanvas}
canvasContext={this.props.canvasContext}
theme={this.props.theme}
/>
<Toolbar
saveFile={this.saveFile}
browseForFile={this.browseForFile}
@@ -547,107 +581,114 @@ export class Application extends StatelessComponent<ApplicationProps> {
}
}
const style = StyleSheet.create({
glCanvasView: {
position: 'absolute',
width: '100vw',
height: '100vh',
zIndex: -1,
pointerEvents: 'none',
},
error: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
},
loading: {
height: 3,
marginBottom: -3,
background: Colors.DARK_BLUE,
transformOrigin: '0% 50%',
animationName: [
{
from: {
transform: `scaleX(0)`,
},
to: {
transform: `scaleX(1)`,
},
},
],
animationTimingFunction: 'cubic-bezier(0, 1, 0, 1)',
animationDuration: '30s',
},
root: {
width: '100vw',
height: '100vh',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
position: 'relative',
fontFamily: FontFamily.MONOSPACE,
lineHeight: '20px',
},
dragTargetRoot: {
cursor: 'copy',
},
dragTarget: {
boxSizing: 'border-box',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
border: `5px dashed ${Colors.DARK_BLUE}`,
pointerEvents: 'none',
},
contentContainer: {
position: 'relative',
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
flex: 1,
},
landingContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: 1,
},
landingMessage: {
maxWidth: 600,
},
landingP: {
marginBottom: 16,
},
hide: {
display: 'none',
},
browseButtonContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
browseButton: {
marginBottom: 16,
height: 72,
flex: 1,
maxWidth: 256,
textAlign: 'center',
fontSize: FontSize.BIG_BUTTON,
lineHeight: '72px',
background: Colors.DARK_BLUE,
color: Colors.WHITE,
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
':hover': {
background: Colors.BRIGHT_BLUE,
const getStyle = withTheme(theme =>
StyleSheet.create({
glCanvasView: {
position: 'absolute',
width: '100vw',
height: '100vh',
zIndex: -1,
pointerEvents: 'none',
},
},
link: {
color: Colors.BRIGHT_BLUE,
cursor: 'pointer',
textDecoration: 'none',
},
})
error: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
},
loading: {
height: 3,
marginBottom: -3,
background: theme.selectionPrimaryColor,
transformOrigin: '0% 50%',
animationName: [
{
from: {
transform: `scaleX(0)`,
},
to: {
transform: `scaleX(1)`,
},
},
],
animationTimingFunction: 'cubic-bezier(0, 1, 0, 1)',
animationDuration: '30s',
},
root: {
width: '100vw',
height: '100vh',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
position: 'relative',
fontFamily: FontFamily.MONOSPACE,
lineHeight: '20px',
color: theme.fgPrimaryColor,
},
dragTargetRoot: {
cursor: 'copy',
},
dragTarget: {
boxSizing: 'border-box',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
border: `5px dashed ${theme.selectionPrimaryColor}`,
pointerEvents: 'none',
},
contentContainer: {
position: 'relative',
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
flex: 1,
},
landingContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: 1,
},
landingMessage: {
maxWidth: 600,
},
landingP: {
marginBottom: 16,
},
hide: {
display: 'none',
},
browseButtonContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
browseButton: {
marginBottom: 16,
height: 72,
flex: 1,
maxWidth: 256,
textAlign: 'center',
fontSize: FontSize.BIG_BUTTON,
lineHeight: '72px',
background: theme.selectionPrimaryColor,
color: theme.altFgPrimaryColor,
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
':hover': {
background: theme.selectionSecondaryColor,
},
},
link: {
color: theme.selectionPrimaryColor,
cursor: 'pointer',
textDecoration: 'none',
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
':hover': {
color: theme.selectionSecondaryColor,
},
},
}),
)
+6 -3
View File
@@ -17,6 +17,7 @@ import {FlamechartWrapper} from './flamechart-wrapper'
import {useAppSelector} from '../store'
import {h} from 'preact'
import {memo} from 'preact/compat'
import {useTheme} from './themes/theme'
const getCalleeProfile = memoizeByShallowEquality<
{
@@ -52,6 +53,7 @@ export const CalleeFlamegraphView = memo((ownProps: FlamechartViewContainerProps
const {index, profile, sandwichViewState} = activeProfileState
const flattenRecursion = useAppSelector(state => state.flattenRecursion, [])
const glCanvas = useAppSelector(state => state.glCanvas, [])
const theme = useTheme()
if (!profile) throw new Error('profile missing')
if (!glCanvas) throw new Error('glCanvas missing')
@@ -61,8 +63,8 @@ export const CalleeFlamegraphView = memo((ownProps: FlamechartViewContainerProps
const frameToColorBucket = getFrameToColorBucket(profile)
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
const canvasContext = getCanvasContext(glCanvas)
const getCSSColorForFrame = createGetCSSColorForFrame({theme, frameToColorBucket})
const canvasContext = getCanvasContext({theme, canvas: glCanvas})
const flamechart = getCalleeFlamegraph({
calleeProfile: getCalleeProfile({profile, frame: selectedFrame, flattenRecursion}),
@@ -72,15 +74,16 @@ export const CalleeFlamegraphView = memo((ownProps: FlamechartViewContainerProps
return (
<FlamechartWrapper
theme={theme}
renderInverted={false}
flamechart={flamechart}
flamechartRenderer={flamechartRenderer}
canvasContext={canvasContext}
getCSSColorForFrame={getCSSColorForFrame}
{...useFlamechartSetters(FlamechartID.SANDWICH_CALLEES, index)}
{...callerCallee.calleeFlamegraph}
// This overrides the setSelectedNode specified in useFlamechartSettesr
setSelectedNode={noop}
{...callerCallee.calleeFlamegraph}
/>
)
})
+17 -13
View File
@@ -1,24 +1,28 @@
import {h} from 'preact'
import {StyleSheet, css} from 'aphrodite'
import {Colors, FontSize} from './style'
import {FontSize} from './style'
import {useTheme, withTheme} from './themes/theme'
interface ColorChitProps {
color: string
}
export function ColorChit(props: ColorChitProps) {
const style = getStyle(useTheme())
return <span className={css(style.stackChit)} style={{backgroundColor: props.color}} />
}
const style = StyleSheet.create({
stackChit: {
position: 'relative',
top: -1,
display: 'inline-block',
verticalAlign: 'middle',
marginRight: '0.5em',
border: `1px solid ${Colors.LIGHT_GRAY}`,
width: FontSize.LABEL - 2,
height: FontSize.LABEL - 2,
},
})
const getStyle = withTheme(theme =>
StyleSheet.create({
stackChit: {
position: 'relative',
top: -1,
display: 'inline-block',
verticalAlign: 'middle',
marginRight: '0.5em',
border: `1px solid ${theme.fgSecondaryColor}`,
width: FontSize.LABEL - 2,
height: FontSize.LABEL - 2,
},
}),
)
+84 -83
View File
@@ -1,10 +1,11 @@
import {StyleDeclarationValue, css} from 'aphrodite'
import {h, Component, JSX} from 'preact'
import {style} from './flamechart-style'
import {h, JSX} from 'preact'
import {getFlamechartStyle} from './flamechart-style'
import {formatPercent} from '../lib/utils'
import {Frame, CallTreeNode} from '../lib/profile'
import {ColorChit} from './color-chit'
import {Flamechart} from '../lib/flamechart'
import {useTheme} from './themes/theme'
interface StatisticsTableProps {
title: string
@@ -15,75 +16,75 @@ interface StatisticsTableProps {
formatter: (v: number) => string
}
class StatisticsTable extends Component<StatisticsTableProps, {}> {
render() {
const total = this.props.formatter(this.props.selectedTotal)
const self = this.props.formatter(this.props.selectedSelf)
const totalPerc = (100.0 * this.props.selectedTotal) / this.props.grandTotal
const selfPerc = (100.0 * this.props.selectedSelf) / this.props.grandTotal
function StatisticsTable(props: StatisticsTableProps) {
const style = getFlamechartStyle(useTheme())
return (
<div className={css(style.statsTable)}>
<div className={css(this.props.cellStyle, style.statsTableCell, style.statsTableHeader)}>
{this.props.title}
</div>
const total = props.formatter(props.selectedTotal)
const self = props.formatter(props.selectedSelf)
const totalPerc = (100.0 * props.selectedTotal) / props.grandTotal
const selfPerc = (100.0 * props.selectedSelf) / props.grandTotal
<div className={css(this.props.cellStyle, style.statsTableCell)}>Total</div>
<div className={css(this.props.cellStyle, style.statsTableCell)}>Self</div>
<div className={css(this.props.cellStyle, style.statsTableCell)}>{total}</div>
<div className={css(this.props.cellStyle, style.statsTableCell)}>{self}</div>
<div className={css(this.props.cellStyle, style.statsTableCell)}>
{formatPercent(totalPerc)}
<div className={css(style.barDisplay)} style={{height: `${totalPerc}%`}} />
</div>
<div className={css(this.props.cellStyle, style.statsTableCell)}>
{formatPercent(selfPerc)}
<div className={css(style.barDisplay)} style={{height: `${selfPerc}%`}} />
</div>
return (
<div className={css(style.statsTable)}>
<div className={css(props.cellStyle, style.statsTableCell, style.statsTableHeader)}>
{props.title}
</div>
)
}
<div className={css(props.cellStyle, style.statsTableCell)}>Total</div>
<div className={css(props.cellStyle, style.statsTableCell)}>Self</div>
<div className={css(props.cellStyle, style.statsTableCell)}>{total}</div>
<div className={css(props.cellStyle, style.statsTableCell)}>{self}</div>
<div className={css(props.cellStyle, style.statsTableCell)}>
{formatPercent(totalPerc)}
<div className={css(style.barDisplay)} style={{height: `${totalPerc}%`}} />
</div>
<div className={css(props.cellStyle, style.statsTableCell)}>
{formatPercent(selfPerc)}
<div className={css(style.barDisplay)} style={{height: `${selfPerc}%`}} />
</div>
</div>
)
}
interface StackTraceViewProps {
getFrameColor: (frame: Frame) => string
node: CallTreeNode
}
class StackTraceView extends Component<StackTraceViewProps, {}> {
render() {
const rows: JSX.Element[] = []
let node: CallTreeNode | null = this.props.node
for (; node && !node.isRoot(); node = node.parent) {
const row: (JSX.Element | string)[] = []
const {frame} = node
function StackTraceView(props: StackTraceViewProps) {
const style = getFlamechartStyle(useTheme())
row.push(<ColorChit color={this.props.getFrameColor(frame)} />)
const rows: JSX.Element[] = []
let node: CallTreeNode | null = props.node
for (; node && !node.isRoot(); node = node.parent) {
const row: (JSX.Element | string)[] = []
const {frame} = node
if (rows.length) {
row.push(<span className={css(style.stackFileLine)}>&gt; </span>)
}
row.push(frame.name)
row.push(<ColorChit color={props.getFrameColor(frame)} />)
if (frame.file) {
let pos = frame.file
if (frame.line) {
pos += `:${frame.line}`
if (frame.col) {
pos += `:${frame.col}`
}
}
row.push(<span className={css(style.stackFileLine)}> ({pos})</span>)
}
rows.push(<div className={css(style.stackLine)}>{row}</div>)
if (rows.length) {
row.push(<span className={css(style.stackFileLine)}>&gt; </span>)
}
return (
<div className={css(style.stackTraceView)}>
<div className={css(style.stackTraceViewPadding)}>{rows}</div>
</div>
)
row.push(frame.name)
if (frame.file) {
let pos = frame.file
if (frame.line != null) {
pos += `:${frame.line}`
if (frame.col != null) {
pos += `:${frame.col}`
}
}
row.push(<span className={css(style.stackFileLine)}> ({pos})</span>)
}
rows.push(<div className={css(style.stackLine)}>{row}</div>)
}
return (
<div className={css(style.stackTraceView)}>
<div className={css(style.stackTraceViewPadding)}>{rows}</div>
</div>
)
}
interface FlamechartDetailViewProps {
@@ -92,31 +93,31 @@ interface FlamechartDetailViewProps {
selectedNode: CallTreeNode
}
export class FlamechartDetailView extends Component<FlamechartDetailViewProps, {}> {
render() {
const {flamechart, selectedNode} = this.props
const {frame} = selectedNode
export function FlamechartDetailView(props: FlamechartDetailViewProps) {
const style = getFlamechartStyle(useTheme())
return (
<div className={css(style.detailView)}>
<StatisticsTable
title={'This Instance'}
cellStyle={style.thisInstanceCell}
grandTotal={flamechart.getTotalWeight()}
selectedTotal={selectedNode.getTotalWeight()}
selectedSelf={selectedNode.getSelfWeight()}
formatter={flamechart.formatValue.bind(flamechart)}
/>
<StatisticsTable
title={'All Instances'}
cellStyle={style.allInstancesCell}
grandTotal={flamechart.getTotalWeight()}
selectedTotal={frame.getTotalWeight()}
selectedSelf={frame.getSelfWeight()}
formatter={flamechart.formatValue.bind(flamechart)}
/>
<StackTraceView node={selectedNode} getFrameColor={this.props.getCSSColorForFrame} />
</div>
)
}
const {flamechart, selectedNode} = props
const {frame} = selectedNode
return (
<div className={css(style.detailView)}>
<StatisticsTable
title={'This Instance'}
cellStyle={style.thisInstanceCell}
grandTotal={flamechart.getTotalWeight()}
selectedTotal={selectedNode.getTotalWeight()}
selectedSelf={selectedNode.getSelfWeight()}
formatter={flamechart.formatValue.bind(flamechart)}
/>
<StatisticsTable
title={'All Instances'}
cellStyle={style.allInstancesCell}
grandTotal={flamechart.getTotalWeight()}
selectedTotal={frame.getTotalWeight()}
selectedSelf={frame.getSelfWeight()}
formatter={flamechart.formatValue.bind(flamechart)}
/>
<StackTraceView node={selectedNode} getFrameColor={props.getCSSColorForFrame} />
</div>
)
}
+25 -4
View File
@@ -3,12 +3,16 @@ import {css} from 'aphrodite'
import {Flamechart} from '../lib/flamechart'
import {Rect, Vec2, AffineTransform, clamp} from '../lib/math'
import {FlamechartRenderer} from '../gl/flamechart-renderer'
import {style} from './flamechart-style'
import {FontFamily, FontSize, Colors, Sizes, commonStyle} from './style'
import {getFlamechartStyle} from './flamechart-style'
import {FontFamily, FontSize, Sizes, commonStyle} from './style'
import {CanvasContext} from '../gl/canvas-context'
import {cachedMeasureTextWidth} from '../lib/text-utils'
import {Color} from '../lib/color'
import {Theme} from './themes/theme'
interface FlamechartMinimapViewProps {
theme: Theme
flamechart: Flamechart
configSpaceViewportRect: Rect
@@ -40,6 +44,10 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
)
}
private getStyle() {
return getFlamechartStyle(this.props.theme)
}
private minimapOrigin() {
return new Vec2(0, Sizes.FRAME_HEIGHT * window.devicePixelRatio)
}
@@ -132,19 +140,22 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
interval *= 2
}
const theme = this.props.theme
{
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
ctx.fillStyle = Color.fromCSSHex(theme.bgPrimaryColor).withAlpha(0.8).toCSS()
ctx.fillRect(0, 0, physicalViewSize.x, physicalViewSpaceFrameHeight)
ctx.textBaseline = 'top'
ctx.fillStyle = Colors.DARK_GRAY
for (let x = Math.ceil(left / interval) * interval; x < right; x += interval) {
// TODO(jlfwong): Ensure that labels do not overlap
const pos = Math.round(configToPhysical.transformPosition(new Vec2(x, 0)).x)
const labelText = this.props.flamechart.formatValue(x)
const textWidth = Math.ceil(cachedMeasureTextWidth(ctx, labelText))
ctx.fillStyle = theme.fgPrimaryColor
ctx.fillText(labelText, pos - textWidth - labelPaddingPx, labelPaddingPx)
ctx.fillStyle = theme.fgSecondaryColor
ctx.fillRect(pos, 0, 1, physicalViewSize.y)
}
}
@@ -159,6 +170,14 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
this.renderCanvas()
} else if (this.props.configSpaceViewportRect != nextProps.configSpaceViewportRect) {
this.renderCanvas()
} else if (this.props.canvasContext !== nextProps.canvasContext) {
if (this.props.canvasContext) {
this.props.canvasContext.removeBeforeFrameHandler(this.onBeforeFrame)
}
if (nextProps.canvasContext) {
nextProps.canvasContext.addBeforeFrameHandler(this.onBeforeFrame)
nextProps.canvasContext.requestFrame()
}
}
}
@@ -406,6 +425,8 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
}
render() {
const style = this.getStyle()
return (
<div
ref={this.containerRef}
+153 -69
View File
@@ -3,11 +3,20 @@ import {CallTreeNode} from '../lib/profile'
import {Flamechart, FlamechartFrame} from '../lib/flamechart'
import {CanvasContext} from '../gl/canvas-context'
import {FlamechartRenderer} from '../gl/flamechart-renderer'
import {Sizes, FontSize, Colors, FontFamily, commonStyle} from './style'
import {cachedMeasureTextWidth, ELLIPSIS, trimTextMid} from '../lib/text-utils'
import {style} from './flamechart-style'
import {Sizes, FontSize, FontFamily, commonStyle} from './style'
import {
cachedMeasureTextWidth,
ELLIPSIS,
trimTextMid,
remapRangesToTrimmedText,
} from '../lib/text-utils'
import {getFlamechartStyle} from './flamechart-style'
import {h, Component} from 'preact'
import {css} from 'aphrodite'
import {ProfileSearchResults} from '../lib/profile-search'
import {BatchCanvasTextRenderer, BatchCanvasRectRenderer} from '../lib/canvas-2d-batch-renderers'
import {Color} from '../lib/color'
import {Theme} from './themes/theme'
interface FlamechartFrameLabel {
configSpaceBounds: Rect
@@ -38,6 +47,7 @@ export interface FlamechartPanZoomViewProps {
flamechartRenderer: FlamechartRenderer
renderInverted: boolean
selectedNode: CallTreeNode | null
theme: Theme
onNodeHover: (hover: {node: CallTreeNode; event: MouseEvent} | null) => void
onNodeSelect: (node: CallTreeNode | null) => void
@@ -47,7 +57,9 @@ export interface FlamechartPanZoomViewProps {
setConfigSpaceViewportRect: (rect: Rect) => void
logicalSpaceViewportSize: Vec2
setLogicalSpaceViewportBounds: (size: Vec2) => void
setLogicalSpaceViewportSize: (size: Vec2) => void
searchResults: ProfileSearchResults | null
}
export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps, {}> {
@@ -61,6 +73,10 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
private hoveredLabel: FlamechartFrameLabel | null = null
private getStyle() {
return getFlamechartStyle(this.props.theme)
}
private setConfigSpaceViewportRect(r: Rect) {
this.props.setConfigSpaceViewportRect(r)
}
@@ -163,27 +179,8 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
ctx.clearRect(0, 0, physicalViewSize.x, physicalViewSize.y)
if (this.hoveredLabel) {
let color = Colors.DARK_GRAY
if (this.props.selectedNode === this.hoveredLabel.node) {
color = Colors.DARK_BLUE
}
ctx.lineWidth = 2 * devicePixelRatio
ctx.strokeStyle = color
const physicalViewBounds = configToPhysical.transformRect(this.hoveredLabel.configSpaceBounds)
ctx.strokeRect(
Math.round(physicalViewBounds.left()),
Math.round(physicalViewBounds.top()),
Math.round(Math.max(0, physicalViewBounds.width())),
Math.round(Math.max(0, physicalViewBounds.height())),
)
}
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${FontFamily.MONOSPACE}`
ctx.textBaseline = 'alphabetic'
ctx.fillStyle = Colors.DARK_GRAY
const minWidthToRender = cachedMeasureTextWidth(ctx, 'M' + ELLIPSIS + 'M')
const minConfigSpaceWidthToRender = (
@@ -192,6 +189,13 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
const LABEL_PADDING_PX = 5 * window.devicePixelRatio
const labelBatch = new BatchCanvasTextRenderer()
const fadedLabelBatch = new BatchCanvasTextRenderer()
const matchedTextHighlightBatch = new BatchCanvasRectRenderer()
const directlySelectedOutlineBatch = new BatchCanvasRectRenderer()
const indirectlySelectedOutlineBatch = new BatchCanvasRectRenderer()
const matchedFrameBatch = new BatchCanvasRectRenderer()
const renderFrameLabelAndChildren = (frame: FlamechartFrame, depth = 0) => {
const width = frame.end - frame.start
const y = this.props.renderInverted ? this.configSpaceSize().y - 1 - depth : depth
@@ -226,22 +230,58 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
}
if (physicalLabelBounds.width() > minWidthToRender) {
const match = this.props.searchResults?.getMatchForFrame(frame.node.frame)
const trimmedText = trimTextMid(
ctx,
frame.node.frame.name,
physicalLabelBounds.width() - 2 * LABEL_PADDING_PX,
)
// Note that this is specifying the position of the starting text
// baseline.
ctx.fillText(
trimmedText,
physicalLabelBounds.left() + LABEL_PADDING_PX,
Math.round(
if (match) {
const rangesToHighlightInTrimmedText = remapRangesToTrimmedText(
trimmedText,
match.matchedRanges,
)
// Once we have the character ranges to highlight, we need to
// actually do the highlighting.
let lastEndIndex = 0
let left = physicalLabelBounds.left() + LABEL_PADDING_PX
const padding = (physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2 - 2
for (let [startIndex, endIndex] of rangesToHighlightInTrimmedText) {
left += cachedMeasureTextWidth(
ctx,
trimmedText.trimmedString.substring(lastEndIndex, startIndex),
)
const highlightWidth = cachedMeasureTextWidth(
ctx,
trimmedText.trimmedString.substring(startIndex, endIndex),
)
matchedTextHighlightBatch.rect({
x: left,
y: physicalLabelBounds.top() + padding,
w: highlightWidth,
h: physicalViewSpaceFrameHeight - 2 * padding,
})
left += highlightWidth
lastEndIndex = endIndex
}
}
const batch = this.props.searchResults != null && !match ? fadedLabelBatch : labelBatch
batch.text({
text: trimmedText.trimmedString,
// This is specifying the position of the starting text baseline.
x: physicalLabelBounds.left() + LABEL_PADDING_PX,
y: Math.round(
physicalLabelBounds.bottom() -
(physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2,
),
)
})
}
}
for (let child of frame.children) {
@@ -249,18 +289,14 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
}
}
for (let frame of this.props.flamechart.getLayers()[0] || []) {
renderFrameLabelAndChildren(frame)
}
const frameOutlineWidth = 2 * window.devicePixelRatio
ctx.strokeStyle = Colors.PALE_DARK_BLUE
ctx.lineWidth = frameOutlineWidth
ctx.strokeStyle = this.props.theme.selectionSecondaryColor
const minConfigSpaceWidthToRenderOutline = (
configToPhysical.inverseTransformVector(new Vec2(1, 0)) || new Vec2(0, 0)
).x
const renderIndirectlySelectedFrameOutlines = (frame: FlamechartFrame, depth = 0) => {
if (!this.props.selectedNode) return
const renderSpecialFrameOutlines = (frame: FlamechartFrame, depth = 0) => {
if (!this.props.selectedNode && this.props.searchResults == null) return
const width = frame.end - frame.start
const y = this.props.renderInverted ? this.configSpaceSize().y - 1 - depth : depth
const configSpaceBounds = new Rect(new Vec2(frame.start, y), new Vec2(width, 1))
@@ -271,44 +307,73 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
if (configSpaceBounds.top() > this.props.configSpaceViewportRect.bottom()) return
if (configSpaceBounds.hasIntersectionWith(this.props.configSpaceViewportRect)) {
const physicalRectBounds = configToPhysical.transformRect(configSpaceBounds)
if (this.props.searchResults?.getMatchForFrame(frame.node.frame)) {
const physicalRectBounds = configToPhysical.transformRect(configSpaceBounds)
matchedFrameBatch.rect({
x: Math.round(physicalRectBounds.left() + frameOutlineWidth / 2),
y: Math.round(physicalRectBounds.top() + frameOutlineWidth / 2),
w: Math.round(Math.max(0, physicalRectBounds.width() - frameOutlineWidth)),
h: Math.round(Math.max(0, physicalRectBounds.height() - frameOutlineWidth)),
})
}
if (frame.node.frame === this.props.selectedNode.frame) {
if (frame.node === this.props.selectedNode) {
if (ctx.strokeStyle !== Colors.DARK_BLUE) {
ctx.stroke()
ctx.beginPath()
ctx.strokeStyle = Colors.DARK_BLUE
}
} else {
if (ctx.strokeStyle !== Colors.PALE_DARK_BLUE) {
ctx.stroke()
ctx.beginPath()
ctx.strokeStyle = Colors.PALE_DARK_BLUE
}
}
if (this.props.selectedNode != null && frame.node.frame === this.props.selectedNode.frame) {
let batch =
frame.node === this.props.selectedNode
? directlySelectedOutlineBatch
: indirectlySelectedOutlineBatch
// Identify the flamechart frames with a function that matches the
// selected flamechart frame.
ctx.rect(
Math.round(physicalRectBounds.left() + 1 + frameOutlineWidth / 2),
Math.round(physicalRectBounds.top() + 1 + frameOutlineWidth / 2),
Math.round(Math.max(0, physicalRectBounds.width() - 2 - frameOutlineWidth)),
Math.round(Math.max(0, physicalRectBounds.height() - 2 - frameOutlineWidth)),
)
const physicalRectBounds = configToPhysical.transformRect(configSpaceBounds)
batch.rect({
x: Math.round(physicalRectBounds.left() + 1 + frameOutlineWidth / 2),
y: Math.round(physicalRectBounds.top() + 1 + frameOutlineWidth / 2),
w: Math.round(Math.max(0, physicalRectBounds.width() - 2 - frameOutlineWidth)),
h: Math.round(Math.max(0, physicalRectBounds.height() - 2 - frameOutlineWidth)),
})
}
}
for (let child of frame.children) {
renderIndirectlySelectedFrameOutlines(child, depth + 1)
renderSpecialFrameOutlines(child, depth + 1)
}
}
ctx.beginPath()
for (let frame of this.props.flamechart.getLayers()[0] || []) {
renderIndirectlySelectedFrameOutlines(frame)
renderSpecialFrameOutlines(frame)
}
for (let frame of this.props.flamechart.getLayers()[0] || []) {
renderFrameLabelAndChildren(frame)
}
const theme = this.props.theme
matchedFrameBatch.fill(ctx, theme.searchMatchPrimaryColor)
matchedTextHighlightBatch.fill(ctx, theme.searchMatchSecondaryColor)
fadedLabelBatch.fill(ctx, theme.fgSecondaryColor)
labelBatch.fill(
ctx,
this.props.searchResults != null ? theme.searchMatchTextColor : theme.fgPrimaryColor,
)
indirectlySelectedOutlineBatch.stroke(ctx, theme.selectionSecondaryColor, frameOutlineWidth)
directlySelectedOutlineBatch.stroke(ctx, theme.selectionPrimaryColor, frameOutlineWidth)
if (this.hoveredLabel) {
let color: string = theme.fgPrimaryColor
if (this.props.selectedNode === this.hoveredLabel.node) {
color = theme.selectionPrimaryColor
}
ctx.lineWidth = 2 * devicePixelRatio
ctx.strokeStyle = color
const physicalViewBounds = configToPhysical.transformRect(this.hoveredLabel.configSpaceBounds)
ctx.strokeRect(
Math.round(physicalViewBounds.left()),
Math.round(physicalViewBounds.top()),
Math.round(Math.max(0, physicalViewBounds.width())),
Math.round(Math.max(0, physicalViewBounds.height())),
)
}
ctx.stroke()
this.renderTimeIndicators()
}
@@ -341,19 +406,22 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
interval *= 2
}
const theme = this.props.theme
{
const y = this.props.renderInverted ? physicalViewSize.y - physicalViewSpaceFrameHeight : 0
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
ctx.fillStyle = Color.fromCSSHex(theme.bgPrimaryColor).withAlpha(0.8).toCSS()
ctx.fillRect(0, y, physicalViewSize.x, physicalViewSpaceFrameHeight)
ctx.fillStyle = Colors.DARK_GRAY
ctx.textBaseline = 'top'
for (let x = Math.ceil(left / interval) * interval; x < right; x += interval) {
// TODO(jlfwong): Ensure that labels do not overlap
const pos = Math.round(configToPhysical.transformPosition(new Vec2(x, 0)).x)
const labelText = this.props.flamechart.formatValue(x)
const textWidth = cachedMeasureTextWidth(ctx, labelText)
ctx.fillStyle = theme.fgPrimaryColor
ctx.fillText(labelText, pos - textWidth - labelPaddingPx, y + labelPaddingPx)
ctx.fillStyle = theme.fgSecondaryColor
ctx.fillRect(pos, 0, 1, physicalViewSize.y)
}
}
@@ -396,7 +464,11 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
),
)
}
this.props.setLogicalSpaceViewportBounds(new Vec2(width, height))
const newSize = new Vec2(width, height)
if (!newSize.equals(logicalSpaceViewportSize)) {
this.props.setLogicalSpaceViewportSize(newSize)
}
}
onWindowResize = () => {
@@ -686,10 +758,20 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
if (this.props.flamechart !== nextProps.flamechart) {
this.hoveredLabel = null
this.renderCanvas()
} else if (this.props.searchResults !== nextProps.searchResults) {
this.renderCanvas()
} else if (this.props.selectedNode !== nextProps.selectedNode) {
this.renderCanvas()
} else if (this.props.configSpaceViewportRect !== nextProps.configSpaceViewportRect) {
this.renderCanvas()
} else if (this.props.canvasContext !== nextProps.canvasContext) {
if (this.props.canvasContext) {
this.props.canvasContext.removeBeforeFrameHandler(this.onBeforeFrame)
}
if (nextProps.canvasContext) {
nextProps.canvasContext.addBeforeFrameHandler(this.onBeforeFrame)
nextProps.canvasContext.requestFrame()
}
}
}
componentDidMount() {
@@ -704,6 +786,8 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
}
render() {
const style = this.getStyle()
return (
<div
className={css(style.panZoomView, commonStyle.vbox)}
+151
View File
@@ -0,0 +1,151 @@
import {memo} from 'preact/compat'
import {useContext, useMemo, useCallback} from 'preact/hooks'
import {SearchView, ProfileSearchContext} from './search-view'
import {
FlamechartSearchMatch,
FlamechartSearchResults,
ProfileSearchResults,
} from '../lib/profile-search'
import {Rect, Vec2} from '../lib/math'
import {h, createContext, ComponentChildren} from 'preact'
import {Flamechart} from '../lib/flamechart'
import {CallTreeNode} from '../lib/profile'
export const FlamechartSearchContext = createContext<FlamechartSearchData | null>(null)
export interface FlamechartSearchProps {
flamechart: Flamechart
selectedNode: CallTreeNode | null
setSelectedNode: (node: CallTreeNode | null) => void
configSpaceViewportRect: Rect
setConfigSpaceViewportRect: (rect: Rect) => void
children: ComponentChildren
}
interface FlamechartSearchData {
results: FlamechartSearchResults | null
flamechart: Flamechart
selectedNode: CallTreeNode | null
setSelectedNode: (node: CallTreeNode | null) => void
configSpaceViewportRect: Rect
setConfigSpaceViewportRect: (rect: Rect) => void
}
export const FlamechartSearchContextProvider = ({
flamechart,
selectedNode,
setSelectedNode,
configSpaceViewportRect,
setConfigSpaceViewportRect,
children,
}: FlamechartSearchProps) => {
const profileSearchResults: ProfileSearchResults | null = useContext(ProfileSearchContext)
const flamechartSearchResults: FlamechartSearchResults | null = useMemo(() => {
if (profileSearchResults == null) {
return null
}
return new FlamechartSearchResults(flamechart, profileSearchResults)
}, [flamechart, profileSearchResults])
return (
<FlamechartSearchContext.Provider
value={{
results: flamechartSearchResults,
flamechart,
selectedNode,
setSelectedNode,
configSpaceViewportRect,
setConfigSpaceViewportRect,
}}
>
{children}
</FlamechartSearchContext.Provider>
)
}
export const FlamechartSearchView = memo(() => {
const flamechartData = useContext(FlamechartSearchContext)
// TODO(jlfwong): This pattern is pretty gross, but I really don't want values
// that can be undefined or null.
const searchResults = flamechartData == null ? null : flamechartData.results
const selectedNode = flamechartData == null ? null : flamechartData.selectedNode
const setSelectedNode = flamechartData == null ? null : flamechartData.setSelectedNode
const configSpaceViewportRect =
flamechartData == null ? null : flamechartData.configSpaceViewportRect
const setConfigSpaceViewportRect =
flamechartData == null ? null : flamechartData.setConfigSpaceViewportRect
const flamechart = flamechartData == null ? null : flamechartData.flamechart
const numResults = searchResults == null ? null : searchResults.count()
const resultIndex: number | null = useMemo(() => {
if (searchResults == null) return null
if (selectedNode == null) return null
return searchResults.indexOf(selectedNode)
}, [searchResults, selectedNode])
const selectAndZoomToMatch = useCallback(
(match: FlamechartSearchMatch) => {
if (!setSelectedNode) return
if (!flamechart) return
if (!configSpaceViewportRect) return
if (!setConfigSpaceViewportRect) return
// After the node is selected, we want to set the viewport so that the new
// node can be seen clearly.
//
// TODO(jlfwong): The lack of animation here can be kind of jarring. It
// would be nice to have some easier way for people to orient themselves
// after the viewport shifted.
const configSpaceResultBounds = match.configSpaceBounds
const viewportRect = new Rect(
configSpaceResultBounds.origin.minus(new Vec2(0, 1)),
configSpaceResultBounds.size.withY(configSpaceViewportRect.height()),
)
setSelectedNode(match.node)
setConfigSpaceViewportRect(
flamechart.getClampedConfigSpaceViewportRect({configSpaceViewportRect: viewportRect}),
)
},
[configSpaceViewportRect, setConfigSpaceViewportRect, setSelectedNode, flamechart],
)
const {selectPrev, selectNext} = useMemo(() => {
if (numResults == null || numResults === 0 || searchResults == null) {
return {selectPrev: () => {}, selectNext: () => {}}
}
return {
selectPrev: () => {
if (!searchResults?.at) return
if (numResults == null || numResults === 0) return
let index = resultIndex == null ? numResults - 1 : resultIndex - 1
if (index < 0) index = numResults - 1
const result = searchResults.at(index)
selectAndZoomToMatch(result)
},
selectNext: () => {
if (!searchResults?.at) return
if (numResults == null || numResults === 0) return
let index = resultIndex == null ? 0 : resultIndex + 1
if (index >= numResults) index = 0
const result = searchResults.at(index)
selectAndZoomToMatch(result)
},
}
}, [numResults, resultIndex, searchResults, searchResults?.at, selectAndZoomToMatch])
return (
<SearchView
resultIndex={resultIndex}
numResults={numResults}
selectPrev={selectPrev}
selectNext={selectNext}
/>
)
})
+93 -79
View File
@@ -1,82 +1,96 @@
import {StyleSheet} from 'aphrodite'
import {FontSize, Colors, Sizes} from './style'
import {FontSize, Sizes} from './style'
import {withTheme} from './themes/theme'
export const style = StyleSheet.create({
hoverCount: {
color: Colors.GREEN,
},
fill: {
width: '100%',
height: '100%',
position: 'absolute',
left: 0,
top: 0,
},
minimap: {
height: Sizes.MINIMAP_HEIGHT,
borderBottom: `${Sizes.SEPARATOR_HEIGHT}px solid ${Colors.LIGHT_GRAY}`,
},
panZoomView: {
flex: 1,
},
export const getFlamechartStyle = withTheme(theme =>
StyleSheet.create({
hoverCount: {
color: theme.weightColor,
},
fill: {
width: '100%',
height: '100%',
position: 'absolute',
left: 0,
top: 0,
},
minimap: {
height: Sizes.MINIMAP_HEIGHT,
borderBottom: `${Sizes.SEPARATOR_HEIGHT}px solid ${theme.fgSecondaryColor}`,
},
panZoomView: {
flex: 1,
},
detailView: {
display: 'grid',
height: Sizes.DETAIL_VIEW_HEIGHT,
overflow: 'hidden',
gridTemplateColumns: '120px 120px 1fr',
gridTemplateRows: 'repeat(4, 1fr)',
borderTop: `${Sizes.SEPARATOR_HEIGHT}px solid ${Colors.LIGHT_GRAY}`,
fontSize: FontSize.LABEL,
position: 'absolute',
background: Colors.WHITE,
width: '100vw',
bottom: 0,
},
stackTraceViewPadding: {
padding: 5,
},
stackTraceView: {
height: Sizes.DETAIL_VIEW_HEIGHT,
lineHeight: `${FontSize.LABEL + 2}px`,
overflow: 'auto',
},
stackLine: {
whiteSpace: 'nowrap',
},
stackFileLine: {
color: Colors.LIGHT_GRAY,
},
statsTable: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gridTemplateRows: `repeat(3, ${FontSize.LABEL + 10}px)`,
gridGap: '1px 1px',
textAlign: 'center',
paddingRight: 1,
},
statsTableHeader: {
gridColumn: '1 / 3',
},
statsTableCell: {
position: 'relative',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
thisInstanceCell: {
background: Colors.DARK_BLUE,
color: Colors.WHITE,
},
allInstancesCell: {
background: Colors.PALE_DARK_BLUE,
color: Colors.WHITE,
},
barDisplay: {
position: 'absolute',
top: 0,
left: 0,
background: 'rgba(0, 0, 0, 0.2)',
width: '100%',
},
})
detailView: {
display: 'grid',
height: Sizes.DETAIL_VIEW_HEIGHT,
overflow: 'hidden',
gridTemplateColumns: '120px 120px 1fr',
gridTemplateRows: 'repeat(4, 1fr)',
borderTop: `${Sizes.SEPARATOR_HEIGHT}px solid ${theme.fgSecondaryColor}`,
fontSize: FontSize.LABEL,
position: 'absolute',
background: theme.bgPrimaryColor,
width: '100vw',
bottom: 0,
},
stackTraceViewPadding: {
padding: 5,
},
stackTraceView: {
height: Sizes.DETAIL_VIEW_HEIGHT,
lineHeight: `${FontSize.LABEL + 2}px`,
overflow: 'auto',
'::-webkit-scrollbar': {
background: theme.bgPrimaryColor,
},
'::-webkit-scrollbar-thumb': {
background: theme.fgSecondaryColor,
borderRadius: 20,
border: `3px solid ${theme.bgPrimaryColor}`,
':hover': {
background: theme.fgPrimaryColor,
},
},
},
stackLine: {
whiteSpace: 'nowrap',
},
stackFileLine: {
color: theme.fgSecondaryColor,
},
statsTable: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gridTemplateRows: `repeat(3, ${FontSize.LABEL + 10}px)`,
gridGap: '1px 1px',
textAlign: 'center',
paddingRight: 1,
},
statsTableHeader: {
gridColumn: '1 / 3',
},
statsTableCell: {
position: 'relative',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
thisInstanceCell: {
background: theme.selectionPrimaryColor,
color: theme.altFgPrimaryColor,
},
allInstancesCell: {
background: theme.selectionSecondaryColor,
color: theme.altFgPrimaryColor,
},
barDisplay: {
position: 'absolute',
top: 0,
left: 0,
background: 'rgba(0, 0, 0, 0.2)',
width: '100%',
},
}),
)
+50 -21
View File
@@ -14,10 +14,12 @@ import {
createGetCSSColorForFrame,
getFrameToColorBucket,
} from '../store/getters'
import {ActiveProfileState} from './application'
import {Vec2, Rect} from '../lib/math'
import {actions} from '../store/actions'
import {memo} from 'preact/compat'
import {ActiveProfileState} from '../store'
import {FlamechartSearchContextProvider} from './flamechart-search-view'
import {Theme, useTheme} from './themes/theme'
interface FlamechartSetters {
setLogicalSpaceViewportSize: (logicalSpaceViewportSize: Vec2) => void
@@ -59,6 +61,7 @@ export function useFlamechartSetters(id: FlamechartID, profileIndex: number): Fl
}
export type FlamechartViewProps = {
theme: Theme
canvasContext: CanvasContext
flamechart: Flamechart
flamechartRenderer: FlamechartRenderer
@@ -115,10 +118,12 @@ export const ChronoFlamechartView = memo((props: FlamechartViewContainerProps) =
const {activeProfileState, glCanvas} = props
const {index, profile, chronoViewState} = activeProfileState
const canvasContext = getCanvasContext(glCanvas)
const theme = useTheme()
const canvasContext = getCanvasContext({theme, canvas: glCanvas})
const frameToColorBucket = getFrameToColorBucket(profile)
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
const getCSSColorForFrame = createGetCSSColorForFrame({theme, frameToColorBucket})
const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame})
const flamechartRenderer = getChronoViewFlamechartRenderer({
@@ -126,16 +131,27 @@ export const ChronoFlamechartView = memo((props: FlamechartViewContainerProps) =
flamechart,
})
const setters = useFlamechartSetters(FlamechartID.CHRONO, index)
return (
<FlamechartView
renderInverted={false}
<FlamechartSearchContextProvider
flamechart={flamechart}
flamechartRenderer={flamechartRenderer}
canvasContext={canvasContext}
getCSSColorForFrame={getCSSColorForFrame}
{...useFlamechartSetters(FlamechartID.CHRONO, index)}
{...chronoViewState}
/>
selectedNode={chronoViewState.selectedNode}
setSelectedNode={setters.setSelectedNode}
configSpaceViewportRect={chronoViewState.configSpaceViewportRect}
setConfigSpaceViewportRect={setters.setConfigSpaceViewportRect}
>
<FlamechartView
theme={theme}
renderInverted={false}
flamechart={flamechart}
flamechartRenderer={flamechartRenderer}
canvasContext={canvasContext}
getCSSColorForFrame={getCSSColorForFrame}
{...chronoViewState}
{...setters}
/>
</FlamechartSearchContextProvider>
)
})
@@ -163,10 +179,12 @@ export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerPr
const {index, profile, leftHeavyViewState} = activeProfileState
const canvasContext = getCanvasContext(glCanvas)
const theme = useTheme()
const canvasContext = getCanvasContext({theme, canvas: glCanvas})
const frameToColorBucket = getFrameToColorBucket(profile)
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
const getCSSColorForFrame = createGetCSSColorForFrame({theme, frameToColorBucket})
const flamechart = getLeftHeavyFlamechart({
profile,
@@ -177,15 +195,26 @@ export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerPr
flamechart,
})
const setters = useFlamechartSetters(FlamechartID.LEFT_HEAVY, index)
return (
<FlamechartView
renderInverted={false}
<FlamechartSearchContextProvider
flamechart={flamechart}
flamechartRenderer={flamechartRenderer}
canvasContext={canvasContext}
getCSSColorForFrame={getCSSColorForFrame}
{...useFlamechartSetters(FlamechartID.LEFT_HEAVY, index)}
{...leftHeavyViewState}
/>
selectedNode={leftHeavyViewState.selectedNode}
setSelectedNode={setters.setSelectedNode}
configSpaceViewportRect={leftHeavyViewState.configSpaceViewportRect}
setConfigSpaceViewportRect={setters.setConfigSpaceViewportRect}
>
<FlamechartView
theme={theme}
renderInverted={false}
flamechart={flamechart}
flamechartRenderer={flamechartRenderer}
canvasContext={canvasContext}
getCSSColorForFrame={getCSSColorForFrame}
{...leftHeavyViewState}
{...setters}
/>
</FlamechartSearchContextProvider>
)
})
+36 -16
View File
@@ -1,4 +1,4 @@
import {h} from 'preact'
import {h, Fragment} from 'preact'
import {css} from 'aphrodite'
import {CallTreeNode} from '../lib/profile'
@@ -7,15 +7,21 @@ import {Rect, Vec2, AffineTransform} from '../lib/math'
import {formatPercent} from '../lib/utils'
import {FlamechartMinimapView} from './flamechart-minimap-view'
import {style} from './flamechart-style'
import {Sizes, commonStyle} from './style'
import {FlamechartDetailView} from './flamechart-detail-view'
import {FlamechartPanZoomView} from './flamechart-pan-zoom-view'
import {Hovertip} from './hovertip'
import {FlamechartViewProps} from './flamechart-view-container'
import {StatelessComponent} from '../lib/typed-redux'
import {ProfileSearchContext} from './search-view'
import {FlamechartSearchView} from './flamechart-search-view'
import {getFlamechartStyle} from './flamechart-style'
export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
private getStyle() {
return getFlamechartStyle(this.props.theme)
}
private configSpaceSize() {
return new Vec2(
this.props.flamechart.getTotalWeight(),
@@ -75,6 +81,8 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
const {width, height, left, top} = this.container.getBoundingClientRect()
const offset = new Vec2(hover.event.clientX - left, hover.event.clientY - top)
const style = this.getStyle()
return (
<Hovertip containerSize={new Vec2(width, height)} offset={offset}>
<span className={css(style.hoverCount)}>
@@ -91,9 +99,12 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
}
render() {
const style = this.getStyle()
return (
<div className={css(style.fill, commonStyle.vbox)} ref={this.containerRef}>
<FlamechartMinimapView
theme={this.props.theme}
configSpaceViewportRect={this.props.configSpaceViewportRect}
transformViewport={this.transformViewport}
flamechart={this.props.flamechart}
@@ -101,20 +112,29 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
canvasContext={this.props.canvasContext}
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
/>
<FlamechartPanZoomView
canvasContext={this.props.canvasContext}
flamechart={this.props.flamechart}
flamechartRenderer={this.props.flamechartRenderer}
renderInverted={false}
onNodeHover={this.onNodeHover}
onNodeSelect={this.onNodeClick}
selectedNode={this.props.selectedNode}
transformViewport={this.transformViewport}
configSpaceViewportRect={this.props.configSpaceViewportRect}
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
logicalSpaceViewportSize={this.props.logicalSpaceViewportSize}
setLogicalSpaceViewportBounds={this.setLogicalSpaceViewportSize}
/>
<ProfileSearchContext.Consumer>
{searchResults => (
<Fragment>
<FlamechartPanZoomView
theme={this.props.theme}
canvasContext={this.props.canvasContext}
flamechart={this.props.flamechart}
flamechartRenderer={this.props.flamechartRenderer}
renderInverted={false}
onNodeHover={this.onNodeHover}
onNodeSelect={this.onNodeClick}
selectedNode={this.props.selectedNode}
transformViewport={this.transformViewport}
configSpaceViewportRect={this.props.configSpaceViewportRect}
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
logicalSpaceViewportSize={this.props.logicalSpaceViewportSize}
setLogicalSpaceViewportSize={this.setLogicalSpaceViewportSize}
searchResults={searchResults}
/>
<FlamechartSearchView />
</Fragment>
)}
</ProfileSearchContext.Consumer>
{this.renderTooltip()}
{this.props.selectedNode && (
<FlamechartDetailView
+18 -16
View File
@@ -1,26 +1,22 @@
import {CallTreeNode} from '../lib/profile'
import {StyleSheet, css} from 'aphrodite'
import {h} from 'preact'
import {commonStyle, Colors} from './style'
import {commonStyle} from './style'
import {Rect, AffineTransform, Vec2} from '../lib/math'
import {FlamechartPanZoomView} from './flamechart-pan-zoom-view'
import {noop, formatPercent} from '../lib/utils'
import {Hovertip} from './hovertip'
import {FlamechartViewProps} from './flamechart-view-container'
import {StatelessComponent} from '../lib/typed-redux'
import {withTheme} from './themes/theme'
export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
private clampViewportToFlamegraph(viewportRect: Rect) {
const {flamechart, renderInverted} = this.props
const configSpaceSize = new Vec2(flamechart.getTotalWeight(), flamechart.getLayers().length)
const width = this.props.flamechart.getClampedViewportWidth(viewportRect.size.x)
const size = viewportRect.size.withX(width)
const origin = Vec2.clamp(
viewportRect.origin,
new Vec2(0, renderInverted ? 0 : -1),
Vec2.max(Vec2.zero, configSpaceSize.minus(size).plus(new Vec2(0, 1))),
)
return new Rect(origin, viewportRect.size.withX(width))
return flamechart.getClampedConfigSpaceViewportRect({
configSpaceViewportRect: viewportRect,
renderInverted,
})
}
private setConfigSpaceViewportRect = (configSpaceViewportRect: Rect) => {
this.props.setConfigSpaceViewportRect(this.clampViewportToFlamegraph(configSpaceViewportRect))
@@ -44,6 +40,8 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
if (!hover) return null
const {width, height, left, top} = this.container.getBoundingClientRect()
const offset = new Vec2(hover.event.clientX - left, hover.event.clientY - top)
const style = getStyle(this.props.theme)
return (
<Hovertip containerSize={new Vec2(width, height)} offset={offset}>
<span className={css(style.hoverCount)}>
@@ -72,6 +70,7 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
ref={this.containerRef}
>
<FlamechartPanZoomView
theme={this.props.theme}
selectedNode={null}
onNodeHover={this.setNodeHover}
onNodeSelect={noop}
@@ -83,7 +82,8 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
canvasContext={this.props.canvasContext}
renderInverted={this.props.renderInverted}
logicalSpaceViewportSize={this.props.logicalSpaceViewportSize}
setLogicalSpaceViewportBounds={this.setLogicalSpaceViewportSize}
setLogicalSpaceViewportSize={this.setLogicalSpaceViewportSize}
searchResults={null}
/>
{this.renderTooltip()}
</div>
@@ -91,8 +91,10 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
}
}
export const style = StyleSheet.create({
hoverCount: {
color: Colors.GREEN,
},
})
export const getStyle = withTheme(theme =>
StyleSheet.create({
hoverCount: {
color: theme.weightColor,
},
}),
)

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