Compare commits

...
56 Commits
Author SHA1 Message Date
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
Jamie Wong ff447c2719 1.7.0 2020-07-13 22:10:28 -07:00
Jamie Wong 668bb032ba Introduce filtering via Ctrl+F/Cmd+F into the sandwich view (#293)
This is the first step towards fixing #38. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This PR seeks to address both of those.

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

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

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

## Test Plan

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

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

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

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

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

Make max width bigger

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

After:

![after](https://user-images.githubusercontent.com/52132927/67621730-04cb2500-f813-11e9-8d36-80e8c58a529e.png)
2019-10-28 11:36:15 -07:00
Jamie Wong cc9750923a Update README.md 2019-10-16 00:15:06 -07:00
Jonathan Chan c3074b7343 1.5.2 2019-10-10 18:27:28 -07:00
Jonathan Chan b15a08b3ff Support newer Emscripten .symbols with hex escapes (#233)
Apparently Emscripten now generates `.symbols` files where names are not mangled using Clang's mangling scheme, but rather hex-escaped! So 'a\20b' means 'a b'. Currently we can't import these symbol maps into Speedscope because a regex rejects them, and they look weird because we don't unescape.
2019-10-10 14:31:34 -07:00
Jamie Wong 68683aa054 Add pyspeedscope & flamescope to README 2019-10-06 15:10:00 -07:00
Jamie Wong eb0e1ce731 Add py-spy to README 2019-10-06 14:59:17 -07:00
Jamie Wong bd546ca893 1.5.1 2019-06-06 00:07:14 -07:00
Jamie Wong 0c1e477f35 Support import from trace event format event when there are too many "E" events. (#222)
Fixes #221
2019-06-06 00:03:08 -07:00
Jamie Wong 30ca6291ca 1.5.0 2019-02-17 18:38:17 -08:00
Jamie Wong 66a9e5d1cf Support importing unterminated JSON in simple cases (#208)
This PR introduces support for importing JSON based profiles that are missing a terminating `]` (and possibly have an extraneous `,`).

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

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

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

This was tested against a profile import from Android Chrome 66.
2019-02-17 17:46:09 -08:00
Archerlly abd74be9fa add default instruments selected run number (#203)
this's will lack `com.apple.xray.owner.template` in instruments archive data where run instruments with command line.
like:
1. run`instruments -t Template.tracetemplate -D demo.trace -l 10000 -w  test.app`
2. drag `demo.trace` into `https://www.speedscope.app`
3. alert `Unrecognized format! See documentation about supported formats`
2019-02-17 17:45:51 -08:00
Jamie Wong c706bdfe04 Revert "Support importing partial JSON files (#202)"
This reverts commit cfc8fe8f6e.
2019-02-08 18:33:30 -08:00
Marcin Kolny cfc8fe8f6e Support importing partial JSON files (#202)
Partial files are allowed in many specs, e.g. Trace Event Format,
so the viewer should be able to load partial files as well.
2019-02-08 18:08:51 -08:00
144 changed files with 41358 additions and 10626 deletions
+11 -4
View File
@@ -1,13 +1,20 @@
module.exports = {
parser: 'typescript-eslint-parser',
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['prettier'],
plugins: ['prettier', '@typescript-eslint', 'react-hooks'],
rules: {
'prettier/prettier': 'error',
'@typescript-eslint/explicit-function-return-type': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': [
'error',
{
additionalHooks: '(useSelector|useAppSelector|useActionCreator)',
},
],
},
};
}
+43
View File
@@ -0,0 +1,43 @@
name: Node.js CI
on:
push:
branches:
- master
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
env:
CI: true
- name: Coveralls Parallel
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
flag-name: run-${{ matrix.node-version }}
parallel: true
finish:
needs: test
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
-3
View File
@@ -1,3 +0,0 @@
language: node_js
node_js:
- '9'
+131 -34
View File
@@ -1,133 +1,230 @@
## Unreleased
## [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
### Added
- Introduced filtering via Ctrl+F/Cmd+F into the sandwich view [[#293](https://github.com/jlfwong/speedscope/pull/293)]
## [1.6.0] - 2020-05-30
### Added
- Improved profile/thread selection UI [[#282](https://github.com/jlfwong/speedscope/pull/282)]
### Fixed
- Crash instead of incorrectly interpreting profiles with incorrectly ordered events [[#273](https://github.com/jlfwong/speedscope/pull/273)]
- A large refactor to upgrade to Preact X was performed [[#267](https://github.com/jlfwong/speedscope/pull/267)]
## [1.5.3] - 2020-01-16
### Fixed
- Bump dependency versions to unbreak build [[#253](https://github.com/jlfwong/speedscope/pull/253)] (by [@jlfwong](https://github.com/jlfwong), with changes from [@Archerlly](https://github.com/Archerlly)'s [#215](https://github.com/jlfwong/speedscope/pull/215))
- Trace event: Prevent event re-ordering from generating incorrect flamegraphs ([#252](https://github.com/jlfwong/speedscope/pull/252), with changes from [@hwajaywang](https://github.com/hwajaywang)'s [#249](https://github.com/jlfwong/speedscope/pull/249))
- Make tooltip width wider [[#239](https://github.com/jlfwong/speedscope/pull/239)] (by [@miso11](https://github.com/miso11))
## [1.5.2] - 2019-10-10
### Fixed
- Fix emscripten remapping when symbols are hex-escaped, like `a\20b` [[#233](https://github.com/jlfwong/speedscope/pull/233)] (by [@jyc](https://github.com/jyc))
## [1.5.1] - 2019-06-06
### Fixed
- Fixed import of trace event files which contain unmatched "E" events ([#222](https://github.com/jlfwong/speedscope/pull/222)) (by [@jlfwong](https://github.com/jlfwong))
## [1.5.0] - 2019-02-17
### Added
- Support importing unterminated JSON in simple cases ([#208](https://github.com/jlfwong/speedscope/pull/208)) (by [@jlfwong](https://github.com/jlfwong))
### Fixed
- Fix crash when importing from stackprof without raw_timestamp_deltas ([#207](https://github.com/jlfwong/speedscope/pull/207)) (by [@jlfwong](https://github.com/jlfwong))
- Alert instead of crash when importing a file containing no profiles ([#205](https://github.com/jlfwong/speedscope/pull/205)) (by [@jlfwong](https://github.com/jlfwong))
- Fixed import of multithreaded profiles from Chrome 66 ([#206](https://github.com/jlfwong/speedscope/pull/206)) (by [@jlfwong](https://github.com/jlfwong))
- Fixed import of instruments trace files with missing run number ([#203](https://github.com/jlfwong/speedscope/pull/203)) (by [@Archerlly](https://github.com/Archerlly))
## [1.4.1] - 2019-01-22
### Fixed
* Fix importing of Trace Event Format files with no ts field on M events [#198] (by @jlfwong)
- Fix importing of Trace Event Format files with no ts field on M events [[#198](https://github.com/jlfwong/speedscope/pull/198)] (by [@jlfwong](https://github.com/jlfwong))
## [1.4.0] - 2019-01-22
### Added
* Import v8 cpu profile (old format) [#177] (by @vmarchaud)
* Import basic "Trace Event Format" profiles [#197] (by @jlfwong)
- Import v8 cpu profile (old format) [[#177](https://github.com/jlfwong/speedscope/pull/177)] (by [@vmarchaud](https://github.com/vmarchaud))
- Import basic "Trace Event Format" profiles [[#197](https://github.com/jlfwong/speedscope/pull/197)] (by [@jlfwong](https://github.com/jlfwong))
## [1.3.2] - 2018-12-03
### Fixed
* Fixed import of multithreaded Chrome profiles [#19] (by @jlfwong)
- Fixed import of multithreaded Chrome profiles [[#19](https://github.com/jlfwong/speedscope/pull/19)] (by [@jlfwong](https://github.com/jlfwong))
## [1.3.1] - 2018-11-08
### Fixed
* Fixed a file import performance regression by using TextDecoder [#188] (by @jlfwong)
- Fixed a file import performance regression by using TextDecoder [[#188](https://github.com/jlfwong/speedscope/pull/188)] (by [@jlfwong](https://github.com/jlfwong))
## [1.3.0] - 2018-10-29
### Added
* Support import from Haskell GHC JSON format support [#183] (by @trishume)
- Support import from Haskell GHC JSON format support [[#183](https://github.com/jlfwong/speedscope/pull/183)] (by [@trishume](https://github.com/trishume))
### Fixed
* Make the wasd keymappings work on azerty keyboards [#184] (by @vrischmann)
* Fix import of binary formats via profileURL [#179] (by @f-hj)
- Make the wasd keymappings work on azerty keyboards [[#184](https://github.com/jlfwong/speedscope/pull/184)] (by [@vrischmann](https://github.com/vrischmann))
- Fix import of binary formats via profileURL [[#179](https://github.com/jlfwong/speedscope/pull/179)] (by [@f](https://github.com/f)-hj)
## [1.2.0] - 2018-10-08
### Added
* Add import of v8 heap allocation profile [#170] (by @vmarchaud)
- Add import of v8 heap allocation profile [[#170](https://github.com/jlfwong/speedscope/pull/170)] (by [@vmarchaud](https://github.com/vmarchaud))
## [1.1.0] - 2018-09-26
### Added
* Add go tool pprof import support [#165]
- Add go tool pprof import support [[#165](https://github.com/jlfwong/speedscope/pull/165)]
## [1.0.4] - 2018-09-12
### Fixed
* Fix import from Chrome < 69 when there are multiple profiles [#161]
- Fix import from Chrome < 69 when there are multiple profiles [[#161](https://github.com/jlfwong/speedscope/pull/161)]
## [1.0.3] - 2018-09-10
### Fixed
* Fix import for Chrome 69, support leading idle time before first call [#160]
- Fix import for Chrome 69, support leading idle time before first call [[#160](https://github.com/jlfwong/speedscope/pull/160)]
## [1.0.2] - 2018-09-04
### Fixed
* Allow optional CR before LF when probing collapsed stacks files [#154]
* Fix import for Firefox 63 [#156]
* Change time formatting for minutes from 1.50min to 1:30 [#153] (by @Alex-Diez)
- Allow optional CR before LF when probing collapsed stacks files [[#154](https://github.com/jlfwong/speedscope/pull/154)]
- Fix import for Firefox 63 [[#156](https://github.com/jlfwong/speedscope/pull/156)]
- Change time formatting for minutes from 1.50min to 1:30 [[#153](https://github.com/jlfwong/speedscope/pull/153)] (by [@Alex](https://github.com/Alex)-Diez)
## [1.0.1] - 2018-08-23
* Fixed an issue where flamegraph bounds were not always being cleared correctly, leading to visual artifacts [#150]
- Fixed an issue where flamegraph bounds were not always being cleared correctly, leading to visual artifacts [[#150](https://github.com/jlfwong/speedscope/pull/150)]
## [1.0.0] - 2018-08-23
### Fixed
* Fixed rendering issues when switching between screens w/ different `devicePixelRatios` [#147]
- Fixed rendering issues when switching between screens w/ different `devicePixelRatios` [[#147](https://github.com/jlfwong/speedscope/pull/147)]
## [0.7.1] - 2018-08-20
### Fixed
* Removed dependency on regl in order to allow speedscope to run in strict content-security-policy environments [#140]
* Fixed text culling bug [#143]
- Removed dependency on regl in order to allow speedscope to run in strict content-security-policy environments [[#140](https://github.com/jlfwong/speedscope/pull/140)]
- Fixed text culling bug [[#143](https://github.com/jlfwong/speedscope/pull/143)]
## [0.7.0] - 2018-08-16
### Added
* Added support to import from linux `perf script` [#135]
- Added support to import from linux `perf script` [[#135](https://github.com/jlfwong/speedscope/pull/135)]
## [0.6.0] - 2018-08-14
### Added
* Added support for multiple threads/processes [#130]
* Import all runs & threads from Instruments .trace files instead of just main thread from selected run [#130]
- Added support for multiple threads/processes [[#130](https://github.com/jlfwong/speedscope/pull/130)]
- Import all runs & threads from Instruments .trace files instead of just main thread from selected run [[#130](https://github.com/jlfwong/speedscope/pull/130)]
### Fixed
* Ensure the JSON schema has actual contents [#133]
- Ensure the JSON schema has actual contents [[#133](https://github.com/jlfwong/speedscope/pull/133)]
## [0.5.1] - 2018-08-09
### Fixed
* Fixed broken CLI
- Fixed broken CLI
## [0.5.0] - 2018-08-09
### Fixed
* Fix emscripten remapping when symbols contain dashes, like `527:i32s-div` [#129]
* Improved firefox import speed and fixed bugs in it [#128]
* Prevent non-contiguous blocks in the time ordered flamechart from appearing as a single node for selection [#123]
* Prevent dragging from changing selection [#122]
* Clamp zoom to prevent floating point issues [#121]
* Preserve view state when switching tabs [#100]
- Fix emscripten remapping when symbols contain dashes, like `527:i32s-div` [[#129](https://github.com/jlfwong/speedscope/pull/129)]
- Improved firefox import speed and fixed bugs in it [[#128](https://github.com/jlfwong/speedscope/pull/128)]
- Prevent non-contiguous blocks in the time ordered flamechart from appearing as a single node for selection [[#123](https://github.com/jlfwong/speedscope/pull/123)]
- Prevent dragging from changing selection [[#122](https://github.com/jlfwong/speedscope/pull/122)]
- Clamp zoom to prevent floating point issues [[#121](https://github.com/jlfwong/speedscope/pull/121)]
- Preserve view state when switching tabs [[#100](https://github.com/jlfwong/speedscope/pull/100)]
## [0.4.0] - 2018-07-21
### Added
* Support for importing v8 logs from node [#98]
* Optionally read from stdin via cli [#99]
- Support for importing v8 logs from node [[#98](https://github.com/jlfwong/speedscope/pull/98)]
- Optionally read from stdin via cli [[#99](https://github.com/jlfwong/speedscope/pull/99)]
## [0.3.0] - 2018-07-18
### Added
* Support for remapping profiles using a wasm symbol file [#93]
- Support for remapping profiles using a wasm symbol file [[#93](https://github.com/jlfwong/speedscope/pull/93)]
+11
View File
@@ -36,15 +36,24 @@ speedscope is designed to ingest profiles from a variety of different profilers
- JavaScript
- [Importing from Chrome](https://github.com/jlfwong/speedscope/wiki/Importing-from-Chrome)
- [Importing from Firefox](https://github.com/jlfwong/speedscope/wiki/Importing-from-Firefox)
- [Importing from Safari](https://github.com/jlfwong/speedscope/wiki/Importing-from-Safari)
- [Importing from Node.js](https://github.com/jlfwong/speedscope/wiki/Importing-from-Node.js)
- Ruby
- [Importing from stackprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-stackprof-(ruby))
- [Importing from rbspy](https://github.com/jlfwong/speedscope/wiki/Importing-from-rbspy-(ruby))
- [Importing from ruby-prof](https://github.com/jlfwong/speedscope/wiki/Importing-from-ruby-prof)
- Python
- [Importing from py-spy](https://github.com/jlfwong/speedscope/wiki/Importing-from-py-spy-(python))
- [pyspeedscope](https://github.com/windelbouwman/pyspeedscope)
- [Importing from Austin](https://github.com/P403n1x87/austin-python#format-conversion)
- Go
- [Importing from pprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-pprof-(go))
- Rust
- [flamescope](https://github.com/coolreader18/flamescope)
- Native code
- [Importing from Instruments.app](https://github.com/jlfwong/speedscope/wiki/Importing-from-Instruments.app) (macOS)
- [Importing from `perf`](https://github.com/jlfwong/speedscope/wiki/Importing-from-perf-(linux)) (linux)
- [Importing from .NET Core](https://github.com/jlfwong/speedscope/wiki/Importing-from-.NET-Core)
- [Importing from GHC (Haskell)](https://github.com/jlfwong/speedscope/wiki/Importing-from-Haskell)
- [Importing from custom sources](https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources)
@@ -108,6 +117,8 @@ Once a profile has loaded, the main view is split into two: the top area is the
* `Cmd+O`/`Ctrl+O` to open a new profile
* `n`: Go to next profile/thread if one is available
* `p`: Go to previous profile/thread if one is available
* `t`: Open the profile/thread selector if available
* `Cmd+F`/`Ctrl+F`: to open search. While open, `Enter` and `Shift+Enter` cycle through results
## Contributing
+6 -2
View File
@@ -4,7 +4,7 @@ const fs = require('fs')
const os = require('os')
const stream = require('stream')
const opn = require('opn')
const open = require('open')
const helpString = `Usage: speedscope [filepath]
@@ -89,7 +89,11 @@ async function main() {
console.log('Opening', urlToOpen, 'in your default browser')
await opn(urlToOpen, {wait: false})
// We'd like to avoid blocking the terminal on the browsing closing,
// but for some reason this doesn't work at all on Windows if we
// don't use wait: true.
const wait = process.platform === "win32";
await open(urlToOpen, {wait})
}
main()
+7176 -8876
View File
File diff suppressed because it is too large Load Diff
+43 -20
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "1.4.1",
"version": "1.11.1",
"description": "",
"repository": "jlfwong/speedscope",
"main": "index.js",
@@ -13,48 +13,71 @@
"prettier": "prettier --write 'src/**/*.ts' 'src/**/*.tsx'",
"lint": "eslint 'src/**/*.ts' 'src/**/*.tsx'",
"jest": "./scripts/test-setup.sh && jest --runInBand",
"coverage": "npm run jest -- --coverage && coveralls < coverage/lcov.info",
"test": "tsc --noEmit && npm run lint && npm run coverage",
"coverage": "npm run jest -- --coverage",
"typecheck": "tsc --noEmit",
"test": "./scripts/ci.sh",
"serve": "parcel assets/index.html --open --no-autoinstall"
},
"files": ["bin/cli.js", "dist/release/**", "!*.map"],
"browserslist": ["last 2 Chrome versions", "last 2 Firefox versions"],
"files": [
"bin/cli.js",
"dist/release/**",
"!*.map"
],
"browserslist": [
"last 2 Chrome versions",
"last 2 Firefox versions"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/jest": "22.2.3",
"@types/jszip": "3.1.4",
"@types/node": "10.1.4",
"@types/node": "14.0.1",
"@types/pako": "1.0.0",
"@typescript-eslint/eslint-plugin": "2.33.0",
"@typescript-eslint/parser": "2.33.0",
"acorn": "7.2.0",
"aphrodite": "2.1.0",
"coveralls": "3.0.1",
"eslint": "4.19.1",
"eslint": "6.0.0",
"eslint-plugin-prettier": "2.6.0",
"jest": "23.0.1",
"eslint-plugin-react-hooks": "4.0.2",
"jest": "24.3.0",
"jsverify": "0.8.3",
"jszip": "3.1.5",
"pako": "1.0.6",
"parcel-bundler": "1.9.2",
"preact": "8.2.7",
"preact-redux": "jlfwong/preact-redux#a56dcc4",
"prettier": "1.12.0",
"parcel-bundler": "1.12.4",
"preact": "10.4.1",
"prettier": "2.0.4",
"protobufjs": "6.8.8",
"quicktype": "15.0.45",
"redux": "^4.0.0",
"ts-jest": "22.4.6",
"typescript": "2.8.1",
"redux": "^4.0.5",
"source-map": "0.6.1",
"ts-jest": "24.3.0",
"typescript": "3.9.2",
"typescript-eslint-parser": "17.0.1",
"typescript-json-schema": "0.42.0",
"uglify-es": "3.2.2"
},
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"setupFilesAfterEnv": [
"./src/jest-setup.js"
],
"testRegex": "\\.test\\.tsx?$",
"collectCoverageFrom": ["**/*.{ts,tsx}", "!**/*.d.{ts,tsx}"],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"]
"collectCoverageFrom": [
"**/*.{ts,tsx}",
"!**/*.d.{ts,tsx}"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
]
},
"dependencies": {
"opn": "5.3.0"
"open": "7.2.0"
}
}
+2 -1
View File
@@ -4,4 +4,5 @@ module.exports = {
semi: false,
singleQuote: true,
trailingComma: 'all',
};
arrowParens: 'avoid'
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+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":""}
@@ -0,0 +1,16 @@
{
"$schema": "https://www.speedscope.app/file-format-schema.json",
"shared": {
"frames": [{"name": "A"}]
},
"profiles": [
{
"type": "evented",
"name": "p1",
"unit": "none",
"startValue": 0,
"endValue": 100,
"events": [{"type": "O", "frame": 0, "at": 0}]
}
]
}
@@ -0,0 +1,25 @@
{
"$schema": "https://www.speedscope.app/file-format-schema.json",
"shared": {
"frames": [{"name": "A"}, {"name": "B"}]
},
"profiles": [
{
"type": "evented",
"name": "p1",
"unit": "none",
"startValue": 0,
"endValue": 100,
"events": [
{"type": "O", "frame": 0, "at": 0},
{"type": "C", "frame": 0, "at": 1},
{"type": "O", "frame": 1, "at": 2},
{"type": "O", "frame": 0, "at": 2},
{"type": "O", "frame": 0, "at": 3},
{"type": "C", "frame": 0, "at": 4},
{"type": "C", "frame": 1, "at": 4},
{"type": "C", "frame": 0, "at": 5}
]
}
]
}
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,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,16 @@
[
{"tid": 1, "ph": "B", "pid": 0, "name": "A", "ts": 0},
{"tid": 1, "ph": "B", "pid": 0, "name": "B", "ts": 0},
{"tid": 1, "ph": "B", "pid": 0, "name": "C", "ts": 0},
{"tid": 1, "ph": "E", "pid": 0, "name": "C", "ts": 1},
{"tid": 1, "ph": "E", "pid": 0, "name": "B", "ts": 2},
{"tid": 1, "ph": "E", "pid": 0, "name": "A", "ts": 3},
{"tid": 1, "ph": "B", "pid": 0, "name": "A", "ts": 4},
{"tid": 1, "ph": "B", "pid": 0, "name": "B", "ts": 5},
{"tid": 1, "ph": "B", "pid": 0, "name": "C", "ts": 6},
{"tid": 1, "ph": "E", "pid": 0, "name": "C", "ts": 7},
{"tid": 1, "ph": "E", "pid": 0, "name": "B", "ts": 7},
{"tid": 1, "ph": "E", "pid": 0, "name": "A", "ts": 7},
{"tid": 1, "ph": "B", "pid": 0, "name": "X", "ts": 7},
{"tid": 1, "ph": "E", "pid": 0, "name": "X", "ts": 10}
]
@@ -0,0 +1,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,7 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
{"pid": 0, "tid": 0, "ph": "X", "name": "gamma", "ts": 2, "dur": 5, "args": {"detail": "foobar"}},
{"pid": 0, "tid": 0, "ph": "X", "name": "epsilon", "ts": 7, "tdur": 4},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14},
@@ -0,0 +1,13 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
{"pid": 0, "tid": 0, "ph": "X", "name": "gamma", "ts": 2, "dur": 5, "args": {"detail": "foobar"}},
{"pid": 0, "tid": 0, "ph": "X", "name": "epsilon", "ts": 7, "tdur": 4},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14},
@@ -0,0 +1,7 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
{"pid": 0, "tid": 0, "ph": "X", "name": "gamma", "ts": 2, "dur": 5, "args": {"detail": "foobar"}},
{"pid": 0, "tid": 0, "ph": "X", "name": "epsilon", "ts": 7, "tdur": 4},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14}
@@ -0,0 +1,8 @@
[
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14},
{"pid": 0, "tid": 0, "ph": "E", "name": "gamma", "ts": 5},
{"pid": 0, "tid": 0, "ph": "E", "name": "delta", "ts": 5}
]
@@ -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'),
},
}
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
set -euxo pipefail
npm run typecheck
npm run lint
npm run coverage
node scripts/generate-file-format-schema-json.js > /dev/null
+1 -1
View File
@@ -3,7 +3,7 @@ const child_process = require('child_process')
// Convert the file-format-spec.ts file into a json schema file
let jsonSchema = child_process.execSync(
'node_modules/.bin/quicktype --lang schema ./src/lib/file-format-spec.ts',
'node_modules/.bin/typescript-json-schema ./src/lib/file-format-spec.ts --titles --required --topRef "*"',
{
encoding: 'utf8',
},
+1 -3
View File
@@ -24,9 +24,7 @@ export class CanvasContext {
const webGLInfo = this.gl.getWebGLInfo()
if (webGLInfo) {
console.log(
`WebGL initialized. renderer: ${webGLInfo.renderer}, vendor: ${
webGLInfo.vendor
}, version: ${webGLInfo.version}`,
`WebGL initialized. renderer: ${webGLInfo.renderer}, vendor: ${webGLInfo.vendor}, version: ${webGLInfo.version}`,
)
}
;(window as any)['testContextLoss'] = () => {
+4 -4
View File
@@ -175,8 +175,8 @@ export class FlamechartRenderer {
// and the blue channel to indicate the color bucket to render.
// We add one to each so we have zero reserved for the background color.
const color = new Color(
(1 + i % 255) / 256,
(1 + stackDepth % 255) / 256,
(1 + (i % 255)) / 256,
(1 + (stackDepth % 255)) / 256,
(1 + this.flamechart.getColorBucketForFrame(frame.node.frame)) / 256,
)
batch.addRect(configSpaceBounds, color)
@@ -288,10 +288,10 @@ export class FlamechartRenderer {
const configSpaceContentWidth = this.flamechart.getTotalWeight()
const numAtlasEntriesPerLayer = Math.pow(2, zoomLevel)
const left = Math.floor(
numAtlasEntriesPerLayer * configSpaceSrcRect.left() / configSpaceContentWidth,
(numAtlasEntriesPerLayer * configSpaceSrcRect.left()) / configSpaceContentWidth,
)
const right = Math.ceil(
numAtlasEntriesPerLayer * configSpaceSrcRect.right() / configSpaceContentWidth,
(numAtlasEntriesPerLayer * configSpaceSrcRect.right()) / configSpaceContentWidth,
)
const nLayers = this.flamechart.getLayers().length
+8 -2
View File
@@ -455,7 +455,8 @@ export namespace WebGL {
widthInAppUnits: number,
heightInAppUnits: number,
) {
const bounds = this._gl.canvas.getBoundingClientRect()
let canvas = this._gl.canvas as HTMLCanvasElement
const bounds = canvas.getBoundingClientRect()
if (
this._width === widthInAppUnits &&
@@ -467,7 +468,6 @@ export namespace WebGL {
return
}
let canvas = this._gl.canvas
let style = canvas.style
canvas.width = widthInPixels
canvas.height = heightInPixels
@@ -1086,11 +1086,17 @@ export namespace WebGL {
_compileShader(gl: WebGLRenderingContext, type: GLenum, source: string) {
let shader = gl.createShader(type)
if (!shader) {
throw new Error('Failed to create shader')
}
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(`${gl.getShaderInfoLog(shader)}`)
}
if (!this._program) {
throw new Error('Tried to attach shader before program was created')
}
gl.attachShader(this._program, shader)
}
+6 -1
View File
@@ -65,7 +65,12 @@ export class ViewportRectangleRenderer {
private buffer: Graphics.VertexBuffer
constructor(private gl: Graphics.Context) {
const vertices = [[-1, 1], [1, 1], [-1, -1], [1, -1]]
const vertices = [
[-1, 1],
[1, 1],
[-1, -1],
[1, -1],
]
const floats: number[] = []
for (let v of vertices) {
floats.push(v[0])
+8 -1
View File
@@ -45,7 +45,14 @@ export class RectangleBatch {
return this.buffer
}
const corners = [[0, 0], [1, 0], [0, 1], [1, 0], [0, 1], [1, 1]]
const corners = [
[0, 0],
[1, 0],
[0, 1],
[1, 0],
[0, 1],
[1, 1],
]
const bytes = new Uint8Array(vertexFormat.stride * corners.length * this.rects.length)
const floats = new Float32Array(bytes.buffer)
@@ -104,6 +104,114 @@ exports[`importFromBGFlameGraph with CRLF: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with CRLF: profileGroup.name 1`] = `"simple-crlf.txt"`;
exports[`importFromBGFlameGraph with UTF-16, Big Endian 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "a",
"line": undefined,
"name": "a",
"selfWeight": 0,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "b",
"line": undefined,
"name": "b",
"selfWeight": 5,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "c",
"line": undefined,
"name": "c",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "d",
"line": undefined,
"name": "d",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "simple-utf16-be.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
"a;b;c 3",
"a;b 5",
],
}
`;
exports[`importFromBGFlameGraph with UTF-16, Big Endian: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with UTF-16, Big Endian: profileGroup.name 1`] = `"simple-utf16-be.txt"`;
exports[`importFromBGFlameGraph with UTF-16, Little Endian 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "a",
"line": undefined,
"name": "a",
"selfWeight": 0,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "b",
"line": undefined,
"name": "b",
"selfWeight": 5,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "c",
"line": undefined,
"name": "c",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "d",
"line": undefined,
"name": "d",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "simple-utf16-le.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
"a;b;c 3",
"a;b 5",
],
}
`;
exports[`importFromBGFlameGraph with UTF-16, Little Endian: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with UTF-16, Little Endian: profileGroup.name 1`] = `"simple-utf16-le.txt"`;
exports[`importFromBGFlameGraph: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph: profileGroup.name 1`] = `"simple.txt"`;
File diff suppressed because it is too large Load Diff
+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,
File diff suppressed because it is too large Load Diff
@@ -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"`;
File diff suppressed because it is too large Load Diff
+8
View File
@@ -7,3 +7,11 @@ test('importFromBGFlameGraph', async () => {
test('importFromBGFlameGraph with CRLF', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-crlf.txt')
})
test('importFromBGFlameGraph with UTF-16, Little Endian', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-utf16-le.txt')
})
test('importFromBGFlameGraph with UTF-16, Big Endian', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-utf16-be.txt')
})
+4
View File
@@ -16,6 +16,10 @@ test('importFromV8Profiler Node 10', async () => {
await checkProfileSnapshot('./sample/profiles/node/10.11.0/example.cpuprofile')
})
test('importFromChromeTimeline Workers Chrome 66', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/66/worker.json')
})
test('importFromChromeTimeline Workers Chrome 70', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/70/worker.json')
})
+37 -23
View File
@@ -79,19 +79,17 @@ export function importFromChromeTimeline(events: TimelineEvent[], fileName: stri
// that they are.
sortBy(events, e => e.ts)
const DEFAULT_ID = '(default id)'
for (let event of events) {
if (event.name === 'CpuProfile') {
cpuProfileByID.set(event.id || DEFAULT_ID, event.args.data.cpuProfile as CPUProfile)
if (event.id) {
pidTidById.set(event.id, `${event.pid}:${event.tid}`)
}
const pidTid = `${event.pid}:${event.tid}`
const id = event.id || pidTid
cpuProfileByID.set(id, event.args.data.cpuProfile as CPUProfile)
pidTidById.set(id, pidTid)
}
if (event.name === 'Profile') {
cpuProfileByID.set(event.id || DEFAULT_ID, {
const pidTid = `${event.pid}:${event.tid}`
cpuProfileByID.set(event.id || pidTid, {
startTime: 0,
endTime: 0,
nodes: [],
@@ -110,7 +108,8 @@ export function importFromChromeTimeline(events: TimelineEvent[], fileName: stri
}
if (event.name === 'ProfileChunk') {
const cpuProfile = cpuProfileByID.get(event.id || DEFAULT_ID)
const pidTid = `${event.pid}:${event.tid}`
const cpuProfile = cpuProfileByID.get(event.id || pidTid)
if (cpuProfile) {
const chunk = event.args.data
if (chunk.cpuProfile) {
@@ -131,9 +130,7 @@ export function importFromChromeTimeline(events: TimelineEvent[], fileName: stri
cpuProfile.endTime = chunk.endTime
}
} else {
console.warn(
`Ignoring ProfileChunk for undeclared Profile with id ${event.id || DEFAULT_ID}`,
)
console.warn(`Ignoring ProfileChunk for undeclared Profile with id ${event.id || pidTid}`)
}
}
}
@@ -173,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,
@@ -227,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
@@ -235,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[]
+4
View File
@@ -5,4 +5,8 @@ test('importProfileGroup', async () => {
expect(await importProfileGroupFromText('unknown', '')).toBe(null)
expect(await importProfileGroupFromText('unknown', 'Hello world')).toBe(null)
expect(await importProfileGroupFromText('unknown', 'Hello\n\nWorld')).toBe(null)
// Importing from a version of stackprof which was missing raw_timestamp_deltas should return null
const oldStackprof = `{"version":1.2,"mode":"wall","interval":1000,"samples":0,"gc_samples":0,"missed_samples":0,"frames":{}}`
expect(await importProfileGroupFromText('unknown', oldStackprof)).toBe(null)
})
+29 -2
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'
@@ -71,6 +72,26 @@ function toGroup(profile: Profile | null): ProfileGroup | null {
return {name: profile.getName(), indexToView: 0, profiles: [profile]}
}
function fixUpJSON(content: string): string {
// This code is similar to the code from here:
// https://github.com/catapult-project/catapult/blob/27e047e0494df162022be6aa8a8862742a270232/tracing/tracing/extras/importer/trace_event_importer.html#L197-L208
//
// If the event data begins with a [, then we know it should end with a ]. The
// reason we check for this is because some tracing implementations cannot
// guarantee that a ']' gets written to the trace file. So, we are forgiving
// and if this is obviously the case, we fix it up before throwing the string
// at JSON.parse.
//
content = content.trim()
if (content[0] === '[') {
content = content.replace(/,\s*$/, '')
if (content[content.length - 1] !== ']') {
content += ']'
}
}
return content
}
async function _importProfileGroup(dataSource: ProfileDataSource): Promise<ProfileGroup | null> {
const fileName = await dataSource.name()
@@ -111,12 +132,15 @@ 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
let parsed: any
try {
parsed = JSON.parse(contents)
parsed = JSON.parse(fixUpJSON(contents))
} catch (e) {}
if (parsed) {
if (parsed['$schema'] === 'https://www.speedscope.app/file-format-schema.json') {
@@ -137,7 +161,7 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
} else if ('head' in parsed && 'samples' in parsed && 'timestamps' in parsed) {
console.log('Importing as Chrome CPU Profile (old format)')
return toGroup(importFromOldV8CPUProfile(parsed))
} else if ('mode' in parsed && 'frames' in parsed) {
} else if ('mode' in parsed && 'frames' in parsed && 'raw_timestamp_deltas' in parsed) {
console.log('Importing as stackprof profile')
return toGroup(importFromStackprof(parsed))
} else if ('code' in parsed && 'functions' in parsed && 'ticks' in parsed) {
@@ -149,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
+3
View File
@@ -98,4 +98,7 @@ describe('importFromInstrumentsTrace', () => {
test('Instruments 9.3.1', async () => {
await importFromTrace('./sample/profiles/Instruments/9.3.1/simple-time-profile.trace.zip')
})
test('Instruments 10.0', async () => {
await importFromTrace('./sample/profiles/Instruments/10.0/simple-time-profile.trace.zip')
})
})
+9 -3
View File
@@ -361,7 +361,10 @@ async function readFormTemplate(tree: TraceDirectoryTree): Promise<FormTemplateD
const archive = readInstrumentsKeyedArchive(await readAsArrayBuffer(formTemplate))
const version = archive['com.apple.xray.owner.template.version']
const selectedRunNumber = archive['com.apple.xray.owner.template'].get('_selectedRunNumber')
let selectedRunNumber = 1
if ('com.apple.xray.owner.template' in archive) {
selectedRunNumber = archive['com.apple.xray.owner.template'].get('_selectedRunNumber')
}
let instrument = archive['$1']
if ('stubInfoByUUID' in archive) {
instrument = Array.from(archive['stubInfoByUUID'].keys())[0]
@@ -630,7 +633,7 @@ export function readInstrumentsKeyedArchive(buffer: ArrayBuffer): any {
////////////////////////////////////////////////////////////////////////////////
export function decodeUTF8(bytes: Uint8Array): string {
let text = String.fromCharCode.apply(String, bytes)
let text = String.fromCharCode.apply(String, Array.from(bytes))
if (text.slice(-1) === '\0') text = text.slice(0, -1) // Remove a single trailing null character if present
return decodeURIComponent(escape(text))
}
@@ -737,7 +740,10 @@ function paternMatchObjectiveC(
// Replace NSString with a string
case 'NSString':
case 'NSMutableString':
return decodeUTF8(value['NS.bytes'])
if (value['NS.string']) return value['NS.string']
if (value['NS.bytes']) return decodeUTF8(value['NS.bytes'])
console.warn(`Unexpected ${name} format: `, value)
return null
// Replace NSArray with an Array
case 'NSArray':
+1 -1
View File
@@ -1,6 +1,6 @@
import {checkProfileSnapshot} from '../lib/test-utils'
describe('importFromLinuxPerf', async () => {
describe('importFromLinuxPerf', () => {
test('simple.linux-perf.txt', async () => {
await checkProfileSnapshot('./sample/profiles/linux-perf/simple.linux-perf.txt')
})
+5
View File
@@ -0,0 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('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()
}
+88
View File
@@ -11,3 +11,91 @@ test('importTraceEvents simple object', async () => {
test('importTraceEvents multiprocess', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/multiprocess.json')
})
test('importTraceEvents partial json import', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial.json')
})
test('importTraceEvents partial json import trailing comma', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-trailing-comma.json')
})
test('importTraceEvents partial json import whitespace padding', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-whitespace.json')
})
test('importTraceEvents bad E events', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/too-many-end-events.json')
})
test('importTraceEvents event re-ordering', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/must-retain-original-order.json')
})
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')
})
+284 -107
View File
@@ -1,5 +1,5 @@
import {sortBy, zeroPad} 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,60 +236,33 @@ function keyForEvent(event: TraceEvent): string {
return name
}
function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
const profileByPidTid = new Map<string, CallTreeProfileBuilder>()
function frameInfoForEvent(event: TraceEvent): FrameInfo {
const key = keyForEvent(event)
return {
name: key,
key: key,
}
}
function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
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
const profilePairs: [string, Profile][] = []
// We have to be careful with events that have the same timestamp
if (a.pid === b.pid && a.tid === b.tid) {
const aKey = keyForEvent(a)
const bKey = keyForEvent(b)
partitioned.forEach(eventsForThread => {
if (eventsForThread.length === 0) return
if (aKey === bKey) {
// If the two elements have the same key, we need to process the begin
// event before the end event. This will be a zero-duration event.
if (a.ph === 'B' && b.ph === 'E') return -1
if (a.ph === 'E' && b.ph === 'B') return 1
} else {
// If the two elements have *different* keys, we want to process
// the end of an event before the beginning of the event to prevent
// out-of-order push/pops from the callstack.
if (a.ph === 'B' && b.ph === 'E') return 1
if (a.ph === 'E' && b.ph === 'B') return -1
}
}
const {pid, tid} = eventsForThread[0]
return -1
})
if (durationEvents.length > 0) {
const firstTs = durationEvents[0].ts
for (let ev of durationEvents) {
ev.ts -= firstTs
}
}
function getOrCreateProfile(pid: number, tid: number) {
// We zero-pad the PID and TID to make sorting them by pid/tid pair later easier.
const pidTid = `${zeroPad('' + pid, 10)}:${zeroPad('' + tid, 10)}`
let profile = profileByPidTid.get(pidTid)
if (profile != null) return profile
profile = new CallTreeProfileBuilder()
const profile = new CallTreeProfileBuilder()
profile.setValueFormatter(new TimeFormatter('microseconds'))
profileByPidTid.set(pidTid, profile)
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})`)
@@ -208,39 +274,154 @@ function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
profile.setName(`pid ${pid}, tid ${tid}`)
}
return profile
}
// The trace event format is hard to deal with because it specifically
// allows events to be recorded out of order, *but* event ordering is still
// important for events with the same timestamp. Because of this, rather
// than thinking about the entire event stream as a single queue of events,
// we're going to first construct two time-ordered lists of events:
//
// 1. ts ordered queue of 'B' events
// 2. ts ordered queue of 'E' events
//
// We deal with 'X' events by converting them to one entry in the 'B' event
// queue and one entry in the 'E' event queue.
//
// The high level goal is to deal with 'B' events in 'ts' order, breaking
// ties by the order the events occurred in the file, and deal with 'E'
// events in 'ts' order, breaking ties in whatever order causes the 'E'
// events to match whatever is on the top of the stack.
const [bEventQueue, eEventQueue] = convertToEventQueues(eventsForThread)
for (let ev of durationEvents) {
const profile = getOrCreateProfile(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':
profile.enterFrame(frameInfo, ev.ts)
break
case 'E':
profile.leaveFrame(frameInfo, ev.ts)
break
const tryToLeaveFrame = (e: ETraceEvent) => {
const b = lastOf(frameStack)
default:
const _exhaustiveCheck: never = ev
return _exhaustiveCheck
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
}
case 'E': {
// Before we take the first event in the 'E' queue, let's first see if
// there are any e events that exactly match the top of the stack.
// We'll prioritize first by key, then by name if we can't find a key
// match.
const stackTop = lastOf(frameStack)
if (stackTop != null) {
const bFrameInfo = frameInfoForEvent(stackTop)
let swapped: boolean = false
for (let i = 1; i < eEventQueue.length; i++) {
const eEvent = eEventQueue[i]
if (eEvent.ts > eEventQueue[0].ts) {
// Only consider 'E' events with the same ts as the front of the queue.
break
}
const eFrameInfo = frameInfoForEvent(eEvent)
if (bFrameInfo.key === eFrameInfo.key) {
// We have a match! Process this one first.
const temp = eEventQueue[0]
eEventQueue[0] = eEventQueue[i]
eEventQueue[i] = temp
swapped = true
break
}
}
if (!swapped) {
// There was no key match, let's see if we can find a name match
for (let i = 1; i < eEventQueue.length; i++) {
const eEvent = eEventQueue[i]
if (eEvent.ts > eEventQueue[0].ts) {
// 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(profileByPidTid.entries())
sortBy(profilePairs, p => p[0])
return {name: '', indexToView: 0, profiles: profilePairs.map(p => p[1])}
return {
name: '',
indexToView: 0,
profiles: profilePairs.map(p => p[1]),
}
}
function isTraceEventList(maybeEventList: any): maybeEventList is TraceEvent[] {
@@ -285,13 +466,9 @@ function isTraceEventObject(
export function isTraceEventFormatted(
rawProfile: any,
): rawProfile is {traceEvents: TraceEvent[]} | TraceEvent[] {
// We're only going to suppor the JSON formatted profiles for now.
// We're only going to support the JSON formatted profiles for now.
// The spec also discusses support for data embedded in ftrace supported data: https://lwn.net/Articles/365835/.
// TODO(jlfwong): The spec also specifies that it's valid for the trace to not contain a terminating `]`.
// That complicates things a bit for us, so let's just ignore that for now until someone writes in with a
// bug report from real data.
return isTraceEventObject(rawProfile) || isTraceEventList(rawProfile)
}
+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])
}
+57
View File
@@ -0,0 +1,57 @@
// Versions of node before 10 had an unstable sort. This isn't really an issue in browsers
// that speedscope supports, but for the purposes of supporting node 10, we'll polyfill
// a stable sort to make the tests pass.
//
// See:
// - https://v8.dev/features/stable-sort
// - https://v8.dev/blog/array-sort
// - https://github.com/jlfwong/speedscope/pull/254#issuecomment-575116995
//
// Once we stop supporting node 10, this can be removed.
//
// An alternative would be to change our sort implementation to be stable by definition
// rather than relying upon native sort being stable. I don't want to do that because
// we'd take a perf hit.
//
// Because we're not going to use this in our actual build, it's okay for this
// to be inefficient.
;(function () {
const nodeVersion = process.versions.node
const versionParts = nodeVersion.split('.')
const majorVersion = parseInt(versionParts[0], 10)
if (majorVersion > 10) {
// Don't need to do the patch for newer node versions
return
}
const defaultCompareFunction = (a, b) => {
const sa = '' + a
const sb = '' + b
if (sa < sb) return -1
if (sa > sb) return 1
return 0
}
const originalSort = Array.prototype.sort
Array.prototype.sort = function (compareFunction) {
const arrayWithIndices = this.map((x, i) => [x, i])
originalSort.call(arrayWithIndices, (a, b) => {
if (!compareFunction) {
compareFunction = defaultCompareFunction
}
const res = compareFunction(a[0], b[0])
if (res !== 0) return res
return a[1] < b[1] ? -1 : 1
})
this.splice(0, this.length, ...arrayWithIndices.map(x => x[0]))
return this
}
})()
;(function () {
// TextDecoder is a global API in browsers, but an imported API in node.
//
// Let's emulate it being a global API during tests.
global.TextDecoder = require('util').TextDecoder
})()
@@ -211,3 +211,7 @@ Object {
exports[`importSpeedscopeProfiles 0.6.0 multiple profiles: indexToView 1`] = `1`;
exports[`importSpeedscopeProfiles 0.6.0 multiple profiles: profileGroup.name 1`] = `"Two Samples"`;
exports[`importSpeedscopeProfiles invalid due to incomplete trace 1`] = `"Tried to complete profile construction with a non-empty stack"`;
exports[`importSpeedscopeProfiles invalid due to out of order events 1`] = `"Tried to leave frame \\"B\\" while frame \\"A\\" was at the top at 4"`;
@@ -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()
}
}
+9 -9
View File
@@ -15,19 +15,19 @@ export class Color {
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue
const hPrime = H / 60
const X = C * (1 - Math.abs(hPrime % 2 - 1))
const X = C * (1 - Math.abs((hPrime % 2) - 1))
const [R1, G1, B1] =
hPrime < 1
? [C, X, 0]
: hPrime < 2
? [X, C, 0]
: hPrime < 3
? [0, C, X]
: hPrime < 4
? [0, X, C]
: hPrime < 5
? [X, 0, C]
: [C, 0, X]
? [X, C, 0]
: hPrime < 3
? [0, C, X]
: hPrime < 4
? [0, X, C]
: hPrime < 5
? [X, 0, C]
: [C, 0, X]
const m = L - (0.3 * R1 + 0.59 * G1 + 0.11 * B1)
+67 -41
View File
@@ -1,55 +1,81 @@
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([['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([['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(
[
/* prettier: ignore */
'a:A',
'b:B',
'c:C',
].join('\n'),
[
['a', 'A'],
['b', 'B'],
['c', 'C'],
],
)
// Valid symbol map with trailing newline
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
checkMap('u6:__ZN8tinyxml210XMLCommentD0Ev\n', [['u6', '__ZN8tinyxml210XMLCommentD0Ev']])
// WebAssembly symbol map
expect(
importEmscriptenSymbolMap(
[
/* prettier: ignore */
'0:A',
'1:B',
'2:C',
'3:D-D',
].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'],
['wasm-function[3]', 'D-D'],
]),
['wasm-function[4]', 'a b'],
['wasm-function[5]', 'a\\2'],
['wasm-function[6]', 'a\\3z'],
['wasm-function[7]', 'a b c'],
],
)
// Invalid symbol map
+24 -7
View File
@@ -1,11 +1,22 @@
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) => {
const scalar = parseInt(group, 16)
return String.fromCharCode(scalar)
})
}
// This imports symbol maps generated by emscripten using the "--emit-symbol-map" flag.
// It allows you to visualize a profile captured in a release build as long as you also
// have the associated symbol map. To do this, first drop the profile into speedscope
// 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
@@ -13,27 +24,33 @@ export function importEmscriptenSymbolMap(contents: string): EmscriptenSymbolMap
if (lines[lines.length - 1] === '') lines.pop()
if (!lines.length) return null
const map: EmscriptenSymbolMap = new Map()
const intRegex = /^(\d+):([\$\w-]+)$/
const map = new Map<string, string>()
const intRegex = /^(\d+):(.+)$/
const idRegex = /^([\$\w]+):([\$\w-]+)$/
for (const line of lines) {
// Match lines like "103:__ZN8tinyxml210XMLCommentD0Ev"
const intMatch = intRegex.exec(line)
if (intMatch) {
map.set(`wasm-function[${intMatch[1]}]`, intMatch[2])
map.set(`wasm-function[${intMatch[1]}]`, unescapeHex(intMatch[2]))
continue
}
// Match lines like "u6:__ZN8tinyxml210XMLCommentD0Ev"
const idMatch = idRegex.exec(line)
if (idMatch) {
map.set(idMatch[1], idMatch[2])
map.set(idMatch[1], unescapeHex(idMatch[2]))
continue
}
return null
}
return map
return (frame: Frame) => {
if (!map.has(frame.name)) {
return null
}
return {name: map.get(frame.name)}
}
}
+11 -2
View File
@@ -1,6 +1,6 @@
import {checkProfileSnapshot} from './test-utils'
import {checkProfileSnapshot, expectImportFailure} from './test-utils'
describe('importSpeedscopeProfiles', async () => {
describe('importSpeedscopeProfiles', () => {
test('0.0.1 evented profile', async () => {
await checkProfileSnapshot('./sample/profiles/speedscope/0.0.1/simple.speedscope.json')
})
@@ -12,4 +12,13 @@ describe('importSpeedscopeProfiles', async () => {
test('0.6.0 multiple profiles', async () => {
await checkProfileSnapshot('./sample/profiles/speedscope/0.6.0/two-sampled.speedscope.json')
})
test('invalid due to out of order events', async () => {
// See: https://github.com/jlfwong/speedscope/issues/272
await expectImportFailure('./sample/profiles/speedscope/invalid/out-of-order-events.json')
})
test('invalid due to incomplete trace', async () => {
await expectImportFailure('./sample/profiles/speedscope/invalid/incomplete-trace.json')
})
})
+4 -1
View File
@@ -138,7 +138,10 @@ function importSpeedscopeProfile(
for (let i = 0; i < samples.length; i++) {
const stack = samples[i]
const weight = weights[i]
profile.appendSampleWithWeight(stack.map(n => frameInfos[n]), weight)
profile.appendSampleWithWeight(
stack.map(n => frameInfos[n]),
weight,
)
}
return profile.build()
+22 -1
View File
@@ -1,7 +1,7 @@
import {Frame, CallTreeNode} from './profile'
import {lastOf} from './utils'
import {clamp} from './math'
import {clamp, Rect, Vec2} from './math'
export interface FlamechartFrame {
node: CallTreeNode
@@ -90,6 +90,27 @@ export class Flamechart {
return clamp(viewportWidth, minWidth, maxWidth)
}
// Given a desired config-space viewport rectangle, clamp the rectangle so
// that it fits within the given flamechart. This prevents the viewport from
// extending past the bounds of the flamechart or zooming in too far.
getClampedConfigSpaceViewportRect({
configSpaceViewportRect,
renderInverted,
}: {
configSpaceViewportRect: Rect
renderInverted?: boolean
}) {
const configSpaceSize = new Vec2(this.getTotalWeight(), this.getLayers().length)
const width = this.getClampedViewportWidth(configSpaceViewportRect.size.x)
const size = configSpaceViewportRect.size.withX(width)
const origin = Vec2.clamp(
configSpaceViewportRect.origin,
new Vec2(0, renderInverted ? 0 : -1),
Vec2.max(Vec2.zero, configSpaceSize.minus(size).plus(new Vec2(0, 1))),
)
return new Rect(origin, configSpaceViewportRect.size.withX(width))
}
constructor(private source: FlamechartDataSource) {
const stack: FlamechartFrame[] = []
const openFrame = (node: CallTreeNode, value: number) => {
+81
View File
@@ -0,0 +1,81 @@
import {fuzzyMatchStrings} from './fuzzy-find'
import {sortBy} from './utils'
function assertMatches(texts: string[], pattern: string, expectedResults: string[]) {
const results: {score: number; highlighted: string}[] = []
for (let text of texts) {
const match = fuzzyMatchStrings(text, pattern)
if (match == null) {
continue
}
let highlighted = ''
let last = 0
for (let range of match.matchedRanges) {
highlighted += `${text.slice(last, range[0])}[${text.slice(range[0], range[1])}]`
last = range[1]
}
highlighted += text.slice(last)
results.push({score: match.score, highlighted})
}
// Sort scores in descending order
sortBy(results, r => -r.score)
expect(results.map(r => r.highlighted)).toEqual(expectedResults)
}
function assertMatch(text: string, pattern: string, expected: string) {
assertMatches([text], pattern, [expected])
}
function assertNoMatch(text: string, pattern: string) {
assertMatches([text], pattern, [])
}
describe('fuzzyMatchStrings', () => {
test('no match', () => {
assertNoMatch('a', 'b')
assertNoMatch('aa', 'ab')
assertNoMatch('a', 'aa')
assertNoMatch('ca', 'ac')
})
test('full text match', () => {
assertMatch('hello', 'hello', '[hello]')
assertMatch('multiple words', 'multiple words', '[multiple words]')
})
test('case sensitivity', () => {
assertMatch('HELLO', 'hello', '[HELLO]')
assertMatch('Hello', 'hello', '[Hello]')
assertNoMatch('hello', 'Hello')
assertNoMatch('hello', 'HELLO')
})
test('multiple occurrences', () => {
assertMatch('hello hello', 'hello', '[hello] hello')
assertMatch('hellohello', 'hello', '[hello]hello')
})
test('prefer earlier matches', () => {
assertMatches(['cab', 'ab'], 'ab', ['[ab]', 'c[ab]'])
})
test('prefer shorter matches', () => {
assertMatches(['abbc', 'abc', 'abbbc'], 'ac', ['[a]b[c]', '[a]bb[c]', '[a]bbb[c]'])
})
test('prefer word boundaries', () => {
assertMatches(['abc', 'a c'], 'ac', ['[a] [c]', '[a]b[c]'])
})
test('prefer camelCase matches', () => {
assertMatches(['downtown', 'OutNode'], 'n', ['Out[N]ode', 'dow[n]town'])
})
test('prefer number prefix matches', () => {
assertMatches(['211', 'a123'], '1', ['a[1]23', '2[1]1'])
})
})
+246
View File
@@ -0,0 +1,246 @@
/**
* This file contains an implementation of fuzzy string matching.
*/
export interface FuzzyMatch {
// List of [start, end] indices in the haystack string that match the needle string
matchedRanges: [number, number][]
// The score of the match for relative ranking. Higher scores indicate
// "better" matches.
score: number
}
export function fuzzyMatchStrings(text: string, pattern: string): FuzzyMatch | null {
return fzfFuzzyMatchV1(text, pattern)
}
// The implementation here is based on FuzzyMatchV1, as described here:
// https://github.com/junegunn/fzf/blob/f81feb1e69e5cb75797d50817752ddfe4933cd68/src/algo/algo.go#L8-L15
//
// This is a hand-port to better understand what the code is doing and for added
// clarity.
//
// Capitalized letters only match capitalized letters, but lower-case letters
// match both.
//
// Note: fzf includes a normalization table for homoglyphs. I'm going to ignore that too
// https://github.com/junegunn/fzf/blob/master/src/algo/normalize.go
const charCodeLowerA = 'a'.charCodeAt(0)
const charCodeLowerZ = 'z'.charCodeAt(0)
const charCodeUpperA = 'A'.charCodeAt(0)
const charCodeUpperZ = 'Z'.charCodeAt(0)
const charCodeDigit0 = '0'.charCodeAt(0)
const charCodeDigit9 = '9'.charCodeAt(0)
enum fzfCharClass {
charNonWord,
charLower,
charUpper,
charNumber,
}
function fzfCharClassOf(char: string): fzfCharClass {
const code = char.charCodeAt(0)
if (charCodeLowerA <= code && code <= charCodeLowerZ) {
return fzfCharClass.charLower
} else if (charCodeUpperA <= code && code <= charCodeUpperZ) {
return fzfCharClass.charUpper
} else if (charCodeDigit0 <= code && code <= charCodeDigit9) {
return fzfCharClass.charNumber
}
return fzfCharClass.charNonWord
}
function charsMatch(textChar: string, patternChar: string): boolean {
if (textChar === patternChar) return true
const patternCharCode = patternChar.charCodeAt(0)
if (charCodeLowerA <= patternCharCode && patternCharCode <= charCodeLowerZ) {
return textChar.charCodeAt(0) === patternCharCode - charCodeLowerA + charCodeUpperA
}
return false
}
function fzfFuzzyMatchV1(text: string, pattern: string): FuzzyMatch | null {
if (pattern.length == 0) {
return {matchedRanges: [], score: 0}
}
// I removed the fzfAsciiFuzzyIndex code because it's not actually clear to
// me that it's a very helpful optimization.
let pidx = 0
let sidx = -1
let eidx = -1
let lenRunes = text.length
let lenPattern = pattern.length
// Forward pass: scan over the text pattern, identifying the earliest start
// and the latest end to consider.
for (let index = 0; index < lenRunes; index++) {
let char = text[index]
let pchar = pattern[pidx]
if (charsMatch(char, pchar)) {
if (sidx < 0) {
sidx = index
}
pidx++
if (pidx == lenPattern) {
// We found the last character in the pattern! eidx is exclusive, so
// we'll set it to the current index + 1.
eidx = index + 1
break
}
}
}
if (eidx == -1) {
// We couldn't find all the characters in the pattern. No match.
return null
}
// Assuming we found all the characters in the pattern, perform the backwards
// pass.
pidx--
for (let index = eidx - 1; index >= sidx; index--) {
const char = text[index]
const pchar = pattern[pidx]
if (charsMatch(char, pchar)) {
pidx--
if (pidx < 0) {
// We found the first character of the pattern, scanning
// backwards. This *may* have narrowed the match further.
// For example, for the following inputs:
//
// text = "xxx a b c abc xxx"
// pattern = "abc"
//
// For the forward pass, you get:
//
// "xxx a b c abc xxx"
// start^ ^end
//
// But after the backward pass, we can narrow this to:
//
// "xxx a b c abc xxx"
// start^ ^end
sidx = index
return fzfCalculateScore(text, pattern, sidx, eidx)
}
}
}
// This should be unreachable.
throw new Error('Implementation error. This must be a bug in fzfFuzzyMatchV1')
}
const fzfScoreMatch = 16
const fzfScoreGapStart = -3
const fzfScoreGapExtension = -1
const fzfBonusBoundary = fzfScoreMatch / 2
const fzfBonusNonWord = fzfScoreMatch / 2
const fzfBonusCamel123 = fzfBonusBoundary + fzfScoreGapExtension
const fzfBonusConsecutive = -(fzfScoreGapStart + fzfScoreGapExtension)
const fzfBonusFirstCharMultiplier = 2
function bonusFor(prevClass: fzfCharClass, curClass: fzfCharClass): number {
if (prevClass === fzfCharClass.charNonWord && curClass !== fzfCharClass.charNonWord) {
// Prefer matching at word boundaries
//
// This should prefer "a c" over "abc" for a pattern of "ac".
return fzfBonusBoundary
}
if (
(prevClass === fzfCharClass.charLower && curClass == fzfCharClass.charUpper) ||
(prevClass !== fzfCharClass.charNumber && curClass == fzfCharClass.charNumber)
) {
// Prefer matching at the transition point between lower & upper for camelCase,
// and from transition from letter to number for identifiers like letter123.
//
// This should prefer "OutNode" over "phone" for a pattern of "n",
// and "abc123" over "x211" for a pattern of "1".
return fzfBonusCamel123
}
if (curClass === fzfCharClass.charNonWord) {
return fzfBonusNonWord
}
return 0
}
function fzfCalculateScore(text: string, pattern: string, sidx: number, eidx: number): FuzzyMatch {
let pidx = 0
let score = 0
let inGap = false
let consecutive = 0
let firstBonus = 0
let pos: number[] = new Array(pattern.length)
let prevClass = fzfCharClass.charNonWord
if (sidx > 0) {
prevClass = fzfCharClassOf(text[sidx - 1])
}
for (let idx = sidx; idx < eidx; idx++) {
let char = text[idx]
let curClass = fzfCharClassOf(char)
if (charsMatch(char, pattern[pidx])) {
pos[pidx] = idx
score += fzfScoreMatch
let bonus = bonusFor(prevClass, curClass)
if (consecutive == 0) {
firstBonus = bonus
} else {
// Break consecutive chunk
if (bonus === fzfBonusBoundary) {
firstBonus = bonus
}
bonus = Math.max(bonus, firstBonus, fzfBonusConsecutive)
}
if (pidx === 0) {
score += bonus * fzfBonusFirstCharMultiplier
} else {
score += bonus
}
inGap = false
consecutive++
pidx++
} else {
if (inGap) {
// Penalize gaps (this bonus is negative)
score += fzfScoreGapExtension
} else {
// Penalize the beginning of gaps more harshly
score += fzfScoreGapStart
}
inGap = true
consecutive = 0
firstBonus = 0
}
prevClass = curClass
}
if (pidx !== pattern.length) {
throw new Error(
'fzfCalculateScore should only be called when pattern is found between sidx and eidx',
)
}
let matchedRanges: [number, number][] = [[pos[0], pos[0] + 1]]
for (let i = 1; i < pos.length; i++) {
const curPos = pos[i]
const curRange = matchedRanges[matchedRanges.length - 1]
if (curRange[1] === curPos) {
curRange[1] = curPos + 1
} else {
matchedRanges.push([curPos, curPos + 1])
}
}
return {
score,
matchedRanges,
}
}

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