Compare commits

...
20 Commits
Author SHA1 Message Date
Jamie Wong 33a8f3f313 1.4.0
Node.js CI / test (10.x) (push) Has been cancelled
Node.js CI / test (12.x) (push) Has been cancelled
Node.js CI / test (14.x) (push) Has been cancelled
Node.js CI / finish (push) Has been cancelled
2022-05-19 01:37:56 -07:00
Jamie Wong 7ae545a6c3 Improve HoverTip placement logic (#395)
This changes the HoverTip placement logic to use measurements from the actual DOM node rather than basing everything on the maximum sizes.

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

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

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

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

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

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

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

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

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

def do_test
  5.times do
    make_a_word
  end
end

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

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

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

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

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

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

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

## Before:

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

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

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

## After:

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

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

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

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

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

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

* Bump eslint deps

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

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

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

See `src/lib/atom.ts` for the library.
2021-03-28 02:44:43 -07:00
Gabriele N. Tornetta d6f5efa06c fix(search): allow paste in search box (#338) 2021-03-27 15:05:16 -07:00
Jamie Wong 03a5104317 1.13.0 2021-02-14 23:36:12 -08:00
Jamie Wong 7dcfab1bbc Update a few remaining references to amster (#333)
Fixes #332
2021-02-14 23:31:33 -08:00
Jamie Wong 24b60dfd4a Callgrind import format support (#331)
Implements import from the [callgrind format](https://www.valgrind.org/docs/manual/cl-format.html).

This comes with a big caveat that the call graph information contained with callgrind formatted files don't uniquely define a flamegraph, so the generated flamegraph is a best-effort guess. Here's the comment from the top of the main file for the callgrind importer with an examplataion:

```
// https://www.valgrind.org/docs/manual/cl-format.html
//
// Larger example files can be found by searching on github:
// https://github.com/search?q=cfn%3D&type=code
//
// Converting callgrind files into flamegraphs is challenging because callgrind
// formatted profiles contain call graphs with weighted nodes and edges, and
// such a weighted call graph does not uniquely define a flamegraph.
//
// Consider a program that looks like this:
//
//    // example.js
//    function backup(read) {
//      if (read) {
//        read()
//      } else {
//        write()
//      }
//    }
//
//    function start() {
//       backup(true)
//    }
//
//    function end() {
//       backup(false)
//    }
//
//    start()
//    end()
//
// Profiling this program might result in a profile that looks like the
// following flame graph defined in Brendan Gregg's plaintext format:
//
//    start;backup;read 4
//    end;backup;write 4
//
// When we convert this execution into a call-graph, we get the following:
//
//      +------------------+     +---------------+
//      | start (self: 0)  |     | end (self: 0) |
//      +------------------+     +---------------|
//                   \               /
//        (total: 4)  \             / (total: 4)
//                     v           v
//                 +------------------+
//                 | backup (self: 0) |
//                 +------------------+
//                    /            \
//       (total: 4)  /              \ (total: 4)
//                  v                v
//      +----------------+      +-----------------+
//      | read (self: 4) |      | write (self: 4) |
//      +----------------+      +-----------------+
//
// In the process of the conversion, we've lost information about the ratio of
// time spent in read v.s. write in the start call v.s. the end call. The
// following flame graph would yield the exact same call-graph, and therefore
// the exact sample call-grind formatted profile:
//
//    start;backup;read 3
//    start;backup;write 1
//    end;backup;read 1
//    end;backup;write 3
//
// This is unfortunate, since it means we can't produce a flamegraph that isn't
// potentially lying about the what the actual execution behavior was. To
// produce a flamegraph at all from the call graph representation, we have to
// decide how much weight each sub-call should have. Given that we know the
// total weight of each node, we'll make the incorrect assumption that every
// invocation of a function will have the average distribution of costs among
// the sub-function invocations. In the example given, this means we assume that
// every invocation of backup() is assumed to spend half its time in read() and
// half its time in write().
//
// So the flamegraph we'll produce from the given call-graph will actually be:
//
//    start;backup;read 2
//    start;backup;write 2
//    end;backup;read 2
//    end;backup;write 2
//
// A particularly bad consequence is that the resulting flamegraph will suggest
// that there was at some point a call stack that looked like
// strat;backup;write, even though that never happened in the real program
// execution.
```

Fixes #18
2021-02-14 23:14:58 -08:00
83 changed files with 19345 additions and 2136 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ name: Node.js CI
on:
push:
branches:
- master
- main
pull_request:
jobs:
+2 -1
View File
@@ -2,4 +2,5 @@ node_modules
.cache
dist
.idea
coverage
coverage
.vscode
-3
View File
@@ -1,3 +0,0 @@
{
"editor.formatOnSave": true,
}
+22
View File
@@ -1,3 +1,25 @@
## [1.14.0] - 2022-05-19
### Added
- File and line information is now displayed in hover tips [[#365](https://github.com/jlfwong/speedscope/pull/365)] (by [@sokra](https://github.com/sokra))
- Support for stackprof object mode [[#391](https://github.com/jlfwong/speedscope/pull/391)] (by [@alexcoco](https://github.com/alexcoco))
- Support for hash params to control view-mode [[#362](https://github.com/jlfwong/speedscope/pull/362)] (by [@djudd](https://github.com/djudd))
- Support for profiles over 512MB now works [[#385](https://github.com/jlfwong/speedscope/pull/385)] (by [@jlfwong](https://github.com/jlfwong))
- Support for relative URLs in profileURL hashParam [[#357](https://github.com/jlfwong/speedscope/pull/357)] (by [@danvk](https://github.com/danvk))
### Fixed
- Allow collapsed stacks with invalid lines for the Brenden Gregg stack format [[#336](https://github.com/jlfwong/speedscope/pull/336)] (by [@P403n1x87](https://github.com/P403n1x87))
- Allow pasting into the search box [[#338](https://github.com/jlfwong/speedscope/pull/338)] (by [@P403n1x87](https://github.com/P403n1x87))
- Prevent hover tips from getting unnecessarily clipped outside container bounds [[#395](https://github.com/jlfwong/speedscope/pull/395)] (by [@jlfwong](https://github.com/jlfwong))
## [1.13.0] - 2021-02-14
### Added
- Support for importing callgrind profiles [[#331](https://github.com/jlfwong/speedscope/pull/331)]
## [1.12.1] - 2020-11-12
### Fixed
+126
View File
@@ -0,0 +1,126 @@
简体中文 | [English](./README.md)
# 🔬speedscope
一个快速,交互式,基于网络的性能分析工具。 [FlameGraphs][1](火焰图)的另一个替代品。它可以轻松显示数兆的配置文件并且不会使浏览器崩溃。
给定原始分析数据,你就可以交互式的探索数据,了解应用程序中什么部分速度较慢,或者分配所有内存,或者对任何数据进行分析。
![Example Profile](https://user-images.githubusercontent.com/150329/40900669-86eced80-6781-11e8-92c1-dc667b651e72.gif)
[0]: https://en.wikipedia.org/wiki/Profiling_(computer_programming)#Statistical_profilers
[1]: https://github.com/brendangregg/FlameGraph
# 使用
访问https://www.speedscope.app,上传文件或者拖拽到页面上。配置文件不会上传到任何地方——应用程序完全在浏览器中。
## 命令行中使用
为了方便在不联网的情况下或者在终端中使用, 你可以使用npm下载speedscope:
npm install -g speedscope
调用`speedscope /path/to/profile` 就可以在默认浏览器中加载speedscope。
## 独立使用
如果你不想使用npm或者node下载,你也可以在这里下载独立的版本https://github.com/jlfwong/speedscope/releases.
下载完一个版本的压缩文件之后,解压并在谷歌或者火狐浏览器中打开`index.html`文件即可。
## 支持的文件格式
Speedscope可以不同编程语言和环境的各种不同探查器中摄取概要文件。单击下面的链接获取从特定源导入的文档。
- JavaScript
- [从 Chrome 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Chrome)
- [从 Firefox 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Firefox)
- [从 Safari 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Safari)
- [从 Node.js 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Node.js)
- Ruby
- [从 stackprof 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-stackprof-(ruby))
- [从 rbspy 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-rbspy-(ruby))
- [从 ruby-prof 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-ruby-prof)
- Python
- [从 py-spy 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-py-spy-(python))
- [pyspeedscope](https://github.com/windelbouwman/pyspeedscope)
- [从 Austin 导入](https://github.com/P403n1x87/austin-python#format-conversion)
- [从 pyinstrument 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-pyinstrument-(python))
- PHP
- [从 phpspy 或者 sj-i/php-profiler 导入](https://github.com/sj-i/php-profiler/pull/101)
- Go
- [从 pprof 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-pprof-(go))
- Rust
- [flamescope](https://github.com/coolreader18/flamescope)
- Native code
- [从 Instruments.app 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Instruments.app) (macOS)
- [从 `perf` 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-perf-(linux)) (linux)
- [从 .NET Core 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-.NET-Core)
- [从 GHC (Haskell) 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-Haskell)
- [从 custom sources 导入](https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources)
极力欢迎贡献添加对其他格式的支持!查看 issues ["import source" tag](https://github.com/jlfwong/speedscope/issues?q=is%3Aissue+is%3Aopen+label%3A%22import+source%22).
## 通过URL导入
要通过URL加载特定的配置文件,你可以添加一个这样的hash片段 `#profileURL=[URL-encoded profile URL]&title=[URL-encoded custom title]`. 注意:托管配置文件的服务器必须配置CORS以允许来自speedscope的AJAX请求。
## 页面
### 🕰Time Order
![Detail View](https://user-images.githubusercontent.com/150329/42108613-e6ef6d3a-7b8f-11e8-93d4-541b2cb93fe5.png)
在 "Time Order" 页面 (默认),调用堆栈按照它们在输入文件中出现的顺序从左到右排列,这通常是安排它们被记录的时间顺序。这个视图对于理解应用程序随时间变化的行为非常有帮助,比如 "首次从数据库获取到数据,然后为序列化准备数据,数据被序列化为JSON"。
水平轴表示每个堆栈的“权重”(最常见的是CPU时间),垂直轴显示在运行期间处于活动状态的堆栈。如果你点击其中一个框,你将能够看到关于它的统计摘要。
### ⬅️Left Heavy
![Left Heavy View](https://user-images.githubusercontent.com/150329/44534434-a05f8380-a6ac-11e8-86ac-e3e05e577c52.png)
在 "Left Heavy" 页面,将相同的堆栈分组在一,不管它们是否按顺序记录。然后,对堆栈进行排序,使每个父堆栈中最重的堆栈位于左侧——因此称为“左权重”。 这个视图对于理解在其他调用栈之间有成百上千个函数交错调用的情况下,时间都花费在了哪里很有用。
### 🥪 Sandwich
![Sandwich View](https://user-images.githubusercontent.com/150329/42108467-76a57baa-7b8f-11e8-815f-1df7b6ac3ede.png)
Sandwich 是一个表格视图,你可以在其中找到所有函数及其相关时间的列表。您可以按自己的时间或总时间排序。
之所以称为"Sandwich"视图,是因为如果你选择表中的某一行,就可以看到所选对象的所有调用者和被调用者的火焰图
## 导航
一旦配置文件被加载,主视图就会被分成两部分:顶部区域是“迷你地图”,底部区域是“堆栈视图”。
### 迷你地图导航
* 在任意一个轴上滚动以进行平移
* 单击并拖动可将视图缩小到特定范围
### 堆栈视图
* 在任意一个轴上滚动以进行平移
* 缩放
* 按住 Cmd+Scroll 进行缩放
* 双击一帧以使视口适应
* 点击一个框以查看关于它的摘要统计
### 键盘导航
* `+`: 放大
* `-`: 缩小
* `0`: 缩小以看到整体情况
* `w`/`a`/`s`/`d` 或者箭头键: pan around the profile
* `1`: 切换到 "Time Order" 视图
* `2`: 切换到 "Left Heavy" 视图
* `3`: 切换到 "Sandwich" 视图
* `r`: 在火焰图中折叠递归
* `Cmd+S`/`Ctrl+S` 保存现有文件
* `Cmd+O`/`Ctrl+O` 打开一个新文件
* `n`: 跳转到下一个文件/如果下一个文件存在
* `p`: 跳转到上一个文件/如果上一个文件存在
* `t`: 打开一个新文件/如果新文件存在
* `Cmd+F`/`Ctrl+F`: 打开搜索。打开时,按下 `Enter` and `Shift+Enter` 查看结果
## 参与贡献
你想成为 speedscope 的贡献者吗? 查看 [CONTRIBUTING.md](./CONTRIBUTING.md) 关于设置开发环境的说明。
+6 -2
View File
@@ -1,4 +1,5 @@
# 🔬speedscope
English | [简体中文](./README-zh_CN.md)
# 🔬speedscope
A fast, interactive web-based viewer for performance profiles. An alternative viewer for [FlameGraphs][1]. Will happily display multi-megabyte profiles without crashing your browser.
@@ -46,6 +47,9 @@ speedscope is designed to ingest profiles from a variety of different profilers
- [Importing from py-spy](https://github.com/jlfwong/speedscope/wiki/Importing-from-py-spy-(python))
- [pyspeedscope](https://github.com/windelbouwman/pyspeedscope)
- [Importing from Austin](https://github.com/P403n1x87/austin-python#format-conversion)
- [Importing from pyinstrument](https://github.com/jlfwong/speedscope/wiki/Importing-from-pyinstrument-(python))
- PHP
- [Importing from phpspy or sj-i/php-profiler](https://github.com/sj-i/php-profiler/pull/101)
- Go
- [Importing from pprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-pprof-(go))
- Rust
@@ -81,7 +85,7 @@ In the "Left Heavy" view, identical stacks are grouped together, regardless of w
### 🥪 Sandwich
![Sandwich View](https://user-images.githubusercontent.com/150329/42108467-76a57baa-7b8f-11e8-815f-1df7b6ac3ede.png)
The Sandwich view is a table view in which you can find a list of all functions an their associated times. You can sort by self time or total time.
The Sandwich view is a table view in which you can find a list of all functions and their associated times. You can sort by self time or total time.
It's called "Sandwich" view because if you select one of the rows in the table, you can see flamegraphs for all the callers and callees of the selected
row.
+14 -4
View File
@@ -66,10 +66,20 @@ async function main() {
const relPath = process.argv[2]
const sourceBuffer = await getProfileBuffer(relPath)
const filename = path.basename(relPath)
const sourceBase64 = sourceBuffer.toString('base64')
const jsSource = `speedscope.loadFileFromBase64(${JSON.stringify(filename)}, ${JSON.stringify(
sourceBase64,
)})`
let jsSource
try {
const sourceBase64 = sourceBuffer.toString('base64')
jsSource = `speedscope.loadFileFromBase64(${JSON.stringify(filename)}, ${JSON.stringify(
sourceBase64,
)})`
} catch(e) {
if (e && e.message && /Cannot create a string longer than/.exec(e.message)) {
jsSource = `alert("Sorry, ${filename} is too large to be loaded via command-line argument! Try dragging it into speedscope instead.")`
} else {
throw e
}
}
const filePrefix = `speedscope-${+new Date()}-${process.pid}`
const jsPath = path.join(os.tmpdir(), `${filePrefix}.js`)
+15377 -143
View File
File diff suppressed because it is too large Load Diff
+6 -7
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "1.12.1",
"version": "1.14.0",
"description": "",
"repository": "jlfwong/speedscope",
"main": "index.js",
@@ -34,8 +34,8 @@
"@types/jszip": "3.1.4",
"@types/node": "14.0.1",
"@types/pako": "1.0.0",
"@typescript-eslint/eslint-plugin": "2.33.0",
"@typescript-eslint/parser": "2.33.0",
"@typescript-eslint/eslint-plugin": "4.19.0",
"@typescript-eslint/parser": "4.19.0",
"acorn": "7.2.0",
"aphrodite": "2.1.0",
"eslint": "6.0.0",
@@ -49,13 +49,12 @@
"preact": "10.4.1",
"prettier": "2.0.4",
"protobufjs": "6.8.8",
"redux": "^4.0.5",
"source-map": "0.6.1",
"ts-jest": "24.3.0",
"typescript": "3.9.2",
"typescript-eslint-parser": "17.0.1",
"typescript": "4.2.3",
"typescript-json-schema": "0.42.0",
"uglify-es": "3.2.2"
"uglify-es": "3.2.2",
"uint8array-json-parser": "jlfwong/uint8array-json-parser#edce51ce"
},
"jest": {
"transform": {
@@ -0,0 +1,24 @@
# callgrind format
events: Instructions
fl=file1.c
fn=main
16 20
cfn=func1
calls=1 50
16 400
cfi=file2.c
cfn=func2
calls=3 20
16 400
fn=func1
51 100
cfi=file2.c
cfn=func2
calls=2 20
51 300
fl=file2.c
fn=func2
20 700
@@ -0,0 +1,29 @@
version: 1
creator: xdebug 3.0.2 (PHP 7.4.14)
cmd: /var/www/html/index.php
part: 1
positions: line
events: Time_(10ns) Memory_(bytes)
fl=(1) file1.c
fn=(1) main
16 20 15000
cfn=(2) func1
calls=1 50
16 400 20000
cfi=(2) file2.c
cfn=(3) func2
calls=3 51
16 400 3000
fn=(2)
51 100 4000
cfi=(2)
cfn=(3)
calls=2 20
51 300 5000
fl=(2)
fn=(3)
20 700 6000
@@ -0,0 +1,24 @@
# callgrind format
events: Instructions
fl=(1) file1.c
fn=(1) main
16 20
cfn=(2) func1
calls=1 50
16 400
cfi=(2) file2.c
cfn=(3) func2
calls=3 20
16 400
fn=(2)
51 100
cfi=(2)
cfn=(3)
calls=2 20
51 300
fl=(2)
fn=(3)
20 700
@@ -0,0 +1,8 @@
# comment that gives extra info about the samples
// some other comment
a;b;c 1
a;b;c 1
a;b;d 4
a;b;c 3
a;b 5
invalid line
File diff suppressed because one or more lines are too long
+25
View File
@@ -0,0 +1,25 @@
require 'json'
require 'stackprof'
def a
for i in 0..2 do
b
c
end
end
def b
Object.new
end
def c
for i in 0..5
(1..10).to_a.sample(3).sort
end
end
profile = StackProf.run(mode: :object, raw: true) do
a
end
puts JSON.generate(profile)
+6 -6
View File
@@ -4,10 +4,10 @@ This directory contains the bulk of speedscope's source code.
## Subdirectories
* `gl/`: WebGL code. This includes e.g. the code to render flamecharts.
* `import/`: Code to import profiles from varous profilers into speedscope. This include e.g. the code to import Chrome performance profiles.
* `lib/`: Mostly dependency-less utilities. This includes e.g. an LRU cache implementation, basic linear algebra classes,
- `gl/`: WebGL code. This includes e.g. the code to render flamecharts.
- `import/`: Code to import profiles from varous profilers into speedscope. This include e.g. the code to import Chrome performance profiles.
- `lib/`: Mostly dependency-less utilities. This includes e.g. an LRU cache implementation, basic linear algebra classes,
and the definition of speedscope's file format.
* `store/`: Speedscope's application state management. Implemented using [`redux`](https://redux.js.org/).
* `typings/`: [TypeScript definition files](https://basarat.gitbooks.io/typescript/docs/types/ambient/d.ts.html)
* `views/`: View code to generate the HTML & CSS used to construct the UI. Implemented using [`preact`](https://preactjs.com/) and [`aphrodite`](https://github.com/Khan/aphrodite). Also contains code mapping from the `redux` store to views using [`preact-redux`](https://github.com/developit/preact-redux)
- `app-state/`: Speedscope's application state management
- `typings/`: [TypeScript definition files](https://basarat.gitbooks.io/typescript/docs/types/ambient/d.ts.html)
- `views/`: View code to generate the HTML & CSS used to construct the UI. Implemented using [`preact`](https://preactjs.com/) and [`aphrodite`](https://github.com/Khan/aphrodite).
+34
View File
@@ -0,0 +1,34 @@
import {Profile} from '../lib/profile'
import {getProfileToView} from './getters'
import {flattenRecursionAtom, profileGroupAtom} from '.'
import {FlamechartViewState, SandwichViewState} from './profile-group'
import {useAtom} from '../lib/atom'
export interface ApplicationState {}
export interface ActiveProfileState {
profile: Profile
index: number
chronoViewState: FlamechartViewState
leftHeavyViewState: FlamechartViewState
sandwichViewState: SandwichViewState
}
export function useActiveProfileState(): ActiveProfileState | null {
const flattenRecursion = useAtom(flattenRecursionAtom)
const profileGroupState = useAtom(profileGroupAtom)
if (!profileGroupState) return null
if (profileGroupState.indexToView >= profileGroupState.profiles.length) return null
const index = profileGroupState.indexToView
const profileState = profileGroupState.profiles[index]
return {
...profileGroupState.profiles[profileGroupState.indexToView],
profile: getProfileToView({
profile: profileState.profile,
flattenRecursion,
}),
index: profileGroupState.indexToView,
}
}
+95
View File
@@ -0,0 +1,95 @@
import {Atom} from '../lib/atom'
export const enum ColorScheme {
// Default: respect prefers-color-schema
SYSTEM,
// Use dark theme
DARK,
// use light theme
LIGHT,
}
const localStorageKey = 'speedscope-color-scheme'
function getStoredPreference(): ColorScheme {
const storedPreference = window.localStorage && window.localStorage[localStorageKey]
if (storedPreference === 'DARK') {
return ColorScheme.DARK
} else if (storedPreference === 'LIGHT') {
return ColorScheme.LIGHT
} else {
return ColorScheme.SYSTEM
}
}
function matchMediaDarkColorScheme(): MediaQueryList {
return matchMedia('(prefers-color-scheme: dark)')
}
function nextColorScheme(scheme: ColorScheme): ColorScheme {
const systemPrefersDarkMode = matchMediaDarkColorScheme().matches
// We'll use a different cycling order for changing the color scheme depending
// on what the *current* system preference is. This should guarantee that when
// a user interacts with the color scheme toggle for the first time, it always
// changes the color scheme.
if (systemPrefersDarkMode) {
switch (scheme) {
case ColorScheme.SYSTEM: {
return ColorScheme.LIGHT
}
case ColorScheme.LIGHT: {
return ColorScheme.DARK
}
case ColorScheme.DARK: {
return ColorScheme.SYSTEM
}
}
} else {
switch (scheme) {
case ColorScheme.SYSTEM: {
return ColorScheme.DARK
}
case ColorScheme.DARK: {
return ColorScheme.LIGHT
}
case ColorScheme.LIGHT: {
return ColorScheme.SYSTEM
}
}
}
}
class ColorSchemeAtom extends Atom<ColorScheme> {
cycleToNextColorScheme = () => {
this.set(nextColorScheme(this.get()))
}
}
export const colorSchemeAtom = new ColorSchemeAtom(getStoredPreference(), 'colorScheme')
colorSchemeAtom.subscribe(() => {
const value = colorSchemeAtom.get()
switch (value) {
case ColorScheme.DARK: {
window.localStorage[localStorageKey] = 'DARK'
break
}
case ColorScheme.LIGHT: {
window.localStorage[localStorageKey] = 'LIGHT'
break
}
case ColorScheme.SYSTEM: {
delete window.localStorage[localStorageKey]
break
}
default: {
const _exhaustiveCheck: never = value
return _exhaustiveCheck
}
}
return value
})
+78
View File
@@ -0,0 +1,78 @@
import {Atom} from '../lib/atom'
import {ViewMode} from '../lib/view-mode'
import {getHashParams, HashParams} from '../lib/hash-params'
import {ProfileGroupAtom} from './profile-group'
// True if recursion should be flattened when viewing flamegraphs
export const flattenRecursionAtom = new Atom<boolean>(false, 'flattenRecursion')
// The query used in top-level views
//
// An empty string indicates that the search is open by no filter is applied.
// searchIsActive is stored separately, because we may choose to persist the
// query even when the search input is closed.
export const searchIsActiveAtom = new Atom<boolean>(false, 'searchIsActive')
export const searchQueryAtom = new Atom<string>('', 'searchQueryAtom')
// Which top-level view should be displayed
export const viewModeAtom = new Atom<ViewMode>(ViewMode.CHRONO_FLAME_CHART, 'viewMode')
// The top-level profile group from which most other data will be derived
export const profileGroupAtom = new ProfileGroupAtom(null, 'profileGroup')
viewModeAtom.subscribe(() => {
// If we switch views, the hover information is no longer relevant
profileGroupAtom.clearHoverNode()
})
// Parameters defined by the URL encoded k=v pairs after the # in the URL
const hashParams = getHashParams()
export const hashParamsAtom = new Atom<HashParams>(hashParams, 'hashParams')
// The <canvas> element used for WebGL
export const glCanvasAtom = new Atom<HTMLCanvasElement | null>(null, 'glCanvas')
// True when a file drag is currently active. Used to indicate that the
// application is a valid drop target.
export const dragActiveAtom = new Atom<boolean>(false, 'dragActive')
// True when the application is currently in a loading state. Used to
// display a loading progress bar.
// Speedscope is usable both from a local HTML file being served
// from a file:// URL, and via websites. In the case of file:// URLs,
// however, XHR will be unavailable to fetching files in adjacent directories.
const protocol = window.location.protocol
export const canUseXHR = protocol === 'http:' || protocol === 'https:'
const isImmediatelyLoading = canUseXHR && hashParams.profileURL != null
export const loadingAtom = new Atom<boolean>(isImmediatelyLoading, 'loading')
// True when the application is an error state, e.g. because the profile
// imported was invalid.
export const errorAtom = new Atom<boolean>(false, 'error')
export enum SortField {
SYMBOL_NAME,
SELF,
TOTAL,
}
export enum SortDirection {
ASCENDING,
DESCENDING,
}
export interface SortMethod {
field: SortField
direction: SortDirection
}
// The table sorting method using for the sandwich view, specifying the column
// to sort by, and the direction to sort that clumn.
export const tableSortMethodAtom = new Atom<SortMethod>(
{
field: SortField.SELF,
direction: SortDirection.DESCENDING,
},
'tableSortMethod',
)
+227
View File
@@ -0,0 +1,227 @@
import {Atom} from '../lib/atom'
import {clamp, Rect, Vec2} from '../lib/math'
import {CallTreeNode, Frame, Profile, ProfileGroup} from '../lib/profile'
import {objectsHaveShallowEquality} from '../lib/utils'
export interface FlamechartViewState {
hover: {
node: CallTreeNode
event: MouseEvent
} | null
selectedNode: CallTreeNode | null
logicalSpaceViewportSize: Vec2
configSpaceViewportRect: Rect
}
export interface CallerCalleeState {
selectedFrame: Frame
invertedCallerFlamegraph: FlamechartViewState
calleeFlamegraph: FlamechartViewState
}
export interface SandwichViewState {
callerCallee: CallerCalleeState | null
}
export interface ProfileState {
profile: Profile
chronoViewState: FlamechartViewState
leftHeavyViewState: FlamechartViewState
sandwichViewState: SandwichViewState
}
export type ProfileGroupState = {
name: string
// The index within the list of profiles currently being viewed
indexToView: number
profiles: ProfileState[]
} | null
export enum FlamechartID {
LEFT_HEAVY = 'LEFT_HEAVY',
CHRONO = 'CHRONO',
SANDWICH_INVERTED_CALLERS = 'SANDWICH_INVERTED_CALLERS',
SANDWICH_CALLEES = 'SANDWICH_CALLEES',
}
let initialFlameChartViewState: FlamechartViewState = {
hover: null,
selectedNode: null,
configSpaceViewportRect: Rect.empty,
logicalSpaceViewportSize: Vec2.zero,
}
export class ProfileGroupAtom extends Atom<ProfileGroupState> {
set(newState: ProfileGroupState) {
const oldState = this.state
if (oldState != null && newState != null && objectsHaveShallowEquality(oldState, newState)) {
return
}
super.set(newState)
}
getActiveProfile(): ProfileState | null {
if (this.state == null) return null
return this.state.profiles[this.state?.indexToView] || null
}
setProfileGroup = (group: ProfileGroup) => {
this.set({
name: group.name,
indexToView: group.indexToView,
profiles: group.profiles.map(p => ({
profile: p,
chronoViewState: initialFlameChartViewState,
leftHeavyViewState: initialFlameChartViewState,
sandwichViewState: {callerCallee: null},
})),
})
}
setProfileIndexToView = (indexToView: number) => {
if (this.state == null) return
indexToView = clamp(indexToView, 0, this.state.profiles.length - 1)
this.set({
...this.state,
indexToView,
})
}
private updateActiveProfileState(fn: (profileState: ProfileState) => ProfileState) {
if (this.state == null) return
const {indexToView, profiles} = this.state
this.set({
...this.state,
profiles: profiles.map((p, i) => {
if (i != indexToView) return p
return fn(p)
}),
})
}
private updateActiveSandwichViewState(
fn: (sandwichViewState: SandwichViewState) => SandwichViewState,
) {
this.updateActiveProfileState(p => ({
...p,
sandwichViewState: fn(p.sandwichViewState),
}))
}
setSelectedFrame = (frame: Frame | null) => {
if (this.state == null) return
const profile = this.getActiveProfile()
if (profile == null) {
return
}
this.updateActiveSandwichViewState(sandwichViewState => {
if (frame == null) {
return {callerCallee: null}
}
return {
callerCallee: {
invertedCallerFlamegraph: initialFlameChartViewState,
calleeFlamegraph: initialFlameChartViewState,
selectedFrame: frame,
},
}
})
}
private updateFlamechartState(
id: FlamechartID,
fn: (flamechartViewState: FlamechartViewState) => FlamechartViewState,
) {
switch (id) {
case FlamechartID.CHRONO: {
this.updateActiveProfileState(p => ({
...p,
chronoViewState: fn(p.chronoViewState),
}))
break
}
case FlamechartID.LEFT_HEAVY: {
this.updateActiveProfileState(p => ({
...p,
leftHeavyViewState: fn(p.leftHeavyViewState),
}))
break
}
case FlamechartID.SANDWICH_CALLEES: {
this.updateActiveSandwichViewState(s => ({
...s,
callerCallee:
s.callerCallee == null
? null
: {
...s.callerCallee,
calleeFlamegraph: fn(s.callerCallee.calleeFlamegraph),
},
}))
break
}
case FlamechartID.SANDWICH_INVERTED_CALLERS: {
this.updateActiveSandwichViewState(s => ({
...s,
callerCallee:
s.callerCallee == null
? null
: {
...s.callerCallee,
invertedCallerFlamegraph: fn(s.callerCallee.invertedCallerFlamegraph),
},
}))
break
}
}
}
setFlamechartHoveredNode(
id: FlamechartID,
hover: {node: CallTreeNode; event: MouseEvent} | null,
) {
this.updateFlamechartState(id, f => ({
...f,
hover,
}))
}
setSelectedNode(id: FlamechartID, selectedNode: CallTreeNode | null) {
this.updateFlamechartState(id, f => ({
...f,
selectedNode,
}))
}
setConfigSpaceViewportRect(id: FlamechartID, configSpaceViewportRect: Rect) {
this.updateFlamechartState(id, f => ({
...f,
configSpaceViewportRect,
}))
}
setLogicalSpaceViewportSize(id: FlamechartID, logicalSpaceViewportSize: Vec2) {
this.updateFlamechartState(id, f => ({
...f,
logicalSpaceViewportSize,
}))
}
clearHoverNode() {
// TODO(jlfwong): This causes 4 separate observer events. This is probably
// fine, since I hope that Preact/React are smart about batching re-renders?
this.setFlamechartHoveredNode(FlamechartID.CHRONO, null)
this.setFlamechartHoveredNode(FlamechartID.LEFT_HEAVY, null)
this.setFlamechartHoveredNode(FlamechartID.SANDWICH_CALLEES, null)
this.setFlamechartHoveredNode(FlamechartID.SANDWICH_INVERTED_CALLERS, null)
}
}
@@ -212,6 +212,60 @@ exports[`importFromBGFlameGraph with UTF-16, Little Endian: indexToView 1`] = `0
exports[`importFromBGFlameGraph with UTF-16, Little Endian: profileGroup.name 1`] = `"simple-utf16-le.txt"`;
exports[`importFromBGFlameGraph with invalid lines 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "a",
"line": undefined,
"name": "a",
"selfWeight": 0,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "b",
"line": undefined,
"name": "b",
"selfWeight": 5,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "c",
"line": undefined,
"name": "c",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "d",
"line": undefined,
"name": "d",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "simple-with-invalids.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
"a;b;c 3",
"a;b 5",
],
}
`;
exports[`importFromBGFlameGraph with invalid lines: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph with invalid lines: profileGroup.name 1`] = `"simple-with-invalids.txt"`;
exports[`importFromBGFlameGraph: indexToView 1`] = `0`;
exports[`importFromBGFlameGraph: profileGroup.name 1`] = `"simple.txt"`;
@@ -0,0 +1,177 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importFromCallgrind 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "file1.c",
"key": "file1.c:main",
"line": undefined,
"name": "main",
"selfWeight": 20,
"totalWeight": 820,
},
Frame {
"col": undefined,
"file": "file1.c",
"key": "file1.c:func1",
"line": undefined,
"name": "func1",
"selfWeight": 100,
"totalWeight": 400,
},
Frame {
"col": undefined,
"file": "file2.c",
"key": "file2.c:func2",
"line": undefined,
"name": "func2",
"selfWeight": 700,
"totalWeight": 700,
},
],
"name": "callgrind.example.log -- Instructions",
"stacks": Array [
"main;func1;func2 300",
"main;func1 100",
"main;func2 400",
"main 20",
],
}
`;
exports[`importFromCallgrind multiple event types 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "file1.c",
"key": "file1.c:main",
"line": undefined,
"name": "main",
"selfWeight": 200,
"totalWeight": 8200,
},
Frame {
"col": undefined,
"file": "file1.c",
"key": "file1.c:func1",
"line": undefined,
"name": "func1",
"selfWeight": 1000,
"totalWeight": 4000,
},
Frame {
"col": undefined,
"file": "file2.c",
"key": "file2.c:func2",
"line": undefined,
"name": "func2",
"selfWeight": 7000,
"totalWeight": 7000,
},
],
"name": "callgrind.multiple-event-types.log -- Time",
"stacks": Array [
"main;func1;func2 3.00µs",
"main;func1 1.00µs",
"main;func2 4.00µs",
"main 200.00ns",
],
}
`;
exports[`importFromCallgrind multiple event types 2`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "file1.c",
"key": "file1.c:main",
"line": undefined,
"name": "main",
"selfWeight": 15000,
"totalWeight": 38000,
},
Frame {
"col": undefined,
"file": "file1.c",
"key": "file1.c:func1",
"line": undefined,
"name": "func1",
"selfWeight": 8888.888888888889,
"totalWeight": 20000,
},
Frame {
"col": undefined,
"file": "file2.c",
"key": "file2.c:func2",
"line": undefined,
"name": "func2",
"selfWeight": 14111.111111111111,
"totalWeight": 14111.111111111111,
},
],
"name": "callgrind.multiple-event-types.log -- Memory",
"stacks": Array [
"main;func1;func2 10.85 KB",
"main;func1 8.68 KB",
"main;func2 2.93 KB",
"main 14.65 KB",
],
}
`;
exports[`importFromCallgrind multiple event types: indexToView 1`] = `0`;
exports[`importFromCallgrind multiple event types: profileGroup.name 1`] = `"callgrind.multiple-event-types.log"`;
exports[`importFromCallgrind name compression 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "file1.c",
"key": "file1.c:main",
"line": undefined,
"name": "main",
"selfWeight": 20,
"totalWeight": 820,
},
Frame {
"col": undefined,
"file": "file1.c",
"key": "file1.c:func1",
"line": undefined,
"name": "func1",
"selfWeight": 100,
"totalWeight": 400,
},
Frame {
"col": undefined,
"file": "file2.c",
"key": "file2.c:func2",
"line": undefined,
"name": "func2",
"selfWeight": 700,
"totalWeight": 700,
},
],
"name": "callgrind.name-compression.log -- Instructions",
"stacks": Array [
"main;func1;func2 300",
"main;func1 100",
"main;func2 400",
"main 20",
],
}
`;
exports[`importFromCallgrind name compression: indexToView 1`] = `0`;
exports[`importFromCallgrind name compression: profileGroup.name 1`] = `"callgrind.name-compression.log"`;
exports[`importFromCallgrind: indexToView 1`] = `0`;
exports[`importFromCallgrind: profileGroup.name 1`] = `"callgrind.example.log"`;
File diff suppressed because it is too large Load Diff
@@ -1376,6 +1376,268 @@ Object {
}
`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt chunked 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "[unknown]",
"key": "[unknown] ([unknown])",
"line": undefined,
"name": "??? ([unknown])",
"selfWeight": 0,
"totalWeight": 0.13823499999125488,
},
Frame {
"col": undefined,
"file": "/lib/x86_64-linux-gnu/libc-2.27.so",
"key": "__libc_start_main (/lib/x86_64-linux-gnu/libc-2.27.so)",
"line": undefined,
"name": "__libc_start_main",
"selfWeight": 0,
"totalWeight": 0.13823499999125488,
},
Frame {
"col": undefined,
"file": "/workdir/simple-terminates",
"key": "main (/workdir/simple-terminates)",
"line": undefined,
"name": "main",
"selfWeight": 0,
"totalWeight": 0.13823499999125488,
},
Frame {
"col": undefined,
"file": "/workdir/simple-terminates",
"key": "_Z5alphav (/workdir/simple-terminates)",
"line": undefined,
"name": "_Z5alphav",
"selfWeight": 0.04059999997843988,
"totalWeight": 0.04059999997843988,
},
Frame {
"col": undefined,
"file": "/workdir/simple-terminates",
"key": "_Z5deltav (/workdir/simple-terminates)",
"line": undefined,
"name": "_Z5deltav",
"selfWeight": 0.023244000098202378,
"totalWeight": 0.07264250004664063,
},
Frame {
"col": undefined,
"file": "/workdir/simple-terminates",
"key": "_Z4betav (/workdir/simple-terminates)",
"line": undefined,
"name": "_Z4betav",
"selfWeight": 0.05354449988226406,
"totalWeight": 0.05447799988905899,
},
Frame {
"col": undefined,
"file": "/workdir/simple-terminates",
"key": "_Z5gammav (/workdir/simple-terminates)",
"line": undefined,
"name": "_Z5gammav",
"selfWeight": 0.01991300002555363,
"totalWeight": 0.01991300002555363,
},
Frame {
"col": undefined,
"file": "[kernel.kallsyms]",
"key": "[unknown] ([kernel.kallsyms])",
"line": undefined,
"name": "??? ([kernel.kallsyms])",
"selfWeight": 0.0009335000067949295,
"totalWeight": 0.0009335000067949295,
},
],
"name": "simple-terminat tid: 9",
"stacks": Array [
"??? ([unknown]);__libc_start_main;main;_Z5alphav 501.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 996.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.03ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 989.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.07ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.04ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav;??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 933.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.46ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.47ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 997.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.04ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.06ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 965.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 2.12ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 925.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.02ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.07ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 987.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 997.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1000.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 997.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 997.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 999.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 921.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.08ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 989.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 974.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 963.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.04ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.04ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 991.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 922.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.02ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.07ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 983.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 999.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 998.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 998.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 974.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.03ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 998.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1000.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 997.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 999.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 997.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 932.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 2.09ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 986.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 998.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 954.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 991.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.05ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 989.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 998.00µs",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1000.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 981.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 921.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 2.09ms",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.01ms",
"??? ([unknown]);__libc_start_main;main;_Z4betav 999.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5gammav 995.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z5alphav 997.50µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav 1.00ms",
"??? ([unknown]);__libc_start_main;main;_Z5alphav 980.00µs",
"??? ([unknown]);__libc_start_main;main;_Z5deltav;_Z4betav 476.00µs",
],
}
`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt chunked 2`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "[kernel.kallsyms]",
"key": "[unknown] ([kernel.kallsyms])",
"line": undefined,
"name": "??? ([kernel.kallsyms])",
"selfWeight": 0.1372890000056941,
"totalWeight": 0.1372890000056941,
},
Frame {
"col": undefined,
"file": "[unknown]",
"key": "[unknown] ([unknown])",
"line": undefined,
"name": "??? ([unknown])",
"selfWeight": 0.0009340000106021762,
"totalWeight": 0.0014475000207312405,
},
],
"name": "swapper",
"stacks": Array [
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 17.00µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 9.55ms",
"??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]) 487.50µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 486.00µs",
"??? ([unknown]);??? ([unknown]) 446.50µs",
"??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 34.50µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 38.00µs",
"??? ([kernel.kallsyms]);??? ([unknown]);??? ([unknown]);??? ([unknown]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 479.00µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 6.52ms",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 517.00µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 111.12ms",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 399.50µs",
"??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]);??? ([kernel.kallsyms]) 8.13ms",
],
}
`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt chunked: indexToView 1`] = `0`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt chunked: profileGroup.name 1`] = `"system-wide.linux-perf.txt"`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt: indexToView 1`] = `0`;
exports[`importFromLinuxPerf system-wide.linux-perf.txt: profileGroup.name 1`] = `"system-wide.linux-perf.txt"`;
+2 -2
View File
@@ -17,7 +17,7 @@ Object {
"file": "__InjectedScript_CommandLineAPIModuleSource.js",
"key": ":__InjectedScript_CommandLineAPIModuleSource.js:2:10",
"line": 2,
"name": "(anonymous)",
"name": "(anonymous __InjectedScript_CommandLineAPIModuleSource.js:2)",
"selfWeight": 0.001,
"totalWeight": 0.001,
},
@@ -78,7 +78,7 @@ Object {
],
"name": "Grabación de Control temporal 1",
"stacks": Array [
"injectModule;(anonymous) 1.00ms",
"injectModule;(anonymous __InjectedScript_CommandLineAPIModuleSource.js:2) 1.00ms",
" 39.93ms",
"(program);alpha;delta;gamma 10.83ms",
" 2.46ms",
@@ -851,6 +851,201 @@ Object {
}
`;
exports[`importFromStackprof object mode 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": "../../alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4317728280,
"line": undefined,
"name": "<main>",
"selfWeight": 0,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4376547240,
"line": undefined,
"name": "<main>",
"selfWeight": 0,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4379035920,
"line": null,
"name": "StackProf.run",
"selfWeight": 0,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4317464320,
"line": 21,
"name": "block in <main>",
"selfWeight": 1,
"totalWeight": 103,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4379033920,
"line": 4,
"name": "Object#a",
"selfWeight": 2,
"totalWeight": 102,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4317869400,
"line": null,
"name": "Range#each",
"selfWeight": 0,
"totalWeight": 102,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4379033880,
"line": 11,
"name": "Object#b",
"selfWeight": 1,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4318054440,
"line": null,
"name": "Class#new",
"selfWeight": 4,
"totalWeight": 4,
},
Frame {
"col": undefined,
"file": "/Users/alex/src/github.com/alexcoco/speedscope/sample/programs/ruby/object.rb",
"key": 4379033840,
"line": 15,
"name": "Object#c",
"selfWeight": 2,
"totalWeight": 95,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4317868920,
"line": null,
"name": "Range#to_a",
"selfWeight": 1,
"totalWeight": 37,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4379016640,
"line": null,
"name": "Enumerable#to_a",
"selfWeight": 36,
"totalWeight": 36,
},
Frame {
"col": undefined,
"file": "<internal:array>",
"key": 4317563560,
"line": 60,
"name": "Array#sample",
"selfWeight": 20,
"totalWeight": 20,
},
Frame {
"col": undefined,
"file": "<cfunc>",
"key": 4317922760,
"line": null,
"name": "Array#sort",
"selfWeight": 36,
"totalWeight": 36,
},
],
"name": "object-stackprof.json",
"stacks": Array [
"<main>;<main>;StackProf.run;block in <main> 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b;Class#new 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 3",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b;Class#new 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#b;Class#new 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Range#to_a;Enumerable#to_a 2",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sample 1",
"<main>;<main>;StackProf.run;block in <main>;Object#a;Range#each;Object#a;Object#c;Range#each;Object#c;Array#sort 2",
],
}
`;
exports[`importFromStackprof object mode: indexToView 1`] = `0`;
exports[`importFromStackprof object mode: profileGroup.name 1`] = `"object-stackprof.json"`;
exports[`importFromStackprof: indexToView 1`] = `0`;
exports[`importFromStackprof: profileGroup.name 1`] = `"simple-stackprof.json"`;
@@ -842,6 +842,62 @@ Object {
}
`;
exports[`importTraceEvents partial json import chunked 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 3,
"totalWeight": 12,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma {\\"detail\\":\\"foobar\\"}",
"line": undefined,
"name": "gamma {\\"detail\\":\\"foobar\\"}",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "epsilon",
"line": undefined,
"name": "epsilon",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma {\\"detail\\":\\"foobar\\"} 5.00µs",
"alpha;beta;epsilon 4.00µs",
"alpha;beta 2.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents partial json import chunked: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import chunked: profileGroup.name 1`] = `"simple-partial.json"`;
exports[`importTraceEvents partial json import trailing comma 1`] = `
Object {
"frames": Array [
@@ -894,6 +950,62 @@ Object {
}
`;
exports[`importTraceEvents partial json import trailing comma chunked 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 3,
"totalWeight": 12,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma {\\"detail\\":\\"foobar\\"}",
"line": undefined,
"name": "gamma {\\"detail\\":\\"foobar\\"}",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "epsilon",
"line": undefined,
"name": "epsilon",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma {\\"detail\\":\\"foobar\\"} 5.00µs",
"alpha;beta;epsilon 4.00µs",
"alpha;beta 2.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents partial json import trailing comma chunked: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import trailing comma chunked: profileGroup.name 1`] = `"simple-partial-trailing-comma.json"`;
exports[`importTraceEvents partial json import trailing comma: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import trailing comma: profileGroup.name 1`] = `"simple-partial-trailing-comma.json"`;
@@ -950,6 +1062,62 @@ Object {
}
`;
exports[`importTraceEvents partial json import whitespace padding chunked 1`] = `
Object {
"frames": Array [
Frame {
"col": undefined,
"file": undefined,
"key": "alpha",
"line": undefined,
"name": "alpha",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": undefined,
"file": undefined,
"key": "beta",
"line": undefined,
"name": "beta",
"selfWeight": 3,
"totalWeight": 12,
},
Frame {
"col": undefined,
"file": undefined,
"key": "gamma {\\"detail\\":\\"foobar\\"}",
"line": undefined,
"name": "gamma {\\"detail\\":\\"foobar\\"}",
"selfWeight": 5,
"totalWeight": 5,
},
Frame {
"col": undefined,
"file": undefined,
"key": "epsilon",
"line": undefined,
"name": "epsilon",
"selfWeight": 4,
"totalWeight": 4,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"alpha 1.00µs",
"alpha;beta 1.00µs",
"alpha;beta;gamma {\\"detail\\":\\"foobar\\"} 5.00µs",
"alpha;beta;epsilon 4.00µs",
"alpha;beta 2.00µs",
"alpha 1.00µs",
],
}
`;
exports[`importTraceEvents partial json import whitespace padding chunked: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import whitespace padding chunked: profileGroup.name 1`] = `"simple-partial-whitespace.json"`;
exports[`importTraceEvents partial json import whitespace padding: indexToView 1`] = `0`;
exports[`importTraceEvents partial json import whitespace padding: profileGroup.name 1`] = `"simple-partial-whitespace.json"`;
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -8,7 +8,7 @@ Object {
"file": "bootstrap_node.js",
"key": " bootstrap_node.js:10:10",
"line": 10,
"name": "(anonymous)",
"name": "(anonymous bootstrap_node.js:10)",
"selfWeight": 0,
"totalWeight": 116424,
},
@@ -53,7 +53,7 @@ Object {
"file": "util.js",
"key": " util.js:1:11",
"line": 1,
"name": "(anonymous)",
"name": "(anonymous util.js:1)",
"selfWeight": 0,
"totalWeight": 29385,
},
@@ -62,7 +62,7 @@ Object {
"file": "internal/encoding.js",
"key": " internal/encoding.js:1:11",
"line": 1,
"name": "(anonymous)",
"name": "(anonymous encoding.js:1)",
"selfWeight": 0,
"totalWeight": 29385,
},
@@ -107,7 +107,7 @@ Object {
"file": "module.js",
"key": " module.js:1:11",
"line": 1,
"name": "(anonymous)",
"name": "(anonymous module.js:1)",
"selfWeight": 0,
"totalWeight": 22888,
},
@@ -116,7 +116,7 @@ Object {
"file": "fs.js",
"key": " fs.js:1:11",
"line": 1,
"name": "(anonymous)",
"name": "(anonymous fs.js:1)",
"selfWeight": 0,
"totalWeight": 22888,
},
@@ -197,7 +197,7 @@ Object {
"file": "/Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": " /Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:1:11",
"line": 1,
"name": "(anonymous)",
"name": "(anonymous simple.js:1)",
"selfWeight": 0,
"totalWeight": 64151,
},
@@ -240,22 +240,22 @@ Object {
],
"name": "simple.v8log.json",
"stacks": Array [
"(anonymous);startup;setupGlobalVariables;NativeModule.require;NativeModule.compile;(anonymous);NativeModule.require;NativeModule.compile;(anonymous);(c++) v8::internal::Runtime_CreateArrayLiteral;(c++) v8::internal::JSFunction::EnsureHasInitialMap 29.38ms",
"(anonymous);startup;setupGlobalConsole;setupInspectorCommandLineAPI;NativeModule.require;NativeModule.compile;(anonymous);NativeModule.require;NativeModule.compile;(anonymous);(c++) v8::internal::Runtime_StoreIC_Miss;(c++) v8::internal::Map::RawCopy 22.89ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 37.52ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.28ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 2.55ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.27ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 1.28ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.27ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 1.29ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.26ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 1.31ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 2.52ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c 1.27ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b 3.62ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c 1.28ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b 6.42ms",
"(anonymous bootstrap_node.js:10);startup;setupGlobalVariables;NativeModule.require;NativeModule.compile;(anonymous util.js:1);NativeModule.require;NativeModule.compile;(anonymous encoding.js:1);(c++) v8::internal::Runtime_CreateArrayLiteral;(c++) v8::internal::JSFunction::EnsureHasInitialMap 29.38ms",
"(anonymous bootstrap_node.js:10);startup;setupGlobalConsole;setupInspectorCommandLineAPI;NativeModule.require;NativeModule.compile;(anonymous module.js:1);NativeModule.require;NativeModule.compile;(anonymous fs.js:1);(c++) v8::internal::Runtime_StoreIC_Miss;(c++) v8::internal::Map::RawCopy 22.89ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b;d 37.52ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c;d 1.28ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b;d 2.55ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c;d 1.27ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b;d 1.28ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c;d 1.27ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b;d 1.29ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c;d 1.26ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b;d 1.31ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c;d 2.52ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c 1.27ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b 3.62ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;c 1.28ms",
"(anonymous bootstrap_node.js:10);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous simple.js:1);a;b 6.42ms",
],
}
`;
+4
View File
@@ -15,3 +15,7 @@ test('importFromBGFlameGraph with UTF-16, Little Endian', async () => {
test('importFromBGFlameGraph with UTF-16, Big Endian', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-utf16-be.txt')
})
test('importFromBGFlameGraph with invalid lines', async () => {
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-with-invalids.txt')
})
+11 -5
View File
@@ -1,25 +1,31 @@
// https://github.com/brendangregg/FlameGraph#2-fold-stacks
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
import {TextFileContent} from './utils'
interface BGSample {
stack: FrameInfo[]
duration: number
}
function parseBGFoldedStacks(contents: string): BGSample[] {
function parseBGFoldedStacks(contents: TextFileContent): BGSample[] {
const samples: BGSample[] = []
contents.replace(/^(.*) (\d+)$/gm, (match: string, stack: string, n: string) => {
for (const line of contents.splitLines()) {
const match = /^(.*) (\d+)$/gm.exec(line)
if (!match) continue
const stack = match[1]
const n = match[2]
samples.push({
stack: stack.split(';').map(name => ({key: name, name: name})),
duration: parseInt(n, 10),
})
return match
})
}
return samples
}
export function importFromBGFlameGraph(contents: string): Profile | null {
export function importFromBGFlameGraph(contents: TextFileContent): Profile | null {
const parsed = parseBGFoldedStacks(contents)
const duration = parsed.reduce((prev: number, cur: BGSample) => prev + cur.duration, 0)
const profile = new StackListProfileBuilder(duration)
+13
View File
@@ -0,0 +1,13 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importFromCallgrind', async () => {
await checkProfileSnapshot('./sample/profiles/callgrind/callgrind.example.log')
})
test('importFromCallgrind name compression', async () => {
await checkProfileSnapshot('./sample/profiles/callgrind/callgrind.name-compression.log')
})
test('importFromCallgrind multiple event types', async () => {
await checkProfileSnapshot('./sample/profiles/callgrind/callgrind.multiple-event-types.log')
})
+518
View File
@@ -0,0 +1,518 @@
// https://www.valgrind.org/docs/manual/cl-format.html
//
// Larger example files can be found by searching on github:
// https://github.com/search?q=cfn%3D&type=code
//
// Converting callgrind files into flamegraphs is challenging because callgrind
// formatted profiles contain call graphs with weighted nodes and edges, and
// such a weighted call graph does not uniquely define a flamegraph.
//
// Consider a program that looks like this:
//
// // example.js
// function backup(read) {
// if (read) {
// read()
// } else {
// write()
// }
// }
//
// function start() {
// backup(true)
// }
//
// function end() {
// backup(false)
// }
//
// start()
// end()
//
// Profiling this program might result in a profile that looks like the
// following flame graph defined in Brendan Gregg's plaintext format:
//
// start;backup;read 4
// end;backup;write 4
//
// When we convert this execution into a call-graph, we get the following:
//
// +------------------+ +---------------+
// | start (self: 0) | | end (self: 0) |
// +------------------+ +---------------|
// \ /
// (total: 4) \ / (total: 4)
// v v
// +------------------+
// | backup (self: 0) |
// +------------------+
// / \
// (total: 4) / \ (total: 4)
// v v
// +----------------+ +-----------------+
// | read (self: 4) | | write (self: 4) |
// +----------------+ +-----------------+
//
// In the process of the conversion, we've lost information about the ratio of
// time spent in read v.s. write in the start call v.s. the end call. The
// following flame graph would yield the exact same call-graph, and therefore
// the exact sample call-grind formatted profile:
//
// start;backup;read 3
// start;backup;write 1
// end;backup;read 1
// end;backup;write 3
//
// This is unfortunate, since it means we can't produce a flamegraph that isn't
// potentially lying about the what the actual execution behavior was. To
// produce a flamegraph at all from the call graph representation, we have to
// decide how much weight each sub-call should have. Given that we know the
// total weight of each node, we'll make the incorrect assumption that every
// invocation of a function will have the average distribution of costs among
// the sub-function invocations. In the example given, this means we assume that
// every invocation of backup() is assumed to spend half its time in read() and
// half its time in write().
//
// So the flamegraph we'll produce from the given call-graph will actually be:
//
// start;backup;read 2
// start;backup;write 2
// end;backup;read 2
// end;backup;write 2
//
// A particularly bad consequence is that the resulting flamegraph will suggest
// that there was at some point a call stack that looked like
// strat;backup;write, even though that never happened in the real program
// execution.
import {CallTreeProfileBuilder, Frame, FrameInfo, Profile, ProfileGroup} from '../lib/profile'
import {getOrElse, getOrInsert, KeyedSet} from '../lib/utils'
import {ByteFormatter, TimeFormatter} from '../lib/value-formatters'
import {TextFileContent} from './utils'
class CallGraph {
private frameSet = new KeyedSet<Frame>()
private totalWeights = new Map<Frame, number>()
private childrenTotalWeights = new Map<Frame, Map<Frame, number>>()
constructor(private fileName: string, private fieldName: string) {}
private getOrInsertFrame(info: FrameInfo): Frame {
return Frame.getOrInsert(this.frameSet, info)
}
private addToTotalWeight(frame: Frame, weight: number) {
if (!this.totalWeights.has(frame)) {
this.totalWeights.set(frame, weight)
} else {
this.totalWeights.set(frame, this.totalWeights.get(frame)! + weight)
}
}
addSelfWeight(frameInfo: FrameInfo, weight: number) {
this.addToTotalWeight(this.getOrInsertFrame(frameInfo), weight)
}
addChildWithTotalWeight(parentInfo: FrameInfo, childInfo: FrameInfo, weight: number) {
const parent = this.getOrInsertFrame(parentInfo)
const child = this.getOrInsertFrame(childInfo)
const childMap = getOrInsert(this.childrenTotalWeights, parent, k => new Map())
if (!childMap.has(child)) {
childMap.set(child, weight)
} else {
childMap.set(child, childMap.get(child) + weight)
}
this.addToTotalWeight(parent, weight)
}
toProfile(): Profile {
// To convert a call graph into a profile, we first need to identify what
// the "root weights" are. "root weights" are the total weight of each frame
// while at the bottom of the call-stack. The majority of functions will have
// zero weight while at the bottom of the call-stack, since most functions
// are never at the bottom of the call-stack.
const rootWeights = new Map<Frame, number>()
for (let [frame, totalWeight] of this.totalWeights) {
rootWeights.set(frame, totalWeight)
}
for (let [_, childMap] of this.childrenTotalWeights) {
for (let [child, weight] of childMap) {
rootWeights.set(child, getOrElse(rootWeights, child, () => weight) - weight)
}
}
let totalProfileWeight = 0
for (let [_, rootWeight] of rootWeights) {
totalProfileWeight += rootWeight
}
const profile = new CallTreeProfileBuilder()
let unitMultiplier = 1
// These are common field names used by Xdebug. Let's give them special
// treatment to more helpfully display units.
if (this.fieldName === 'Time_(10ns)') {
profile.setName(`${this.fileName} -- Time`)
unitMultiplier = 10
profile.setValueFormatter(new TimeFormatter('nanoseconds'))
} else if (this.fieldName == 'Memory_(bytes)') {
profile.setName(`${this.fileName} -- Memory`)
profile.setValueFormatter(new ByteFormatter())
} else {
profile.setName(`${this.fileName} -- ${this.fieldName}`)
}
let totalCumulative = 0
const currentStack = new Set<Frame>()
const visit = (frame: Frame, callTreeWeight: number) => {
if (currentStack.has(frame)) {
// Call-graphs are allowed to have cycles. Call-trees are not. In case
// we run into a cycle, we'll just avoid recursing into the same subtree
// more than once in a call stack. The result will be that the time
// spent in the recursive call will instead be attributed as self time
// in the parent.
return
}
// We need to calculate how much weight to give to a particular node in
// the call-tree based on information from the call-graph. A given node
// from the call-graph might correspond to several nodes in the call-tree,
// so we need to decide how to distribute the weight of the call-graph
// node to the various call-tree nodes.
//
// We assume that the weighting is evenly distributed. If a call-tree node
// X occurs with weights x1 and x2, and we know from the call-graph that
// child Y of X has a total weight y, then we assume the child Y of X has
// weight y*x1/(x1 + x2) for the first occurrence, and y*x2(y1 + x2) for
// the second occurrence.
//
// This assumption is incorrectly (sometimes wildly so), but we need to
// make *some* assumption, and this seems to me the sanest option.
//
// See the comment at the top of the file for an example where this
// assumption can yield especially misleading results.
if (callTreeWeight < 1e-4 * totalProfileWeight) {
// This assumption about even distribution can cause us to generate a
// call tree with dramatically more nodes than the call graph.
//
// Consider a function which is called 1000 times, where the result is
// cached. The first invocation has a complex call tree and may take
// 100ms. Let's say that this complex call tree has 250 nodes.
//
// Subsequent calls use the cached result, so take only 1ms, and have no
// children in their call trees. So we have, in total, (1 + 250) + 999
// nodes in the call-tree for a total of 1250 nodes.
//
// The information specific to each invocation is, however, lost in the
// call-graph representation.
//
// Because of the even distribution assumption we make, this means that
// the call-trees of each invocation will have the same shape. Each 1ms
// call-tree will look identical to the 100ms call-tree, just
// horizontally compacted. So instead of 1251 nodes, we have
// 1000*250=250,000 nodes in the resulting call graph.
//
// To mitigate this explosion of the # of nodes, we ignore subtrees
// whose weights are less than 0.01% of the total weight of the profile.
return
}
// totalWeightForFrame is the total weight for the given frame in the
// entire call graph.
const callGraphWeightForFrame = getOrElse(this.totalWeights, frame, () => 0)
if (callGraphWeightForFrame === 0) {
return
}
// This is the portion of the total time the given child spends within the
// given parent that we'll attribute to this specific path in the call
// tree.
const ratio = callTreeWeight / callGraphWeightForFrame
let selfWeightForFrame = callGraphWeightForFrame
profile.enterFrame(frame, totalCumulative * unitMultiplier)
currentStack.add(frame)
for (let [child, callGraphEdgeWeight] of this.childrenTotalWeights.get(frame) || []) {
selfWeightForFrame -= callGraphEdgeWeight
const childCallTreeWeight = callGraphEdgeWeight * ratio
visit(child, childCallTreeWeight)
}
currentStack.delete(frame)
totalCumulative += selfWeightForFrame * ratio
profile.leaveFrame(frame, totalCumulative * unitMultiplier)
}
for (let [rootFrame, rootWeight] of rootWeights) {
if (rootWeight <= 0) {
continue
}
// If we've reached here, it means that the given root frame has some
// weight while at the top of the call-stack.
visit(rootFrame, rootWeight)
}
return profile.build()
}
}
// In writing this, I initially tried to use the formal grammar described in
// section 3.2 of https://www.valgrind.org/docs/manual/cl-format.html, but
// stopped because most of the information isn't relevant for visualization, and
// because there's inconsistency between the grammar and subsequence
// descriptions.
//
// For example, the grammar for headers specifies all the valid header names,
// but then the writing below that mentions there may be a "totals" or "summary"
// header, which should be disallowed by the formal grammar.
//
// So, instead, I'm not going to bother with a formal parse. Since there are no
// real recursive structures in this file format, that should be okay.
class CallgrindParser {
private lines: string[]
private lineNum: number
private callGraphs: CallGraph[] | null = null
private eventsLine: string | null = null
private filename: string | null = null
private functionName: string | null = null
private calleeFilename: string | null = null
private calleeFunctionName: string | null = null
private savedFileNames: {[id: string]: string} = {}
private savedFunctionNames: {[id: string]: string} = {}
constructor(contents: TextFileContent, private importedFileName: string) {
this.lines = contents.splitLines()
this.lineNum = 0
}
parse(): ProfileGroup | null {
while (this.lineNum < this.lines.length) {
const line = this.lines[this.lineNum++]
if (/^\s*#/.exec(line)) {
// Line is a comment. Ignore it.
continue
}
if (/^\s*$/.exec(line)) {
// Line is empty. Ignore it.
continue
}
if (this.parseHeaderLine(line)) {
continue
}
if (this.parseAssignmentLine(line)) {
continue
}
if (this.parseCostLine(line, 'self')) {
continue
}
throw new Error(`Unrecognized line "${line}" on line ${this.lineNum}`)
}
if (!this.callGraphs) {
return null
}
return {
name: this.importedFileName,
indexToView: 0,
profiles: this.callGraphs.map(cg => cg.toProfile()),
}
}
private frameInfo(): FrameInfo {
const file = this.filename || '(unknown)'
const name = this.functionName || '(unknown)'
const key = `${file}:${name}`
return {key, name, file}
}
private calleeFrameInfo(): FrameInfo {
const file = this.calleeFilename || '(unknown)'
const name = this.calleeFunctionName || '(unknown)'
const key = `${file}:${name}`
return {key, name, file}
}
private parseHeaderLine(line: string): boolean {
const headerMatch = /^\s*(\w+):\s*(.*)+$/.exec(line)
if (!headerMatch) return false
if (headerMatch[1] !== 'events') {
// We don't care about other headers. Ignore this line.
return true
}
// Line specifies the formatting of subsequent cost lines.
const fields = headerMatch[2].split(' ')
if (this.callGraphs != null) {
throw new Error(
`Duplicate "events: " lines specified. First was "${this.eventsLine}", now received "${line}" on ${this.lineNum}.`,
)
}
this.callGraphs = fields.map(fieldName => {
return new CallGraph(this.importedFileName, fieldName)
})
return true
}
private parseAssignmentLine(line: string): boolean {
const assignmentMatch = /^(\w+)=\s*(.*)$/.exec(line)
if (!assignmentMatch) return false
const key = assignmentMatch[1]
const value = assignmentMatch[2]
switch (key) {
case 'fe':
case 'fi':
case 'fl': {
this.filename = this.parseNameWithCompression(value, this.savedFileNames)
this.calleeFilename = this.filename
break
}
case 'fn': {
this.functionName = this.parseNameWithCompression(value, this.savedFunctionNames)
break
}
case 'cfi':
case 'cfl': {
this.calleeFilename = this.parseNameWithCompression(value, this.savedFileNames)
break
}
case 'cfn': {
this.calleeFunctionName = this.parseNameWithCompression(value, this.savedFunctionNames)
break
}
case 'calls': {
// TODO(jlfwong): This is currently ignoring the number of calls being
// made. Accounting for the number of calls might be unhelpful anyway,
// since it'll just be copying the exact same frame over-and-over again,
// but that might be better than ignoring it.
this.parseCostLine(this.lines[this.lineNum++], 'child')
break
}
default: {
console.log(`Ignoring assignment to unrecognized key "${line}" on line ${this.lineNum}`)
}
}
return true
}
private parseNameWithCompression(name: string, saved: {[id: string]: string}): string {
{
const nameDefinitionMatch = /^\((\d+)\)\s*(.+)$/.exec(name)
if (nameDefinitionMatch) {
const id = nameDefinitionMatch[1]
const name = nameDefinitionMatch[2]
if (id in saved) {
throw new Error(
`Redefinition of name with id: ${id}. Original value was "${saved[id]}". Tried to redefine as "${name}" on line ${this.lineNum}.`,
)
}
saved[id] = name
return name
}
}
{
const nameUseMatch = /^\((\d+)\)$/.exec(name)
if (nameUseMatch) {
const id = nameUseMatch[1]
if (!(id in saved)) {
throw new Error(
`Tried to use name with id ${id} on line ${this.lineNum} before it was defined.`,
)
}
return saved[id]
}
}
return name
}
private parseCostLine(line: string, costType: 'self' | 'child'): boolean {
// TODO(jlfwong): Handle "Subposition compression"
// TODO(jlfwong): Allow hexadecimal encoding
const parts = line.split(/\s+/)
const nums: number[] = []
for (let part of parts) {
// As far as I can tell from the specification, the callgrind format does
// not accept floating point numbers.
const asNum = parseInt(part)
if (isNaN(asNum)) {
return false
}
nums.push(asNum)
}
if (nums.length == 0) {
return false
}
// TODO(jlfwong): Handle custom positions format w/ multiple parts
const numPositionFields = 1
// NOTE: We intentionally do not include the line number here because
// callgrind uses the line number of the function invocation, not the
// line number of the function definition, which conflicts with how
// speedscope uses line numbers.
//
// const lineNum = nums[0]
if (!this.callGraphs) {
throw new Error(
`Encountered a cost line on line ${this.lineNum} before event specification was provided.`,
)
}
for (let i = 0; i < this.callGraphs.length; i++) {
if (costType === 'self') {
this.callGraphs[i].addSelfWeight(this.frameInfo(), nums[numPositionFields + i])
} else if (costType === 'child') {
this.callGraphs[i].addChildWithTotalWeight(
this.frameInfo(),
this.calleeFrameInfo(),
nums[numPositionFields + i] || 0,
)
}
}
return true
}
}
export function importFromCallgrind(
contents: TextFileContent,
importedFileName: string,
): ProfileGroup | null {
return new CallgrindParser(contents, importedFileName).parse()
}
+7
View File
@@ -1,4 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
import {withMockedFileChunkSizeForTests} from './utils'
test('importFromChromeCPUProfile', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/65/simple.cpuprofile')
@@ -8,6 +9,12 @@ test('importFromChromeTimeline', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/65/simple-timeline.json')
})
test('importFromChromeTimeline chunked', async () => {
await withMockedFileChunkSizeForTests(100, async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/65/simple-timeline.json')
})
})
test('importFromChromeTimeline Chrome 69', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/69/simple.json')
})
+4 -1
View File
@@ -168,7 +168,6 @@ export function importFromChromeTimeline(events: TimelineEvent[], fileName: stri
const callFrameToFrameInfo = new Map<CPUProfileCallFrame, FrameInfo>()
function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
return getOrInsert(callFrameToFrameInfo, callFrame, callFrame => {
const name = callFrame.functionName || '(anonymous)'
const file = callFrame.url
// In Chrome profiles, line numbers & column numbers are both 0-indexed.
@@ -181,6 +180,10 @@ function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
let col = callFrame.columnNumber
if (col != null) col++
const name =
callFrame.functionName ||
(file ? `(anonymous ${file.split('/').pop()}:${line})` : '(anonymous)')
return {
key: `${name}:${file}:${line}:${col}`,
name,
+33 -41
View File
@@ -21,6 +21,7 @@ import {importAsPprofProfile} from './pprof'
import {decodeBase64} from '../lib/utils'
import {importFromChromeHeapProfile} from './v8heapalloc'
import {isTraceEventFormatted, importTraceEvents} from './trace-event'
import {importFromCallgrind} from './callgrind'
export async function importProfileGroupFromText(
fileName: string,
@@ -72,26 +73,6 @@ function toGroup(profile: Profile | null): ProfileGroup | null {
return {name: profile.getName(), indexToView: 0, profiles: [profile]}
}
function fixUpJSON(content: string): string {
// This code is similar to the code from here:
// https://github.com/catapult-project/catapult/blob/27e047e0494df162022be6aa8a8862742a270232/tracing/tracing/extras/importer/trace_event_importer.html#L197-L208
//
// If the event data begins with a [, then we know it should end with a ]. The
// reason we check for this is because some tracing implementations cannot
// guarantee that a ']' gets written to the trace file. So, we are forgiving
// and if this is obviously the case, we fix it up before throwing the string
// at JSON.parse.
//
content = content.trim()
if (content[0] === '[') {
content = content.replace(/,\s*$/, '')
if (content[content.length - 1] !== ']') {
content += ']'
}
}
return content
}
async function _importProfileGroup(dataSource: ProfileDataSource): Promise<ProfileGroup | null> {
const fileName = await dataSource.name()
@@ -110,13 +91,13 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
// First pass: Check known file format names to infer the file type
if (fileName.endsWith('.speedscope.json')) {
console.log('Importing as speedscope json file')
return importSpeedscopeProfiles(JSON.parse(contents))
return importSpeedscopeProfiles(contents.parseAsJSON())
} else if (fileName.endsWith('.chrome.json') || /Profile-\d{8}T\d{6}/.exec(fileName)) {
console.log('Importing as Chrome Timeline')
return importFromChromeTimeline(JSON.parse(contents), fileName)
return importFromChromeTimeline(contents.parseAsJSON(), fileName)
} else if (fileName.endsWith('.stackprof.json')) {
console.log('Importing as stackprof profile')
return toGroup(importFromStackprof(JSON.parse(contents)))
return toGroup(importFromStackprof(contents.parseAsJSON()))
} else if (fileName.endsWith('.instruments.txt')) {
console.log('Importing as Instruments.app deep copy')
return toGroup(importFromInstrumentsDeepCopy(contents))
@@ -128,24 +109,27 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
return toGroup(importFromBGFlameGraph(contents))
} else if (fileName.endsWith('.v8log.json')) {
console.log('Importing as --prof-process v8 log')
return toGroup(importFromV8ProfLog(JSON.parse(contents)))
return toGroup(importFromV8ProfLog(contents.parseAsJSON()))
} else if (fileName.endsWith('.heapprofile')) {
console.log('Importing as Chrome Heap Profile')
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
return toGroup(importFromChromeHeapProfile(contents.parseAsJSON()))
} else if (fileName.endsWith('-recording.json')) {
console.log('Importing as Safari profile')
return toGroup(importFromSafari(JSON.parse(contents)))
return toGroup(importFromSafari(contents.parseAsJSON()))
} else if (fileName.startsWith('callgrind.')) {
console.log('Importing as Callgrind profile')
return importFromCallgrind(contents, fileName)
}
// Second pass: Try to guess what file format it is based on structure
let parsed: any
try {
parsed = JSON.parse(fixUpJSON(contents))
parsed = contents.parseAsJSON()
} catch (e) {}
if (parsed) {
if (parsed['$schema'] === 'https://www.speedscope.app/file-format-schema.json') {
console.log('Importing as speedscope json file')
return importSpeedscopeProfiles(JSON.parse(contents))
return importSpeedscopeProfiles(parsed)
} else if (parsed['systemHost'] && parsed['systemHost']['name'] == 'Firefox') {
console.log('Importing as Firefox profile')
return toGroup(importFromFirefox(parsed))
@@ -169,30 +153,32 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
return toGroup(importFromV8ProfLog(parsed))
} else if ('head' in parsed && 'selfSize' in parsed['head']) {
console.log('Importing as Chrome Heap Profile')
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
return toGroup(importFromChromeHeapProfile(parsed))
} else if ('rts_arguments' in parsed && 'initial_capabilities' in parsed) {
console.log('Importing as Haskell GHC JSON Profile')
return importFromHaskell(parsed)
} else if ('recording' in parsed && 'sampleStackTraces' in parsed.recording) {
console.log('Importing as Safari profile')
return toGroup(importFromSafari(JSON.parse(contents)))
return toGroup(importFromSafari(parsed))
}
} else {
// Format is not JSON
// If the first line contains "Symbol Name", preceded by a tab, it's probably
// a deep copy from OS X Instruments.app
if (/^[\w \t\(\)]*\tSymbol Name/.exec(contents)) {
console.log('Importing as Instruments.app deep copy')
return toGroup(importFromInstrumentsDeepCopy(contents))
// If the first line is "# callgrind format", it's probably in Callgrind
// Profile Format.
if (
/^# callgrind format/.exec(contents.firstChunk()) ||
(/^events:/m.exec(contents.firstChunk()) && /^fn=/m.exec(contents.firstChunk()))
) {
console.log('Importing as Callgrind profile')
return importFromCallgrind(contents, fileName)
}
// If every line ends with a space followed by a number, it's probably
// the collapsed stack format.
const lineCount = contents.split(/\n/).length
if (lineCount >= 1 && lineCount === contents.split(/ \d+\r?\n/).length) {
console.log('Importing as collapsed stack format')
return toGroup(importFromBGFlameGraph(contents))
// If the first line contains "Symbol Name", preceded by a tab, it's probably
// a deep copy from OS X Instruments.app
if (/^[\w \t\(\)]*\tSymbol Name/.exec(contents.firstChunk())) {
console.log('Importing as Instruments.app deep copy')
return toGroup(importFromInstrumentsDeepCopy(contents))
}
const fromLinuxPerf = importFromLinuxPerf(contents)
@@ -200,6 +186,12 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
console.log('Importing from linux perf script output')
return fromLinuxPerf
}
const fromBGFlameGraph = importFromBGFlameGraph(contents)
if (fromBGFlameGraph) {
console.log('Importing as collapsed stack format')
return toGroup(fromBGFlameGraph)
}
}
// Unrecognized format
+6 -6
View File
@@ -11,10 +11,10 @@ import {
import {sortBy, getOrThrow, getOrInsert, lastOf, getOrElse, zeroPad} from '../lib/utils'
import {ByteFormatter, TimeFormatter} from '../lib/value-formatters'
import {FileSystemDirectoryEntry, FileSystemEntry, FileSystemFileEntry} from './file-system-entry'
import {MaybeCompressedDataReader} from './utils'
import {MaybeCompressedDataReader, TextFileContent} from './utils'
function parseTSV<T>(contents: string): T[] {
const lines = contents.split('\n').map(l => l.split('\t'))
function parseTSV<T>(contents: TextFileContent): T[] {
const lines = contents.splitLines().map(l => l.split('\t'))
const headerLine = lines.shift()
if (!headerLine) return []
@@ -94,7 +94,7 @@ function getWeight(deepCopyRow: any): number {
}
// Import from a deep copy made of a profile
export function importFromInstrumentsDeepCopy(contents: string): Profile {
export function importFromInstrumentsDeepCopy(contents: TextFileContent): Profile {
const profile = new CallTreeProfileBuilder()
const rows = parseTSV<PastedTimeProfileRow | PastedAllocationsProfileRow>(contents)
@@ -187,7 +187,7 @@ function readAsArrayBuffer(file: File): Promise<ArrayBuffer> {
return MaybeCompressedDataReader.fromFile(file).readAsArrayBuffer()
}
function readAsText(file: File): Promise<string> {
function readAsText(file: File): Promise<TextFileContent> {
return MaybeCompressedDataReader.fromFile(file).readAsText()
}
@@ -259,7 +259,7 @@ async function getRawSampleList(core: TraceDirectoryTree): Promise<Sample[]> {
const schemaFile = storedir.files.get('schema.xml')
if (!schemaFile) continue
const schema = await readAsText(schemaFile)
if (!/name="time-profile"/.exec(schema)) {
if (!/name="time-profile"/.exec(schema.firstChunk())) {
continue
}
const bulkstore = new BinReader(
+6
View File
@@ -1,4 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
import {withMockedFileChunkSizeForTests} from './utils'
describe('importFromLinuxPerf', () => {
test('simple.linux-perf.txt', async () => {
@@ -19,4 +20,9 @@ describe('importFromLinuxPerf', () => {
test('system-wide.linux-perf.txt', async () => {
await checkProfileSnapshot('./sample/profiles/linux-perf/system-wide.linux-perf.txt')
})
test('system-wide.linux-perf.txt chunked', async () => {
await withMockedFileChunkSizeForTests(100, async () => {
await checkProfileSnapshot('./sample/profiles/linux-perf/system-wide.linux-perf.txt')
})
})
})
+31 -2
View File
@@ -1,6 +1,7 @@
import {StackListProfileBuilder, ProfileGroup} from '../lib/profile'
import {itMap, getOrInsert} from '../lib/utils'
import {TimeFormatter} from '../lib/value-formatters'
import {TextFileContent} from './utils'
// This imports the output of the "perf script" command on linux.
//
@@ -80,11 +81,39 @@ function parseEvent(rawEvent: string): PerfEvent | null {
return event
}
export function importFromLinuxPerf(contents: string): ProfileGroup | null {
function splitBlocks(contents: TextFileContent): string[] {
// In perf files, blocks are separated by '\n\n'. If our input was a simple
// string, we could use str.split('\n\n'), but since we have a TextFileContent
// object which may be backed by several strings, we can't easily split this
// way.
//
// Instead, we'll split into lines, and then re-group runs of non-empty strings.
const blocks: string[] = []
let pending: string = ''
for (let line of contents.splitLines()) {
if (line === '') {
if (pending.length > 0) {
blocks.push(pending)
}
pending = line
} else {
if (pending.length > 0) {
pending += '\n'
}
pending += line
}
}
if (pending.length > 0) {
blocks.push(pending)
}
return blocks
}
export function importFromLinuxPerf(contents: TextFileContent): ProfileGroup | null {
const profiles = new Map<string, StackListProfileBuilder>()
let eventType: string | null = null
const events = contents.split('\n\n').map(parseEvent)
const events = splitBlocks(contents).map(parseEvent)
for (let event of events) {
if (event == null) continue
+1 -1
View File
@@ -72,7 +72,7 @@ function makeStack(frames: StackFrame[]): FrameInfo[] {
file: url,
line,
col: column,
name: name || '(anonymous)',
name: name || (url ? `(anonymous ${url.split('/').pop()}:${line})` : '(anonymous)'),
}))
.reverse()
}
+4
View File
@@ -3,3 +3,7 @@ import {checkProfileSnapshot} from '../lib/test-utils'
test('importFromStackprof', async () => {
await checkProfileSnapshot('./sample/profiles/stackprof/simple-stackprof.json')
})
test('importFromStackprof object mode', async () => {
await checkProfileSnapshot('./sample/profiles/stackprof/object-stackprof.json')
})
+20 -8
View File
@@ -11,15 +11,19 @@ interface StackprofFrame {
export interface StackprofProfile {
frames: {[number: string]: StackprofFrame}
mode: string
raw: number[]
raw_timestamp_deltas: number[]
samples: number
}
export function importFromStackprof(stackprofProfile: StackprofProfile): Profile {
const duration = stackprofProfile.raw_timestamp_deltas.reduce((a, b) => a + b, 0)
const profile = new StackListProfileBuilder(duration)
const {frames, mode, raw, raw_timestamp_deltas, samples} = stackprofProfile
const objectMode = mode == 'object'
const size = objectMode ? samples : stackprofProfile.raw_timestamp_deltas.reduce((a, b) => a + b, 0)
const profile = new StackListProfileBuilder(size)
const {frames, raw, raw_timestamp_deltas} = stackprofProfile
let sampleIndex = 0
let prevStack: FrameInfo[] = []
@@ -40,15 +44,23 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile
}
const nSamples = raw[i++]
let sampleDuration = 0
for (let j = 0; j < nSamples; j++) {
sampleDuration += raw_timestamp_deltas[sampleIndex++]
if (objectMode) {
profile.appendSampleWithWeight(stack, nSamples)
} else {
let sampleDuration = 0
for (let j = 0; j < nSamples; j++) {
sampleDuration += raw_timestamp_deltas[sampleIndex++]
}
profile.appendSampleWithWeight(stack, sampleDuration)
}
profile.appendSampleWithWeight(stack, sampleDuration)
prevStack = stack
}
profile.setValueFormatter(new TimeFormatter('microseconds'))
if (!objectMode) {
profile.setValueFormatter(new TimeFormatter('microseconds'))
}
return profile.build()
}
+19
View File
@@ -1,4 +1,5 @@
import {checkProfileSnapshot} from '../lib/test-utils'
import {withMockedFileChunkSizeForTests} from './utils'
test('importTraceEvents simple', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple.json')
@@ -16,14 +17,32 @@ test('importTraceEvents partial json import', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial.json')
})
test('importTraceEvents partial json import chunked', async () => {
await withMockedFileChunkSizeForTests(100, async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial.json')
})
})
test('importTraceEvents partial json import trailing comma', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-trailing-comma.json')
})
test('importTraceEvents partial json import trailing comma chunked', async () => {
await withMockedFileChunkSizeForTests(100, async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-trailing-comma.json')
})
})
test('importTraceEvents partial json import whitespace padding', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-whitespace.json')
})
test('importTraceEvents partial json import whitespace padding chunked', async () => {
await withMockedFileChunkSizeForTests(100, async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-whitespace.json')
})
})
test('importTraceEvents bad E events', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/too-many-end-events.json')
})
+194 -37
View File
@@ -1,9 +1,197 @@
import * as pako from 'pako'
import {JSON_parse} from 'uint8array-json-parser'
export interface ProfileDataSource {
name(): Promise<string>
readAsArrayBuffer(): Promise<ArrayBuffer>
readAsText(): Promise<string>
readAsText(): Promise<TextFileContent>
}
export interface TextFileContent {
splitLines(): string[]
firstChunk(): string
parseAsJSON(): any
}
// V8 has a maximum string size. To support files whose contents exceeds that
// size, we provide an alternate string interface for text backed by a
// Uint8Array instead.
//
// We provide a simple splitLines() which returns simple strings under the
// assumption that most extremely large text profiles will be broken into many
// lines. This isn't true in the general case, but will be true for most common
// large files.
//
// See: https://github.com/v8/v8/blob/8b663818fc311217c2cdaaab935f020578bfb7a8/src/objects/string.h#L479-L483
//
// At time of writing (2021/03/27), the maximum string length in V8 is
// 32 bit systems: 2^28 - 16 = ~268M chars
// 64 bit systems: 2^29 - 24 = ~537M chars
//
// https://source.chromium.org/chromium/chromium/src/+/main:v8/include/v8-primitive.h;drc=cb88fe94d9044d860cc75c89e1bc270ab4062702;l=125
//
// We'll be conservative and feed in 2^27 bytes at a time (~134M chars
// assuming utf-8 encoding)
let TEXT_FILE_CHUNK_SIZE = 1 << 27
export async function withMockedFileChunkSizeForTests(chunkSize: number, cb: () => any) {
const original = TEXT_FILE_CHUNK_SIZE
TEXT_FILE_CHUNK_SIZE = chunkSize
try {
await cb()
} finally {
TEXT_FILE_CHUNK_SIZE = original
}
}
function permissivelyParseJSONString(content: string) {
// This code is similar to the code from here:
// https://github.com/catapult-project/catapult/blob/27e047e0494df162022be6aa8a8862742a270232/tracing/tracing/extras/importer/trace_event_importer.html#L197-L208
//
// If the event data begins with a [, then we know it should end with a ]. The
// reason we check for this is because some tracing implementations cannot
// guarantee that a ']' gets written to the trace file. So, we are forgiving
// and if this is obviously the case, we fix it up before throwing the string
// at JSON.parse.
content = content.trim()
if (content[0] === '[') {
content = content.replace(/,\s*$/, '')
if (content[content.length - 1] !== ']') {
content += ']'
}
}
return JSON.parse(content)
}
function permissivelyParseJSONUint8Array(byteArray: Uint8Array) {
let indexOfFirstNonWhitespaceChar = 0
for (let i = 0; i < byteArray.length; i++) {
if (!/\s/.exec(String.fromCharCode(byteArray[i]))) {
indexOfFirstNonWhitespaceChar = i
break
}
}
if (
byteArray[indexOfFirstNonWhitespaceChar] === '['.charCodeAt(0) &&
byteArray[byteArray.length - 1] !== ']'.charCodeAt(0)
) {
// Strip trailing whitespace from the end of the array
let trimmedLength = byteArray.length
while (trimmedLength > 0 && /\s/.exec(String.fromCharCode(byteArray[trimmedLength - 1]))) {
trimmedLength--
}
// Ignore trailing comma
if (String.fromCharCode(byteArray[trimmedLength - 1]) === ',') {
trimmedLength--
}
if (String.fromCharCode(byteArray[trimmedLength - 1]) !== ']') {
// Clone the array, ignoring any whitespace & trailing comma, then append a ']'
//
// Note: We could save a tiny bit of space here by avoiding copying the
// leading whitespace, but it's a trivial perf boost and it complicates
// the code.
const newByteArray = new Uint8Array(trimmedLength + 1)
newByteArray.set(byteArray.subarray(0, trimmedLength))
newByteArray[trimmedLength] = ']'.charCodeAt(0)
byteArray = newByteArray
}
}
return JSON_parse(byteArray)
}
export class BufferBackedTextFileContent implements TextFileContent {
private chunks: string[] = []
private byteArray: Uint8Array
constructor(buffer: ArrayBuffer) {
const byteArray = (this.byteArray = new Uint8Array(buffer))
let encoding: string = 'utf-8'
if (byteArray.length > 2) {
if (byteArray[0] === 0xff && byteArray[1] === 0xfe) {
// UTF-16, Little Endian encoding
encoding = 'utf-16le'
} else if (byteArray[0] === 0xfe && byteArray[1] === 0xff) {
// UTF-16, Big Endian encoding
encoding = 'utf-16be'
}
}
if (typeof TextDecoder !== 'undefined') {
// If TextDecoder is available, we'll try to use it to decode the string.
const decoder = new TextDecoder(encoding)
for (let chunkNum = 0; chunkNum < buffer.byteLength / TEXT_FILE_CHUNK_SIZE; chunkNum++) {
const offset = chunkNum * TEXT_FILE_CHUNK_SIZE
const view = new Uint8Array(
buffer,
offset,
Math.min(buffer.byteLength - offset, TEXT_FILE_CHUNK_SIZE),
)
const chunk = decoder.decode(view, {stream: true})
this.chunks.push(chunk)
}
} else {
// JavaScript strings are UTF-16 encoded, but we're reading data from disk
// that we're going to blindly assume it's ASCII encoded. This codepath
// only exists for older browser support.
console.warn('This browser does not support TextDecoder. Decoding text as ASCII.')
this.chunks.push('')
for (let i = 0; i < byteArray.length; i++) {
this.chunks[this.chunks.length - 1] += String.fromCharCode(byteArray[i])
;(this.chunks[this.chunks.length - 1] as any) | 0 // This forces the string to be flattened
if (this.chunks[this.chunks.length - 1].length >= TEXT_FILE_CHUNK_SIZE) {
this.chunks.push('')
}
}
}
}
splitLines(): string[] {
let parts: string[] = this.chunks[0].split('\n')
for (let i = 1; i < this.chunks.length; i++) {
const chunkParts = this.chunks[i].split('\n')
if (chunkParts.length === 0) continue
if (parts.length > 0) {
parts[parts.length - 1] += chunkParts.shift()
}
parts = parts.concat(chunkParts)
}
return parts
}
firstChunk(): string {
return this.chunks[0] || ''
}
parseAsJSON(): any {
// We only use the Uint8Array version of JSON.parse when necessary, because
// it's around 4x slower than native.
if (this.chunks.length === 1) {
return permissivelyParseJSONString(this.chunks[0])
}
return permissivelyParseJSONUint8Array(this.byteArray)
}
}
export class StringBackedTextFileContent implements TextFileContent {
constructor(private s: string) {}
splitLines(): string[] {
return this.s.split('\n')
}
firstChunk(): string {
return this.s
}
parseAsJSON(): any {
return permissivelyParseJSONString(this.s)
}
}
export class TextProfileDataSource implements ProfileDataSource {
@@ -11,16 +199,13 @@ export class TextProfileDataSource implements ProfileDataSource {
async name() {
return this.fileName
}
async readAsArrayBuffer() {
// JavaScript strings are UTF-16 encoded, but if this string is
// constructed based on
// TODO(jlfwong): Might want to make this construct an array
// buffer based on the text
async readAsArrayBuffer() {
return new ArrayBuffer(0)
}
async readAsText() {
return this.contents
return new StringBackedTextFileContent(this.contents)
}
}
@@ -49,37 +234,9 @@ export class MaybeCompressedDataReader implements ProfileDataSource {
return await this.uncompressedData
}
async readAsText(): Promise<string> {
async readAsText(): Promise<TextFileContent> {
const buffer = await this.readAsArrayBuffer()
// By default, we assume the file is utf-8 encoded.
let encoding = 'utf-8'
const array = new Uint8Array(buffer)
if (array.length > 2) {
if (array[0] === 0xff && array[1] === 0xfe) {
// UTF-16, Little Endian encoding
encoding = 'utf-16le'
} else if (array[0] === 0xfe && array[1] === 0xff) {
// UTF-16, Big Endian encoding
encoding = 'utf-16be'
}
}
if (typeof TextDecoder !== 'undefined') {
const decoder = new TextDecoder(encoding)
return decoder.decode(buffer)
} else {
// JavaScript strings are UTF-16 encoded, but we're reading data from disk
// that we're going to blindly assume it's ASCII encoded. This codepath
// only exists for older browser support.
console.warn('This browser does not support TextDecoder. Decoding text as ASCII.')
let ret: string = ''
for (let i = 0; i < array.length; i++) {
ret += String.fromCharCode(array[i])
}
return ret
}
return new BufferBackedTextFileContent(buffer)
}
static fromFile(file: File): MaybeCompressedDataReader {
+3 -1
View File
@@ -37,10 +37,12 @@ interface HeapProfile {
const callFrameToFrameInfo = new Map<HeapProfileCallFrame, FrameInfo>()
function frameInfoForCallFrame(callFrame: HeapProfileCallFrame) {
return getOrInsert(callFrameToFrameInfo, callFrame, callFrame => {
const name = callFrame.functionName || '(anonymous)'
const file = callFrame.url
const line = callFrame.lineNumber
const col = callFrame.columnNumber
const name =
callFrame.functionName ||
(file ? `(anonymous ${file.split('/').pop()}:${line})` : '(anonymous)')
return {
key: `${name}:${file}:${line}:${col}`,
name,
+13 -4
View File
@@ -80,12 +80,21 @@ function codeToFrameInfo(code: Code, v8log: V8LogProfile): FrameInfo {
case 'JS': {
const matches = name.match(/([a-zA-Z0-9\._\-$]*) ([a-zA-Z0-9\.\-_\/$]*):(\d+):(\d+)/)
if (matches) {
const file = matches[2]
const line = parseInt(matches[3], 10)
const col = parseInt(matches[4], 10)
const functionName =
matches[1].length > 0
? matches[1]
: file
? `(anonymous ${file.split('/').pop()}:${line})`
: '(anonymous)'
return {
key: name,
name: matches[1].length > 0 ? matches[1] : '(anonymous)',
file: matches[2].length > 0 ? matches[2] : '(unknown file)',
line: parseInt(matches[3], 10),
col: parseInt(matches[4], 10),
name: functionName,
file: file.length > 0 ? file : '(unknown file)',
line,
col,
}
}
break
@@ -2,7 +2,7 @@
exports[`source-map remapping of chrome-85-esbuild 1`] = `
Array [
"((anonymous) @ alpha.ts:5:5) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:170)",
"((anonymous typescript-source-map-test.js:1) @ alpha.ts:5:5) <- ((anonymous typescript-source-map-test.js:1) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:170)",
"(e @ beta.ts:3:8) <- (e @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:75)",
"(get floop @ kludge.ts:13:7) <- (get floop @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:276)",
"(l constructor @ kludge.ts:4:3) <- (l @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:226)",
@@ -15,7 +15,7 @@ Array [
exports[`source-map remapping of chrome-85-parcel 1`] = `
Array [
"((anonymous) @ ../alpha.ts:5:5) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:8:152)",
"((anonymous typescript-source-map-test.js:8) @ ../alpha.ts:5:5) <- ((anonymous typescript-source-map-test.js:8) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:8:152)",
"(e @ ../gamma.ts:1:22) <- (e @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:2:104)",
"(get @ ../kludge.ts:13:3) <- (get @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:10:284)",
"(o.zap @ ../kludge.ts:9:3) <- (o.zap @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:10:211)",
@@ -32,7 +32,7 @@ Array [
exports[`source-map remapping of chrome-85-webpack 1`] = `
Array [
"((anonymous) @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
"((anonymous typescript-source-map-test.js:1) @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- ((anonymous typescript-source-map-test.js:1) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
"(get @ webpack://speedscope-sourcemap-test-project/kludge.ts:13:3) <- (get @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:368)",
"(n.zap @ webpack://speedscope-sourcemap-test-project/kludge.ts:9:3) <- (n.zap @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:305)",
"(n constructor @ webpack://speedscope-sourcemap-test-project/kludge.ts:4:3) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:243)",
@@ -58,8 +58,8 @@ Array [
exports[`source-map remapping of safari-13-webpack 1`] = `
Array [
"((anonymous) @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
"(zap @ webpack://speedscope-sourcemap-test-project/kludge.ts:9:3) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:305)",
"((anonymous typescript-source-map-test.js:1) @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- ((anonymous typescript-source-map-test.js:1) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
"(zap @ webpack://speedscope-sourcemap-test-project/kludge.ts:9:3) <- ((anonymous typescript-source-map-test.js:1) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:305)",
"(get @ webpack://speedscope-sourcemap-test-project/kludge.ts:13:3) <- (get @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:368)",
"(n constructor @ webpack://speedscope-sourcemap-test-project/kludge.ts:4:3) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:243)",
"(n @ webpack://speedscope-sourcemap-test-project/gamma.ts:1:22) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:34)",
+104
View File
@@ -0,0 +1,104 @@
import {Atom} from './atom'
describe('Atom', () => {
test('basic get/set', () => {
const num = new Atom<number>(0, 'test1')
expect(num.get()).toEqual(0)
num.set(1)
expect(num.get()).toEqual(1)
})
test('basic subscribe/unsubscribe', () => {
let count = 0
let latestNum = 0
function listener() {
count++
latestNum = num.get()
}
const num = new Atom<number>(0, 'test2')
num.subscribe(listener)
expect(count).toEqual(0)
expect(latestNum).toEqual(0)
num.set(3)
expect(count).toEqual(1)
expect(latestNum).toEqual(3)
num.set(7)
expect(count).toEqual(2)
expect(latestNum).toEqual(7)
num.unsubscribe(listener)
num.set(11)
expect(count).toEqual(2)
expect(latestNum).toEqual(7)
expect(num.get()).toEqual(11)
})
test('basic no-op on no-value change', () => {
let count = 0
let latestNum = 0
function listener() {
count++
latestNum = num.get()
}
const num = new Atom<number>(0, 'test3')
num.subscribe(listener)
expect(count).toEqual(0)
expect(latestNum).toEqual(0)
num.set(3)
expect(count).toEqual(1)
expect(latestNum).toEqual(3)
num.set(3)
expect(count).toEqual(1)
expect(latestNum).toEqual(3)
})
test('bound getters and setters', () => {
const num = new Atom<number>(0, 'test1')
let setter = num.set
let getter = num.get
expect(getter()).toEqual(0)
setter(1)
expect(getter()).toEqual(1)
expect(num.get()).toEqual(1)
num.set(2)
expect(getter()).toEqual(2)
})
test('subclassing', () => {
class ListAtom<T> extends Atom<T[]> {
push(t: T) {
const next = [...this.state, t]
this.set(next)
}
removeLast() {
const next = [...this.state]
next.pop()
this.set(next)
}
}
const myListAtom = new ListAtom<number>([1, 2, 3], '')
let count = 0
myListAtom.subscribe(() => {
count++
})
expect(myListAtom.get()).toEqual([1, 2, 3])
myListAtom.push(4)
expect(myListAtom.get()).toEqual([1, 2, 3, 4])
myListAtom.removeLast()
expect(myListAtom.get()).toEqual([1, 2, 3])
expect(count).toEqual(2)
})
})
+153
View File
@@ -0,0 +1,153 @@
// This is a very small application state management library.
//
// All it provides is a way of specifying state "atoms" (basically a value with
// a setter and a way to be notified when the value updates), and a single
// preact hook to manage the subscribe/unsubscribe process for you.
//
// At the moment, atoms are intended to be globally defined, but the system
// could easily be adapted to pass down the atoms via context rather than being
// globally available.
//
// To support complex data-types, you can either have simple functions that call
// the public setter, or subclass Atom. So, for example, here are a few
// different ways of making a list atom with convenient helpers:
//
// # Simple functions being passed atom
//
// const myListAtom = new Atom<number[]>([], "myList")
// function push<T>(atom: Atom<T[]>, t: T) {
// const next = [...atom.get(), t]
// atom.set(next)
// }
// function removeLast<T>(atom: Atom<T[]>, t: T) {
// const next = [...atom.get()]
// next.pop()
// atom.set(next)
// }
//
//
// # Simple functions operating on global atoms
//
// const myListAtom = new Atom<number[]>([], "myLisT")
// function push(t: number) {
// myListAtom.set([...myListAtom.get(), t])
// }
// function removeLast() {
// const next = [...myListAtom.get()]
// myListAtom.set(next)
// }
//
//
// # Subclassing
//
// class ListAtom<T> extends Atom<T[]> {
// push(t: T) {
// const next = [...this.state, t]
// this.set(next)
// }
// removeLast() {
// const next = [...this.state]
// next.pop()
// this.set(next)
// }
// }
// const myListAtom = new ListAtom<number>([], "myList")
//
// This library is inspired by https://recoiljs.org/
import {useLayoutEffect, useState} from 'preact/hooks'
type AtomListener = () => void
let AtomDev: {[key: string]: Atom<any>} | null = null
let hotReloadStash: Map<string, any> | null = null
declare const module: any
if (process.env.NODE_ENV === 'development') {
;(window as any)['Atom'] = AtomDev = {}
module.hot.dispose(() => {
if (AtomDev) {
hotReloadStash = new Map()
for (let key in AtomDev) {
hotReloadStash.set(key, AtomDev[key].get())
}
}
;(window as any)['Atom_hotReloadStash'] = hotReloadStash
})
hotReloadStash = (window as any)['Atom_hotReloadStash'] || null
}
export class Atom<T> {
private observers: AtomListener[] = []
constructor(protected state: T, debugKey: string) {
if (process.env.NODE_ENV === 'development') {
if (hotReloadStash?.has(debugKey)) {
// If we have a stored value from a previous hot reload, use that
// instead of whatever was passed to the constructor.
this.state = hotReloadStash.get(debugKey)
}
if (AtomDev) {
if (debugKey in AtomDev) {
console.warn(`[Atom] Multiple atoms tried to register with the key ${debugKey}`)
}
AtomDev[debugKey] = this
}
}
// We do the bind here rather than in the definition to facilitate
// inheritance (we want the value defined on both the prototype and the
// instance).
this.set = this.set.bind(this)
this.get = this.get.bind(this)
}
set(t: T) {
if (this.state === t) {
// No-op if the value didn't actually change
return
}
this.state = t
this.observers.forEach(fn => fn())
}
get(): T {
return this.state
}
subscribe(listener: AtomListener) {
this.observers.push(listener)
}
unsubscribe(listener: AtomListener) {
const index = this.observers.indexOf(listener)
if (index !== -1) {
this.observers.splice(index, 1)
}
}
}
export function useAtom<T>(atom: Atom<T>): T {
const [value, setValue] = useState(atom.get())
useLayoutEffect(() => {
// We need to setValue here because it's possible something has changed the
// value in the store between the atom.get() call above and layout. In most
// cases this should no-op.
setValue(atom.get())
function listener() {
setValue(atom.get())
}
atom.subscribe(listener)
return () => {
atom.unsubscribe(listener)
}
}, [atom])
return value
}
+2 -2
View File
@@ -9,11 +9,11 @@ test('getHashParams', () => {
})
expect(
getHashParams(
'#profileURL=https://raw.githubusercontent.com/jlfwong/speedscope/master/sample/profiles/speedscope/0.1.2/simple-sampled.speedscope.json',
'#profileURL=https://raw.githubusercontent.com/jlfwong/speedscope/main/sample/profiles/speedscope/0.1.2/simple-sampled.speedscope.json',
),
).toEqual({
profileURL:
'https://raw.githubusercontent.com/jlfwong/speedscope/master/sample/profiles/speedscope/0.1.2/simple-sampled.speedscope.json',
'https://raw.githubusercontent.com/jlfwong/speedscope/main/sample/profiles/speedscope/0.1.2/simple-sampled.speedscope.json',
})
expect(getHashParams('#title=hello&localProfilePath=file:///tmp/file.js')).toEqual({
title: 'hello',
+23
View File
@@ -1,7 +1,23 @@
import {ViewMode} from '../lib/view-mode'
export interface HashParams {
profileURL?: string
title?: string
localProfilePath?: string
viewMode?: ViewMode
}
function getViewMode(value: string): ViewMode | null {
switch (value) {
case 'time-ordered':
return ViewMode.CHRONO_FLAME_CHART
case 'left-heavy':
return ViewMode.LEFT_HEAVY_FLAME_GRAPH
case 'sandwich':
return ViewMode.SANDWICH_VIEW
default:
return null
}
}
export function getHashParams(hashContents = window.location.hash): HashParams {
@@ -20,6 +36,13 @@ export function getHashParams(hashContents = window.location.hash): HashParams {
result.title = value
} else if (key === 'localProfilePath') {
result.localProfilePath = value
} else if (key === 'view') {
const mode = getViewMode(value)
if (mode !== null) {
result.viewMode = mode
} else {
console.error(`Ignoring invalid view specifier: ${value}`)
}
}
}
return result
+6
View File
@@ -0,0 +1,6 @@
import {Component} from 'preact'
export type VoidState = {
__dummyField: void
}
export abstract class StatelessComponent<P> extends Component<P, VoidState> {}
-74
View File
@@ -1,74 +0,0 @@
/**
* As of Preact 10.x, they no longer have an officially supported preact-redux library.
* It's possible to use react-redux with some hacks, but these hacks cause npm run pack
* to error out because of (intentionally) unmet peer dependencies.
*
* I could stack more hacks to fix this problem, but I'd rather just drop the dependency
* and remove the need to do any dependency hacking by writing the very small part of
* react-redux that I actually need myself.
*/
import {h} from 'preact'
import * as redux from 'redux'
import {createContext, ComponentChildren} from 'preact'
import {Dispatch, Action} from './typed-redux'
import {useState, useContext, useCallback, useLayoutEffect} from 'preact/hooks'
const PreactRedux = createContext<redux.Store<any> | null>(null)
interface ProviderProps {
store: redux.Store<any>
children?: ComponentChildren
}
export function Provider(props: ProviderProps) {
return <PreactRedux.Provider value={props.store} children={props.children} />
}
function useStore<T>(): redux.Store<T> {
const store = useContext(PreactRedux)
if (store == null) {
throw new Error('Called useStore when no store exists in context')
}
return store
}
export function useDispatch(): Dispatch {
const store = useStore()
return store.dispatch
}
export function useActionCreator<T, U>(
creator_: (payload: T) => Action<U>,
cacheArgs: any[],
): (t: T) => void {
const dispatch = useDispatch()
/* eslint-disable react-hooks/exhaustive-deps */
const creator = useCallback(creator_, cacheArgs)
return useCallback((t: T) => dispatch(creator(t)), [dispatch, creator])
}
export function useSelector<T, U>(selector_: (t: T) => U, cacheArgs: any[]): U {
const store = useStore<T>()
/* eslint-disable react-hooks/exhaustive-deps */
const selector = useCallback(selector_, cacheArgs)
const getValueFromStore = useCallback(() => selector(store.getState()), [store, selector])
const [value, setValue] = useState(getValueFromStore)
useLayoutEffect(() => {
// We need to setValue here because it's possible something has changed the
// value in the store between the useSelector call and layout. In most cases
// this should no-op.
setValue(getValueFromStore())
return store.subscribe(() => {
setValue(getValueFromStore())
})
}, [store, getValueFromStore])
return value
}
-68
View File
@@ -1,68 +0,0 @@
import * as redux from 'redux'
import {Component} from 'preact'
export interface Action<TPayload> extends redux.Action<string> {
payload: TPayload
}
export interface ActionCreator<TPayload> {
// Returns an action with a non-empty payload
(payload: TPayload): Action<TPayload>
// Returns an action with an empty payload ({})
(): Action<TPayload>
matches(action: Action<any>): action is Action<TPayload>
}
const usedActionTypes = new Set<string>()
export function actionCreator(type: string): ActionCreator<void>
export function actionCreator<TPayload>(type: string): ActionCreator<TPayload>
export function actionCreator(type: string) {
if (usedActionTypes.has(type)) {
throw new Error(`Cannot re-use action type name: ${type}`)
}
const creator: any = (payload = {}) => {
return {type, payload}
}
creator.matches = (action: Action<any>) => {
return action.type === type
}
return creator
}
export type Reducer<T> = (state: T | undefined, action: Action<any>) => T
export type ReducerWithActionType<T, A> = (state: T | undefined, action: Action<A>) => T
export function setter<T>(
setterAction: ActionCreator<T>,
defaultVal: T,
): (state: T | undefined, action: Action<any>) => T {
return (state = defaultVal, action) => {
if (setterAction.matches(action)) {
return action.payload
}
return state
}
}
export type Dispatch = redux.Dispatch<Action<any>>
export type VoidState = {
__dummyField: void
}
export abstract class StatelessComponent<P> extends Component<P, VoidState> {}
export function bindActionCreator<T>(
dispatch: Dispatch,
actionCreator: (payload: T) => Action<T>,
): (t: T) => void {
return (t: T) => {
dispatch(actionCreator(t))
}
}
+57
View File
@@ -17,7 +17,14 @@ import {
findIndexBisect,
} from './utils'
import {TextEncoder} from 'util'
import * as jsc from 'jsverify'
import {
BufferBackedTextFileContent,
StringBackedTextFileContent,
withMockedFileChunkSizeForTests,
} from '../import/utils'
test('sortBy', () => {
const ls = ['a3', 'b2', 'c1', 'd4']
@@ -229,3 +236,53 @@ test('decodeBase64', () => {
return true
})
})
test('BufferBackedTextFileContent.firstChunk', async () => {
await withMockedFileChunkSizeForTests(2, () => {
const str = 'may\nyour\nrope\nbe\nlong'
const buffer = new TextEncoder().encode(str).buffer
const content = new BufferBackedTextFileContent(buffer)
expect(content.firstChunk()).toEqual('ma')
})
})
test('BufferBackedTextFileContent.splitLines', async () => {
await withMockedFileChunkSizeForTests(2, () => {
const str = 'may\nyour\nrope\nbe\nlong'
const buffer = new TextEncoder().encode(str).buffer
const content = new BufferBackedTextFileContent(buffer)
expect(content.splitLines()).toEqual(['may', 'your', 'rope', 'be', 'long'])
})
})
test('BufferBackedTextFileContent.parseAsJSON', async () => {
await withMockedFileChunkSizeForTests(2, () => {
// parseAsJSON is special cased to permissively allow trailing commas
// and a mission closing bracket
const str = '[200,300,400,'
const buffer = new TextEncoder().encode(str).buffer
const content = new BufferBackedTextFileContent(buffer)
expect(content.parseAsJSON()).toEqual([200, 300, 400])
})
})
test('StringBackedTextFileContent.firstChunk', async () => {
const str = 'may\nyour\nrope\nbe\nlong'
const content = new StringBackedTextFileContent(str)
expect(content.firstChunk()).toEqual(str)
})
test('StringBackedTextFileContent.splitLines', async () => {
const str = 'may\nyour\nrope\nbe\nlong'
const content = new StringBackedTextFileContent(str)
expect(content.splitLines()).toEqual(['may', 'your', 'rope', 'be', 'long'])
})
test('StringBackedTextFileContent.parseAsJSON', async () => {
// parseAsJSON is special cased to permissively allow trailing commas
// and a mission closing bracket
const str = '[200,300,400,'
const content = new StringBackedTextFileContent(str)
expect(content.parseAsJSON()).toEqual([200, 300, 400])
})
+5
View File
@@ -0,0 +1,5 @@
export const enum ViewMode {
CHRONO_FLAME_CHART,
LEFT_HEAVY_FLAME_GRAPH,
SANDWICH_VIEW,
}
+3 -11
View File
@@ -1,7 +1,5 @@
import {h, render} from 'preact'
import {createAppStore} from './store'
import {ApplicationContainer} from './views/application-container'
import {Provider} from './lib/preact-redux'
import {ThemeProvider} from './views/themes/theme'
console.log(`speedscope v${require('../package.json').version}`)
@@ -15,16 +13,10 @@ if (module.hot) {
module.hot.accept()
}
const lastStore: any = (window as any)['store']
const store = lastStore ? createAppStore(lastStore.getState()) : createAppStore()
;(window as any)['store'] = store
render(
<Provider store={store}>
<ThemeProvider>
<ApplicationContainer />
</ThemeProvider>
</Provider>,
<ThemeProvider>
<ApplicationContainer />
</ThemeProvider>,
document.body,
document.body.lastElementChild || undefined,
)
-53
View File
@@ -1,53 +0,0 @@
import {actionCreator} from '../lib/typed-redux'
import {CallTreeNode, Frame, ProfileGroup} from '../lib/profile'
import {SortMethod} from '../views/profile-table-view'
import {ColorScheme, ViewMode} from '.'
import {FlamechartID} from './flamechart-view-state'
import {Rect, Vec2} from '../lib/math'
import {HashParams} from '../lib/hash-params'
import {actionCreatorWithIndex} from './profiles-state'
export namespace actions {
export const setProfileGroup = actionCreator<ProfileGroup>('setProfileGroup')
export const setProfileIndexToView = actionCreator<number>('setProfileIndexToView')
export const setGLCanvas = actionCreator<HTMLCanvasElement | null>('setGLCanvas')
export const setViewMode = actionCreator<ViewMode>('setViewMode')
export const setFlattenRecursion = actionCreator<boolean>('setFlattenRecursion')
export const setSearchQuery = actionCreator<string>('setSearchQuery')
export const setSearchIsActive = actionCreator<boolean>('setSearchIsActive')
export const setDragActive = actionCreator<boolean>('setDragActive')
export const setLoading = actionCreator<boolean>('setLoading')
export const setError = actionCreator<boolean>('setError')
export const setHashParams = actionCreator<HashParams>('setHashParams')
export const setColorScheme = actionCreator<ColorScheme>('setColorScheme')
export namespace sandwichView {
export const setTableSortMethod = actionCreator<SortMethod>('sandwichView.setTableSortMethod')
export const setSelectedFrame = actionCreatorWithIndex<Frame | null>(
'sandwichView.setSelectedFrame',
)
}
export namespace flamechart {
export const setHoveredNode = actionCreatorWithIndex<{
id: FlamechartID
hover: {node: CallTreeNode; event: MouseEvent} | null
}>('flamechart.setHoveredNode')
export const setSelectedNode = actionCreatorWithIndex<{
id: FlamechartID
selectedNode: CallTreeNode | null
}>('flamechart.setSelectedNode')
export const setConfigSpaceViewportRect = actionCreatorWithIndex<{
id: FlamechartID
configSpaceViewportRect: Rect
}>('flamechart.setConfigSpaceViewportRect')
export const setLogicalSpaceViewportSize = actionCreatorWithIndex<{
id: FlamechartID
logicalSpaceViewportSize: Vec2
}>('flamechart.setLogicalSpaceViewportSpace')
}
}
-105
View File
@@ -1,105 +0,0 @@
import {storeTest, profileGroupTwoSampled} from './store-test-utils'
import {actions} from './actions'
import {Rect, Vec2} from '../lib/math'
import {FlamechartID} from './flamechart-view-state'
import {ViewMode} from '.'
describe('flamechart view state', () => {
storeTest('setHoveredNode', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(null)
const {profileGroup} = getState()
const hover = {
node: profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0],
event: {} as MouseEvent,
}
dispatch(
actions.flamechart.setHoveredNode({
profileIndex: 0,
args: {
id: FlamechartID.CHRONO,
hover,
},
}),
)
expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(hover)
expect(getState().profileGroup!.profiles[0].leftHeavyViewState.hover).toEqual(null)
expect(getState().profileGroup!.profiles[1].chronoViewState.hover).toEqual(null)
// Changing view mode should clear the hover state
dispatch(actions.setViewMode(ViewMode.LEFT_HEAVY_FLAME_GRAPH))
expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(null)
})
storeTest('setSelectedNode', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
expect(getState().profileGroup!.profiles[0].chronoViewState.selectedNode).toEqual(null)
const {profileGroup} = getState()
const selectedNode = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0]
dispatch(
actions.flamechart.setSelectedNode({
profileIndex: 0,
args: {
id: FlamechartID.CHRONO,
selectedNode,
},
}),
)
expect(getState().profileGroup!.profiles[0].chronoViewState.selectedNode).toEqual(selectedNode)
expect(getState().profileGroup!.profiles[0].leftHeavyViewState.selectedNode).toEqual(null)
expect(getState().profileGroup!.profiles[1].chronoViewState.selectedNode).toEqual(null)
})
storeTest('setLogicalSpaceViewportSize', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
expect(getState().profileGroup!.profiles[0].chronoViewState.logicalSpaceViewportSize).toEqual(
Vec2.zero,
)
const logicalSpaceViewportSize = new Vec2(3, 5)
dispatch(
actions.flamechart.setLogicalSpaceViewportSize({
profileIndex: 0,
args: {
id: FlamechartID.CHRONO,
logicalSpaceViewportSize,
},
}),
)
expect(getState().profileGroup!.profiles[0].chronoViewState.logicalSpaceViewportSize).toEqual(
logicalSpaceViewportSize,
)
expect(
getState().profileGroup!.profiles[0].leftHeavyViewState.logicalSpaceViewportSize,
).toEqual(Vec2.zero)
expect(getState().profileGroup!.profiles[1].chronoViewState.logicalSpaceViewportSize).toEqual(
Vec2.zero,
)
})
storeTest('setConfigSpaceViewportRect', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
expect(getState().profileGroup!.profiles[0].chronoViewState.configSpaceViewportRect).toEqual(
Rect.empty,
)
dispatch(
actions.flamechart.setConfigSpaceViewportRect({
profileIndex: 0,
args: {
id: FlamechartID.CHRONO,
configSpaceViewportRect: new Rect(Vec2.zero, Vec2.unit),
},
}),
)
expect(getState().profileGroup!.profiles[0].chronoViewState.configSpaceViewportRect).toEqual(
new Rect(Vec2.zero, Vec2.unit),
)
expect(getState().profileGroup!.profiles[0].leftHeavyViewState.configSpaceViewportRect).toEqual(
Rect.empty,
)
expect(getState().profileGroup!.profiles[1].chronoViewState.configSpaceViewportRect).toEqual(
Rect.empty,
)
})
})
-62
View File
@@ -1,62 +0,0 @@
import {CallTreeNode} from '../lib/profile'
import {Rect, Vec2} from '../lib/math'
import {Reducer} from '../lib/typed-redux'
import {actions} from './actions'
export enum FlamechartID {
LEFT_HEAVY = 'LEFT_HEAVY',
CHRONO = 'CHRONO',
SANDWICH_INVERTED_CALLERS = 'SANDWICH_INVERTED_CALLERS',
SANDWICH_CALLEES = 'SANDWICH_CALLEES',
}
export interface FlamechartViewState {
hover: {
node: CallTreeNode
event: MouseEvent
} | null
selectedNode: CallTreeNode | null
logicalSpaceViewportSize: Vec2
configSpaceViewportRect: Rect
}
export function createFlamechartViewStateReducer(
id: FlamechartID,
profileIndex: number,
): Reducer<FlamechartViewState> {
let initialState: FlamechartViewState = {
hover: null,
selectedNode: null,
configSpaceViewportRect: Rect.empty,
logicalSpaceViewportSize: Vec2.zero,
}
function applies(action: {payload: {profileIndex: number; args: {id: FlamechartID}}}) {
const {payload} = action
return payload.args.id === id && payload.profileIndex === profileIndex
}
return (state = initialState, action) => {
if (actions.flamechart.setHoveredNode.matches(action) && applies(action)) {
const {hover} = action.payload.args
return {...state, hover}
}
if (actions.flamechart.setSelectedNode.matches(action) && applies(action)) {
const {selectedNode} = action.payload.args
return {...state, selectedNode}
}
if (actions.flamechart.setConfigSpaceViewportRect.matches(action) && applies(action)) {
const {configSpaceViewportRect} = action.payload.args
return {...state, configSpaceViewportRect}
}
if (actions.flamechart.setLogicalSpaceViewportSize.matches(action) && applies(action)) {
const {logicalSpaceViewportSize} = action.payload.args
return {...state, logicalSpaceViewportSize}
}
if (actions.setViewMode.matches(action)) {
// If we switch views, the hover information is no longer relevant
return {...state, hover: null}
}
return state
}
}
-51
View File
@@ -1,51 +0,0 @@
import {ViewMode} from '.'
import {actions} from './actions'
import {storeTest} from './store-test-utils'
import {SortField, SortDirection} from '../views/profile-table-view'
storeTest('flattenRecursion', ({getState, dispatch}) => {
expect(getState().flattenRecursion).toBe(false)
dispatch(actions.setFlattenRecursion(true))
expect(getState().flattenRecursion).toBe(true)
})
storeTest('viewMode', ({getState, dispatch}) => {
expect(getState().viewMode).toBe(ViewMode.CHRONO_FLAME_CHART)
dispatch(actions.setViewMode(ViewMode.SANDWICH_VIEW))
expect(getState().viewMode).toBe(ViewMode.SANDWICH_VIEW)
})
storeTest('dragActive', ({getState, dispatch}) => {
expect(getState().dragActive).toBe(false)
dispatch(actions.setDragActive(true))
expect(getState().dragActive).toBe(true)
})
storeTest('loading', ({getState, dispatch}) => {
expect(getState().loading).toBe(false)
dispatch(actions.setLoading(true))
expect(getState().loading).toBe(true)
})
storeTest('error', ({getState, dispatch}) => {
expect(getState().error).toBe(false)
dispatch(actions.setError(true))
expect(getState().error).toBe(true)
})
storeTest('setTableSortMethod', ({getState, dispatch}) => {
expect(getState().tableSortMethod).toEqual({
field: SortField.SELF,
direction: SortDirection.DESCENDING,
})
dispatch(
actions.sandwichView.setTableSortMethod({
field: SortField.SYMBOL_NAME,
direction: SortDirection.ASCENDING,
}),
)
expect(getState().tableSortMethod).toEqual({
field: SortField.SYMBOL_NAME,
direction: SortDirection.ASCENDING,
})
})
-191
View File
@@ -1,191 +0,0 @@
import {actions} from './actions'
/**
* The root node of application state. We use redux (https://redux.js.org/)
* as our state management solution.
*/
import * as redux from 'redux'
import {setter, Reducer, Action} from '../lib/typed-redux'
import {HashParams, getHashParams} from '../lib/hash-params'
import {ProfileGroupState, profileGroup} from './profiles-state'
import {SortMethod, SortField, SortDirection} from '../views/profile-table-view'
import {useSelector} from '../lib/preact-redux'
import {Profile} from '../lib/profile'
import {FlamechartViewState} from './flamechart-view-state'
import {SandwichViewState} from './sandwich-view-state'
import {getProfileToView} from './getters'
export const enum ViewMode {
CHRONO_FLAME_CHART,
LEFT_HEAVY_FLAME_GRAPH,
SANDWICH_VIEW,
}
export const enum ColorScheme {
// Default: respect prefers-color-schema
SYSTEM,
// Use dark theme
DARK,
// use light theme
LIGHT,
}
export interface ApplicationState {
// The top-level profile group from which most other data will be derived
profileGroup: ProfileGroupState
// Parameters defined by the URL encoded k=v pairs after the # in the URL
hashParams: HashParams
glCanvas: HTMLCanvasElement | null
// Which top-level view should be displayed
viewMode: ViewMode
// True if recursion should be flattened when viewing flamegraphs
flattenRecursion: boolean
// The query used in top-level views
//
// An empty string indicates that the search is open by no filter is applied.
// searchIsActive is stored separately, because we may choose to persist the
// query even when the search input is closed.
searchQuery: string
searchIsActive: boolean
// True when a file drag is currently active. Used to indicate that the
// application is a valid drop target.
dragActive: boolean
// True when the application is currently in a loading state. Used to
// display a loading progress bar.
loading: boolean
// True when the application is an error state, e.g. because the profile
// imported was invalid.
error: boolean
// The table sorting method using for the sandwich view, specifying the column
// to sort by, and the direction to sort that clumn.
tableSortMethod: SortMethod
// The color scheme to use for the entire UI
colorScheme: ColorScheme
}
const protocol = window.location.protocol
// Speedscope is usable both from a local HTML file being served
// from a file:// URL, and via websites. In the case of file:// URLs,
// however, XHR will be unavailable to fetching files in adjacent directories.
export const canUseXHR = protocol === 'http:' || protocol === 'https:'
function colorScheme(state: ColorScheme | undefined, action: Action<any>): ColorScheme {
const localStorageKey = 'speedscope-color-scheme'
if (state === undefined) {
const storedPreference = window.localStorage && window.localStorage[localStorageKey]
if (storedPreference === 'DARK') {
return ColorScheme.DARK
} else if (storedPreference === 'LIGHT') {
return ColorScheme.LIGHT
} else {
return ColorScheme.SYSTEM
}
}
if (actions.setColorScheme.matches(action)) {
const value = action.payload
switch (value) {
case ColorScheme.DARK: {
window.localStorage[localStorageKey] = 'DARK'
break
}
case ColorScheme.LIGHT: {
window.localStorage[localStorageKey] = 'LIGHT'
break
}
case ColorScheme.SYSTEM: {
delete window.localStorage[localStorageKey]
break
}
default: {
const _exhaustiveCheck: never = value
return _exhaustiveCheck
}
}
return value
}
return state
}
export function createAppStore(initialState?: ApplicationState): redux.Store<ApplicationState> {
const hashParams = getHashParams()
const loading = canUseXHR && hashParams.profileURL != null
const reducer: Reducer<ApplicationState> = redux.combineReducers({
profileGroup,
hashParams: setter<HashParams>(actions.setHashParams, hashParams),
flattenRecursion: setter<boolean>(actions.setFlattenRecursion, false),
viewMode: setter<ViewMode>(actions.setViewMode, ViewMode.CHRONO_FLAME_CHART),
searchQuery: setter<string>(actions.setSearchQuery, ''),
searchIsActive: setter<boolean>(actions.setSearchIsActive, false),
glCanvas: setter<HTMLCanvasElement | null>(actions.setGLCanvas, null),
dragActive: setter<boolean>(actions.setDragActive, false),
loading: setter<boolean>(actions.setLoading, loading),
error: setter<boolean>(actions.setError, false),
tableSortMethod: setter<SortMethod>(actions.sandwichView.setTableSortMethod, {
field: SortField.SELF,
direction: SortDirection.DESCENDING,
}),
colorScheme,
})
return redux.createStore(reducer, initialState)
}
export function useAppSelector<T>(selector: (t: ApplicationState) => T, cacheArgs: any[]): T {
/* eslint-disable react-hooks/exhaustive-deps */
return useSelector(selector, cacheArgs)
}
export interface ActiveProfileState {
profile: Profile
index: number
chronoViewState: FlamechartViewState
leftHeavyViewState: FlamechartViewState
sandwichViewState: SandwichViewState
}
export function useActiveProfileState(): ActiveProfileState | null {
return useAppSelector(state => {
const {profileGroup} = state
if (!profileGroup) return null
if (profileGroup.indexToView >= profileGroup.profiles.length) return null
const index = profileGroup.indexToView
const profileState = profileGroup.profiles[index]
return {
...profileGroup.profiles[profileGroup.indexToView],
profile: getProfileToView({
profile: profileState.profile,
flattenRecursion: state.flattenRecursion,
}),
index: profileGroup.indexToView,
}
}, [])
}
-36
View File
@@ -1,36 +0,0 @@
import {storeTest, profileGroupTwoSampled} from './store-test-utils'
import {actions} from './actions'
describe('profileGroup', () => {
storeTest('setProfileGroup', ({getState, dispatch}) => {
expect(getState().profileGroup).toBe(null)
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
const {profileGroup} = getState()
expect(profileGroup!.name).toBe(profileGroupTwoSampled.name)
expect(profileGroup!.indexToView).toBe(profileGroupTwoSampled.indexToView)
for (let i = 0; i < profileGroupTwoSampled.profiles.length; i++) {
expect(profileGroup!.profiles[i].profile).toEqual(profileGroupTwoSampled.profiles[i])
}
})
storeTest('setProfileIndexToView', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
expect(getState().profileGroup!.indexToView).toBe(1)
dispatch(actions.setProfileIndexToView(0))
expect(getState()!.profileGroup!.indexToView).toBe(0)
dispatch(actions.setProfileIndexToView(-1))
expect(getState()!.profileGroup!.indexToView).toBe(0)
dispatch(actions.setProfileIndexToView(2))
expect(getState().profileGroup!.indexToView).toBe(1)
})
storeTest('preserve state', ({getState, dispatch}) => {
// When an unrelated action occurs, we should not change the identiy of the profileGroup
// property
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
const profileGroup = getState().profileGroup
dispatch(actions.setDragActive(true))
expect(getState().profileGroup).toBe(profileGroup)
})
})
-103
View File
@@ -1,103 +0,0 @@
import {Profile} from '../lib/profile'
import {
FlamechartViewState,
createFlamechartViewStateReducer,
FlamechartID,
} from './flamechart-view-state'
import {SandwichViewState, createSandwichView} from './sandwich-view-state'
import {Reducer, actionCreator, setter} from '../lib/typed-redux'
import {actions} from './actions'
import {clamp} from '../lib/math'
import {objectsHaveShallowEquality} from '../lib/utils'
export type ProfileGroupState = {
name: string
// The index within the list of profiles currently being viewed
indexToView: number
profiles: ProfileState[]
} | null
export interface ProfileWithIndex {
profile: Profile
index: number
}
export interface ProfileState {
profile: Profile
chronoViewState: FlamechartViewState
leftHeavyViewState: FlamechartViewState
sandwichViewState: SandwichViewState
}
export function actionCreatorWithIndex<T>(name: string) {
return actionCreator<{profileIndex: number; args: T}>(name)
}
function createProfileReducer(profile: Profile, index: number): Reducer<ProfileState> {
const chronoViewStateReducer = createFlamechartViewStateReducer(FlamechartID.CHRONO, index)
const leftHeavyViewStateReducer = createFlamechartViewStateReducer(FlamechartID.LEFT_HEAVY, index)
const sandwichViewStateReducer = createSandwichView(index)
return (state = undefined, action) => {
if (state === undefined) {
return {
profile,
chronoViewState: chronoViewStateReducer(undefined, action),
leftHeavyViewState: leftHeavyViewStateReducer(undefined, action),
sandwichViewState: sandwichViewStateReducer(undefined, action),
}
}
const nextState = {
profile,
chronoViewState: chronoViewStateReducer(state.chronoViewState, action),
leftHeavyViewState: leftHeavyViewStateReducer(state.leftHeavyViewState, action),
sandwichViewState: sandwichViewStateReducer(state.sandwichViewState, action),
}
if (objectsHaveShallowEquality(state, nextState)) {
return state
}
return nextState
}
}
export const profileGroup: Reducer<ProfileGroupState> = (state = null, action) => {
if (actions.setProfileGroup.matches(action)) {
const {indexToView, profiles, name} = action.payload
return {
indexToView,
name,
profiles: profiles.map((p, i) => {
return createProfileReducer(p, i)(undefined, action)
}),
}
}
if (state != null) {
const {indexToView, profiles} = state
const nextIndexToView = clamp(
setter(actions.setProfileIndexToView, 0)(indexToView, action),
0,
profiles.length - 1,
)
const nextProfiles = profiles.map((profileState, profileIndex) => {
return createProfileReducer(profileState.profile, profileIndex)(profileState, action)
})
if (indexToView === nextIndexToView && objectsHaveShallowEquality(profiles, nextProfiles)) {
return state
}
return {
...state,
indexToView: nextIndexToView,
profiles: nextProfiles,
}
}
return state
}
-70
View File
@@ -1,70 +0,0 @@
import {storeTest, profileGroupTwoSampled} from './store-test-utils'
import {actions} from './actions'
import {Vec2} from '../lib/math'
import {FlamechartID} from './flamechart-view-state'
describe('sandwich view state', () => {
storeTest('setSelectedFrame', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee).toBe(null)
const {profileGroup} = getState()
const selectedFrame = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0]
.frame
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame}))
expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.selectedFrame).toBe(
selectedFrame,
)
// Other profiles selection state should not change
expect(getState().profileGroup!.profiles[1].sandwichViewState.callerCallee).toBe(null)
// Changing selection when something is already selected should work
const selectedFrame2 = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot()
.children[0].children[0].frame
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame2}))
expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.selectedFrame).toBe(
selectedFrame2,
)
// Clear selection
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: null}))
expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee).toBe(null)
})
storeTest('contained flamegraphs receive updates', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
const {profileGroup} = getState()
const selectedFrame = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0]
.frame
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame}))
expect(
getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.calleeFlamegraph
.logicalSpaceViewportSize,
).toEqual(Vec2.zero)
dispatch(
actions.flamechart.setLogicalSpaceViewportSize({
profileIndex: 0,
args: {id: FlamechartID.SANDWICH_CALLEES, logicalSpaceViewportSize: Vec2.unit},
}),
)
expect(
getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.calleeFlamegraph
.logicalSpaceViewportSize,
).toEqual(Vec2.unit)
})
storeTest('preserve identity', ({getState, dispatch}) => {
dispatch(actions.setProfileGroup(profileGroupTwoSampled))
const {profileGroup} = getState()
const selectedFrame = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0]
.frame
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame}))
// When an unrelated action is triggered, it should not change the identity of the sandwichViewState
const sandwichViewState = getState().profileGroup!.profiles[0].sandwichViewState
dispatch(actions.setLoading(true))
expect(getState().profileGroup!.profiles[0].sandwichViewState).toBe(sandwichViewState)
})
})
-78
View File
@@ -1,78 +0,0 @@
import {Frame} from '../lib/profile'
import {
FlamechartViewState,
FlamechartID,
createFlamechartViewStateReducer,
} from './flamechart-view-state'
import {Reducer} from '../lib/typed-redux'
import {actions} from './actions'
export interface SandwichViewState {
callerCallee: CallerCalleeState | null
}
export interface CallerCalleeState {
selectedFrame: Frame
invertedCallerFlamegraph: FlamechartViewState
calleeFlamegraph: FlamechartViewState
}
export function createSandwichView(profileIndex: number): Reducer<SandwichViewState> {
const calleesReducer = createFlamechartViewStateReducer(
FlamechartID.SANDWICH_CALLEES,
profileIndex,
)
const invertedCallersReducer = createFlamechartViewStateReducer(
FlamechartID.SANDWICH_INVERTED_CALLERS,
profileIndex,
)
function applies(action: {payload: {profileIndex: number}}) {
const {payload} = action
return payload.profileIndex === profileIndex
}
return (state = {callerCallee: null}, action) => {
if (actions.sandwichView.setSelectedFrame.matches(action) && applies(action)) {
if (action.payload.args == null) {
return {
...state,
callerCallee: null,
}
} else {
return {
...state,
callerCallee: {
selectedFrame: action.payload.args,
calleeFlamegraph: calleesReducer(undefined, action),
invertedCallerFlamegraph: invertedCallersReducer(undefined, action),
},
}
}
}
const {callerCallee} = state
if (callerCallee) {
const {calleeFlamegraph, invertedCallerFlamegraph} = callerCallee
const nextCalleeFlamegraph = calleesReducer(calleeFlamegraph, action)
const nextInvertedCallerFlamegraph = invertedCallersReducer(invertedCallerFlamegraph, action)
if (
nextCalleeFlamegraph === calleeFlamegraph &&
nextInvertedCallerFlamegraph === invertedCallerFlamegraph
) {
return state
}
return {
...state,
callerCallee: {
...callerCallee,
calleeFlamegraph: nextCalleeFlamegraph,
invertedCallerFlamegraph: nextInvertedCallerFlamegraph,
},
}
}
return state
}
}
-16
View File
@@ -1,16 +0,0 @@
import * as fs from 'fs'
import {Store, AnyAction} from 'redux'
import {ApplicationState, createAppStore} from '.'
import {importSpeedscopeProfiles} from '../lib/file-format'
export function storeTest(name: string, cb: (store: Store<ApplicationState, AnyAction>) => void) {
const store = createAppStore()
test(name, () => {
cb(store)
})
}
const filepath = './sample/profiles/speedscope/0.6.0/two-sampled.speedscope.json'
const input = fs.readFileSync(filepath, 'utf8')
export const profileGroupTwoSampled = importSpeedscopeProfiles(JSON.parse(input))
+37 -32
View File
@@ -1,47 +1,52 @@
import {h} from 'preact'
import {Application} from './application'
import {getCanvasContext} from '../store/getters'
import {actions} from '../store/actions'
import {useActionCreator} from '../lib/preact-redux'
import {memo} from 'preact/compat'
import {useAppSelector, useActiveProfileState} from '../store'
import {ProfileSearchContextProvider} from './search-view'
import {getCanvasContext} from '../app-state/getters'
import {memo, useMemo} from 'preact/compat'
import {useActiveProfileState} from '../app-state/active-profile-state'
import {useTheme} from './themes/theme'
const {
setLoading,
setError,
setProfileGroup,
setDragActive,
setViewMode,
setGLCanvas,
setFlattenRecursion,
setProfileIndexToView,
} = actions
import {
dragActiveAtom,
errorAtom,
flattenRecursionAtom,
glCanvasAtom,
hashParamsAtom,
loadingAtom,
profileGroupAtom,
viewModeAtom,
} from '../app-state'
import {useAtom} from '../lib/atom'
import {ProfileSearchContextProvider} from './search-view'
import {Application} from './application'
export const ApplicationContainer = memo(() => {
const appState = useAppSelector(state => state, [])
const canvas = useAtom(glCanvasAtom)
const theme = useTheme()
const canvasContext = useAppSelector(
state => (state.glCanvas ? getCanvasContext({theme, canvas: state.glCanvas}) : null),
[theme],
)
const canvasContext = useMemo(() => (canvas ? getCanvasContext({theme, canvas}) : null), [
theme,
canvas,
])
return (
<ProfileSearchContextProvider>
<Application
activeProfileState={useActiveProfileState()}
canvasContext={canvasContext}
setGLCanvas={useActionCreator(setGLCanvas, [])}
setLoading={useActionCreator(setLoading, [])}
setError={useActionCreator(setError, [])}
setProfileGroup={useActionCreator(setProfileGroup, [])}
setDragActive={useActionCreator(setDragActive, [])}
setViewMode={useActionCreator(setViewMode, [])}
setFlattenRecursion={useActionCreator(setFlattenRecursion, [])}
setProfileIndexToView={useActionCreator(setProfileIndexToView, [])}
setGLCanvas={glCanvasAtom.set}
setLoading={loadingAtom.set}
setError={errorAtom.set}
setProfileGroup={profileGroupAtom.setProfileGroup}
setDragActive={dragActiveAtom.set}
setViewMode={viewModeAtom.set}
setFlattenRecursion={flattenRecursionAtom.set}
setProfileIndexToView={profileGroupAtom.setProfileIndexToView}
profileGroup={useAtom(profileGroupAtom)}
theme={theme}
{...appState}
flattenRecursion={useAtom(flattenRecursionAtom)}
viewMode={useAtom(viewModeAtom)}
hashParams={useAtom(hashParamsAtom)}
glCanvas={canvas}
dragActive={useAtom(dragActiveAtom)}
loading={useAtom(loadingAtom)}
error={useAtom(errorAtom)}
/>
</ProfileSearchContextProvider>
)
+25 -6
View File
@@ -7,13 +7,17 @@ import {FontFamily, FontSize, Duration} from './style'
import {importEmscriptenSymbolMap as importEmscriptenSymbolRemapper} from '../lib/emscripten'
import {SandwichViewContainer} from './sandwich-view'
import {saveToFile} from '../lib/file-format'
import {ApplicationState, ViewMode, canUseXHR, ActiveProfileState} from '../store'
import {StatelessComponent} from '../lib/typed-redux'
import {ActiveProfileState} from '../app-state/active-profile-state'
import {LeftHeavyFlamechartView, ChronoFlamechartView} from './flamechart-view-container'
import {CanvasContext} from '../gl/canvas-context'
import {Toolbar} from './toolbar'
import {importJavaScriptSourceMapSymbolRemapper} from '../lib/js-source-map'
import {Theme, withTheme} from './themes/theme'
import {ViewMode} from '../lib/view-mode'
import {canUseXHR} from '../app-state'
import {ProfileGroupState} from '../app-state/profile-group'
import {HashParams} from '../lib/hash-params'
import {StatelessComponent} from '../lib/preact-helpers'
const importModule = import('../import')
@@ -137,7 +141,7 @@ export class GLCanvas extends StatelessComponent<GLCanvasProps> {
}
}
export type ApplicationProps = ApplicationState & {
export type ApplicationProps = {
setGLCanvas: (canvas: HTMLCanvasElement | null) => void
setLoading: (loading: boolean) => void
setError: (error: boolean) => void
@@ -149,6 +153,14 @@ export type ApplicationProps = ApplicationState & {
activeProfileState: ActiveProfileState | null
canvasContext: CanvasContext | null
theme: Theme
profileGroup: ProfileGroupState
flattenRecursion: boolean
viewMode: ViewMode
hashParams: HashParams
dragActive: boolean
loading: boolean
glCanvas: HTMLCanvasElement | null
error: boolean
}
export class Application extends StatelessComponent<ApplicationProps> {
@@ -188,6 +200,10 @@ export class Application extends StatelessComponent<ApplicationProps> {
}
document.title = `${profileGroup.name} - speedscope`
if (this.props.hashParams.viewMode) {
this.props.setViewMode(this.props.hashParams.viewMode)
}
for (let profile of profileGroup.profiles) {
await profile.demangle()
}
@@ -373,6 +389,8 @@ export class Application extends StatelessComponent<ApplicationProps> {
}
onDocumentPaste = (ev: Event) => {
if (document.activeElement != null && document.activeElement.nodeName === 'INPUT') return
ev.preventDefault()
ev.stopPropagation()
@@ -398,7 +416,8 @@ export class Application extends StatelessComponent<ApplicationProps> {
}
async maybeLoadHashParamProfile() {
if (this.props.hashParams.profileURL) {
const {profileURL} = this.props.hashParams;
if (profileURL) {
if (!canUseXHR) {
alert(
`Cannot load a profile URL when loading from "${window.location.protocol}" URL protocol`,
@@ -406,8 +425,8 @@ export class Application extends StatelessComponent<ApplicationProps> {
return
}
this.loadProfile(async () => {
const response: Response = await fetch(this.props.hashParams.profileURL!)
let filename = new URL(this.props.hashParams.profileURL!).pathname
const response: Response = await fetch(profileURL)
let filename = new URL(profileURL, window.location.href).pathname
if (filename.includes('/')) {
filename = filename.slice(filename.lastIndexOf('/') + 1)
}
+8 -7
View File
@@ -11,13 +11,14 @@ import {
createGetColorBucketForFrame,
createGetCSSColorForFrame,
getFrameToColorBucket,
} from '../store/getters'
import {FlamechartID} from '../store/flamechart-view-state'
} from '../app-state/getters'
import {FlamechartWrapper} from './flamechart-wrapper'
import {useAppSelector} from '../store'
import {h} from 'preact'
import {memo} from 'preact/compat'
import {useTheme} from './themes/theme'
import {FlamechartID} from '../app-state/profile-group'
import {flattenRecursionAtom, glCanvasAtom} from '../app-state'
import {useAtom} from '../lib/atom'
const getCalleeProfile = memoizeByShallowEquality<
{
@@ -50,9 +51,9 @@ const getCalleeFlamegraphRenderer = createMemoizedFlamechartRenderer()
export const CalleeFlamegraphView = memo((ownProps: FlamechartViewContainerProps) => {
const {activeProfileState} = ownProps
const {index, profile, sandwichViewState} = activeProfileState
const flattenRecursion = useAppSelector(state => state.flattenRecursion, [])
const glCanvas = useAppSelector(state => state.glCanvas, [])
const {profile, sandwichViewState} = activeProfileState
const flattenRecursion = useAtom(flattenRecursionAtom)
const glCanvas = useAtom(glCanvasAtom)
const theme = useTheme()
if (!profile) throw new Error('profile missing')
@@ -80,7 +81,7 @@ export const CalleeFlamegraphView = memo((ownProps: FlamechartViewContainerProps
flamechartRenderer={flamechartRenderer}
canvasContext={canvasContext}
getCSSColorForFrame={getCSSColorForFrame}
{...useFlamechartSetters(FlamechartID.SANDWICH_CALLEES, index)}
{...useFlamechartSetters(FlamechartID.SANDWICH_CALLEES)}
{...callerCallee.calleeFlamegraph}
// This overrides the setSelectedNode specified in useFlamechartSettesr
setSelectedNode={noop}
+1 -1
View File
@@ -138,7 +138,7 @@ export const FlamechartSearchView = memo(() => {
selectAndZoomToMatch(result)
},
}
}, [numResults, resultIndex, searchResults, searchResults?.at, selectAndZoomToMatch])
}, [numResults, resultIndex, searchResults, selectAndZoomToMatch])
return (
<SearchView
+30 -34
View File
@@ -1,9 +1,7 @@
import {h} from 'preact'
import {FlamechartID, FlamechartViewState} from '../store/flamechart-view-state'
import {CanvasContext} from '../gl/canvas-context'
import {Flamechart} from '../lib/flamechart'
import {FlamechartRenderer, FlamechartRendererOptions} from '../gl/flamechart-renderer'
import {useActionCreator} from '../lib/preact-redux'
import {Frame, Profile, CallTreeNode} from '../lib/profile'
import {memoizeByShallowEquality} from '../lib/utils'
import {FlamechartView} from './flamechart-view'
@@ -13,13 +11,14 @@ import {
getCanvasContext,
createGetCSSColorForFrame,
getFrameToColorBucket,
} from '../store/getters'
} from '../app-state/getters'
import {Vec2, Rect} from '../lib/math'
import {actions} from '../store/actions'
import {memo} from 'preact/compat'
import {ActiveProfileState} from '../store'
import {memo, useCallback} from 'preact/compat'
import {ActiveProfileState} from '../app-state/active-profile-state'
import {FlamechartSearchContextProvider} from './flamechart-search-view'
import {Theme, useTheme} from './themes/theme'
import {FlamechartID, FlamechartViewState} from '../app-state/profile-group'
import {profileGroupAtom} from '../app-state'
interface FlamechartSetters {
setLogicalSpaceViewportSize: (logicalSpaceViewportSize: Vec2) => void
@@ -28,34 +27,31 @@ interface FlamechartSetters {
setSelectedNode: (node: CallTreeNode | null) => void
}
const {
setHoveredNode,
setLogicalSpaceViewportSize,
setConfigSpaceViewportRect,
setSelectedNode,
} = actions.flamechart
export function useFlamechartSetters(id: FlamechartID, profileIndex: number): FlamechartSetters {
export function useFlamechartSetters(id: FlamechartID): FlamechartSetters {
return {
setNodeHover: useActionCreator(
(hover: {node: CallTreeNode; event: MouseEvent} | null) =>
setHoveredNode({profileIndex, args: {id, hover}}),
[profileIndex, id],
setNodeHover: useCallback(
(hover: {node: CallTreeNode; event: MouseEvent} | null) => {
profileGroupAtom.setFlamechartHoveredNode(id, hover)
},
[id],
),
setLogicalSpaceViewportSize: useActionCreator(
(logicalSpaceViewportSize: Vec2) =>
setLogicalSpaceViewportSize({profileIndex, args: {id, logicalSpaceViewportSize}}),
[profileIndex, id],
setLogicalSpaceViewportSize: useCallback(
(logicalSpaceViewportSize: Vec2) => {
profileGroupAtom.setLogicalSpaceViewportSize(id, logicalSpaceViewportSize)
},
[id],
),
setConfigSpaceViewportRect: useActionCreator(
(configSpaceViewportRect: Rect) =>
setConfigSpaceViewportRect({profileIndex, args: {id, configSpaceViewportRect}}),
[profileIndex, id],
setConfigSpaceViewportRect: useCallback(
(configSpaceViewportRect: Rect) => {
profileGroupAtom.setConfigSpaceViewportRect(id, configSpaceViewportRect)
},
[id],
),
setSelectedNode: useActionCreator(
(selectedNode: CallTreeNode | null) =>
setSelectedNode({profileIndex, args: {id, selectedNode}}),
[profileIndex, id],
setSelectedNode: useCallback(
(selectedNode: CallTreeNode | null) => {
profileGroupAtom.setSelectedNode(id, selectedNode)
},
[id],
),
}
}
@@ -116,7 +112,7 @@ export interface FlamechartViewContainerProps {
export const ChronoFlamechartView = memo((props: FlamechartViewContainerProps) => {
const {activeProfileState, glCanvas} = props
const {index, profile, chronoViewState} = activeProfileState
const {profile, chronoViewState} = activeProfileState
const theme = useTheme()
@@ -131,7 +127,7 @@ export const ChronoFlamechartView = memo((props: FlamechartViewContainerProps) =
flamechart,
})
const setters = useFlamechartSetters(FlamechartID.CHRONO, index)
const setters = useFlamechartSetters(FlamechartID.CHRONO)
return (
<FlamechartSearchContextProvider
@@ -177,7 +173,7 @@ const getLeftHeavyFlamechartRenderer = createMemoizedFlamechartRenderer()
export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerProps) => {
const {activeProfileState, glCanvas} = ownProps
const {index, profile, leftHeavyViewState} = activeProfileState
const {profile, leftHeavyViewState} = activeProfileState
const theme = useTheme()
@@ -195,7 +191,7 @@ export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerPr
flamechart,
})
const setters = useFlamechartSetters(FlamechartID.LEFT_HEAVY, index)
const setters = useFlamechartSetters(FlamechartID.LEFT_HEAVY)
return (
<FlamechartSearchContextProvider
+8 -2
View File
@@ -12,10 +12,10 @@ import {FlamechartDetailView} from './flamechart-detail-view'
import {FlamechartPanZoomView} from './flamechart-pan-zoom-view'
import {Hovertip} from './hovertip'
import {FlamechartViewProps} from './flamechart-view-container'
import {StatelessComponent} from '../lib/typed-redux'
import {ProfileSearchContext} from './search-view'
import {FlamechartSearchView} from './flamechart-search-view'
import {getFlamechartStyle} from './flamechart-style'
import {StatelessComponent} from '../lib/preact-helpers'
export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
private getStyle() {
@@ -80,6 +80,7 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
if (!hover) return null
const {width, height, left, top} = this.container.getBoundingClientRect()
const offset = new Vec2(hover.event.clientX - left, hover.event.clientY - top)
const frame = hover.node.frame
const style = this.getStyle()
@@ -88,7 +89,12 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
<span className={css(style.hoverCount)}>
{this.formatValue(hover.node.getTotalWeight())}
</span>{' '}
{hover.node.frame.name}
{frame.name}
{frame.file ? (
<div>
{frame.file}:{frame.line}
</div>
) : undefined}
</Hovertip>
)
}
+8 -2
View File
@@ -7,8 +7,8 @@ import {FlamechartPanZoomView} from './flamechart-pan-zoom-view'
import {noop, formatPercent} from '../lib/utils'
import {Hovertip} from './hovertip'
import {FlamechartViewProps} from './flamechart-view-container'
import {StatelessComponent} from '../lib/typed-redux'
import {withTheme} from './themes/theme'
import {StatelessComponent} from '../lib/preact-helpers'
export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
private clampViewportToFlamegraph(viewportRect: Rect) {
@@ -41,13 +41,19 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
const {width, height, left, top} = this.container.getBoundingClientRect()
const offset = new Vec2(hover.event.clientX - left, hover.event.clientY - top)
const style = getStyle(this.props.theme)
const frame = hover.node.frame
return (
<Hovertip containerSize={new Vec2(width, height)} offset={offset}>
<span className={css(style.hoverCount)}>
{this.formatValue(hover.node.getTotalWeight())}
</span>{' '}
{hover.node.frame.name}
{frame.name}
{frame.file ? (
<div>
{frame.file}:{frame.line}
</div>
) : undefined}
</Hovertip>
)
}
+40 -15
View File
@@ -3,6 +3,7 @@ import {Sizes, FontSize, FontFamily, ZIndex} from './style'
import {css, StyleSheet} from 'aphrodite'
import {ComponentChildren, h} from 'preact'
import {useTheme, withTheme} from './themes/theme'
import { useCallback } from 'preact/hooks'
interface HovertipProps {
containerSize: Vec2
@@ -14,26 +15,50 @@ export function Hovertip(props: HovertipProps) {
const style = getStyle(useTheme())
const {containerSize, offset} = props
const width = containerSize.x
const height = containerSize.y
const positionStyle: {[key: string]: number} = {}
const containerWidth = containerSize.x
const containerHeight = containerSize.y
const OFFSET_FROM_MOUSE = 7
if (offset.x + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_WIDTH_MAX < width) {
positionStyle.left = offset.x + OFFSET_FROM_MOUSE
} else {
positionStyle.right = width - offset.x + 1
}
if (offset.y + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_HEIGHT_MAX < height) {
positionStyle.top = offset.y + OFFSET_FROM_MOUSE
} else {
positionStyle.bottom = height - offset.y + 1
}
const updateLocation = useCallback((el: HTMLDivElement | null) => {
if (!el) return
const clientRect = el.getBoundingClientRect()
// Place the hovertip to the right of the cursor.
let leftEdgeX = offset.x + OFFSET_FROM_MOUSE
// If this would cause it to overflow the container, align the right
// edge of the hovertip with the right edge of the container.
if (leftEdgeX + clientRect.width > containerWidth - 1) {
leftEdgeX = containerWidth - clientRect.width - 1
// If aligning the right edge overflows the container, align the left edge
// of the hovertip with the left edge of the container.
if (leftEdgeX < 1) { leftEdgeX = 1 }
}
el.style.left = `${leftEdgeX}px`
// Place the tooltip below the cursor
let topEdgeY = offset.y + OFFSET_FROM_MOUSE
// If this would cause it to overflow the container, place the hovertip
// above the cursor instead. This intentionally differs from the horizontal
// axis logic to avoid the cursor being in the middle of a hovertip when
// possible.
if (topEdgeY + clientRect.height > containerHeight - 1) {
topEdgeY = offset.y - clientRect.height - 1
// If placing the hovertip above the cursor overflows the container, align
// the top edge of the hovertip with the top edge of the container.
if (topEdgeY < 1) { topEdgeY = 1 }
}
el.style.top = `${topEdgeY}px`
}, [containerWidth, containerHeight, offset.x, offset.y])
return (
<div className={css(style.hoverTip)} style={positionStyle}>
<div className={css(style.hoverTip)} ref={updateLocation}>
<div className={css(style.hoverTipRow)}>{props.children}</div>
</div>
)
@@ -11,13 +11,14 @@ import {
createGetColorBucketForFrame,
createGetCSSColorForFrame,
getFrameToColorBucket,
} from '../store/getters'
import {FlamechartID} from '../store/flamechart-view-state'
import {useAppSelector} from '../store'
} from '../app-state/getters'
import {FlamechartWrapper} from './flamechart-wrapper'
import {h} from 'preact'
import {memo} from 'preact/compat'
import {useTheme} from './themes/theme'
import {FlamechartID} from '../app-state/profile-group'
import {flattenRecursionAtom, glCanvasAtom} from '../app-state'
import {useAtom} from '../lib/atom'
const getInvertedCallerProfile = memoizeByShallowEquality(
({
@@ -55,9 +56,9 @@ const getInvertedCallerFlamegraphRenderer = createMemoizedFlamechartRenderer({in
export const InvertedCallerFlamegraphView = memo((ownProps: FlamechartViewContainerProps) => {
const {activeProfileState} = ownProps
let {profile, sandwichViewState, index} = activeProfileState
const flattenRecursion = useAppSelector(state => state.flattenRecursion, [])
const glCanvas = useAppSelector(state => state.glCanvas, [])
let {profile, sandwichViewState} = activeProfileState
const flattenRecursion = useAtom(flattenRecursionAtom)
const glCanvas = useAtom(glCanvasAtom)
const theme = useTheme()
if (!profile) throw new Error('profile missing')
@@ -89,7 +90,7 @@ export const InvertedCallerFlamegraphView = memo((ownProps: FlamechartViewContai
flamechartRenderer={flamechartRenderer}
canvasContext={canvasContext}
getCSSColorForFrame={getCSSColorForFrame}
{...useFlamechartSetters(FlamechartID.SANDWICH_INVERTED_CALLERS, index)}
{...useFlamechartSetters(FlamechartID.SANDWICH_INVERTED_CALLERS)}
{...callerCallee.invertedCallerFlamegraph}
// This overrides the setSelectedNode specified in useFlamechartSettesr
setSelectedNode={noop}
+20 -34
View File
@@ -5,31 +5,23 @@ import {formatPercent} from '../lib/utils'
import {FontSize, Sizes, commonStyle} from './style'
import {ColorChit} from './color-chit'
import {ListItem, ScrollableListView} from './scrollable-list-view'
import {actions} from '../store/actions'
import {createGetCSSColorForFrame, getFrameToColorBucket} from '../store/getters'
import {useActionCreator} from '../lib/preact-redux'
import {useAppSelector, ActiveProfileState} from '../store'
import {createGetCSSColorForFrame, getFrameToColorBucket} from '../app-state/getters'
import {memo} from 'preact/compat'
import {useCallback, useMemo, useContext} from 'preact/hooks'
import {SandwichViewContext} from './sandwich-view'
import {Color} from '../lib/color'
import {useTheme, withTheme} from './themes/theme'
export enum SortField {
SYMBOL_NAME,
SELF,
TOTAL,
}
export enum SortDirection {
ASCENDING,
DESCENDING,
}
export interface SortMethod {
field: SortField
direction: SortDirection
}
import {
SortDirection,
SortMethod,
SortField,
profileGroupAtom,
tableSortMethodAtom,
searchIsActiveAtom,
searchQueryAtom,
} from '../app-state'
import {useAtom} from '../lib/atom'
import {ActiveProfileState} from '../app-state/active-profile-state'
interface HBarProps {
perc: number
@@ -430,28 +422,22 @@ interface ProfileTableViewContainerProps {
activeProfileState: ActiveProfileState
}
const {setTableSortMethod} = actions.sandwichView
export const ProfileTableViewContainer = memo((ownProps: ProfileTableViewContainerProps) => {
const {activeProfileState} = ownProps
const {profile, sandwichViewState, index} = activeProfileState
const {profile, sandwichViewState} = activeProfileState
if (!profile) throw new Error('profile missing')
const tableSortMethod = useAppSelector(state => state.tableSortMethod, [])
const tableSortMethod = useAtom(tableSortMethodAtom)
const theme = useTheme()
const {callerCallee} = sandwichViewState
const selectedFrame = callerCallee ? callerCallee.selectedFrame : null
const frameToColorBucket = getFrameToColorBucket(profile)
const getCSSColorForFrame = createGetCSSColorForFrame({theme, frameToColorBucket})
const setSelectedFrame = useActionCreator(
(selectedFrame: Frame | null) => {
return actions.sandwichView.setSelectedFrame({profileIndex: index, args: selectedFrame})
},
[index],
)
const setSortMethod = useActionCreator(setTableSortMethod, [])
const searchIsActive = useAppSelector(state => state.searchIsActive, [])
const searchQuery = useAppSelector(state => state.searchQuery, [])
const setSelectedFrame = useCallback((selectedFrame: Frame | null) => {
profileGroupAtom.setSelectedFrame(selectedFrame)
}, [])
const searchIsActive = useAtom(searchIsActiveAtom)
const searchQuery = useAtom(searchQueryAtom)
return (
<ProfileTableView
@@ -460,7 +446,7 @@ export const ProfileTableViewContainer = memo((ownProps: ProfileTableViewContain
getCSSColorForFrame={getCSSColorForFrame}
sortMethod={tableSortMethod}
setSelectedFrame={setSelectedFrame}
setSortMethod={setSortMethod}
setSortMethod={tableSortMethodAtom.set}
searchIsActive={searchIsActive}
searchQuery={searchQuery}
/>
+9 -18
View File
@@ -1,21 +1,21 @@
import {Frame} from '../lib/profile'
import {StyleSheet, css} from 'aphrodite'
import {ProfileTableViewContainer, SortField, SortDirection} from './profile-table-view'
import {ProfileTableViewContainer} from './profile-table-view'
import {h, JSX, createContext} from 'preact'
import {memo} from 'preact/compat'
import {useCallback, useMemo, useContext} from 'preact/hooks'
import {commonStyle, Sizes, FontSize} from './style'
import {actions} from '../store/actions'
import {StatelessComponent} from '../lib/typed-redux'
import {InvertedCallerFlamegraphView} from './inverted-caller-flamegraph-view'
import {CalleeFlamegraphView} from './callee-flamegraph-view'
import {useDispatch} from '../lib/preact-redux'
import {SandwichSearchView} from './sandwich-search-view'
import {useAppSelector, ActiveProfileState} from '../store'
import {ActiveProfileState} from '../app-state/active-profile-state'
import {sortBy} from '../lib/utils'
import {ProfileSearchContext} from './search-view'
import {FuzzyMatch} from '../lib/fuzzy-find'
import {Theme, useTheme, withTheme} from './themes/theme'
import {SortField, SortDirection, profileGroupAtom, tableSortMethodAtom} from '../app-state'
import {useAtom} from '../lib/atom'
import {StatelessComponent} from '../lib/preact-helpers'
interface SandwichViewProps {
selectedFrame: Frame | null
@@ -152,21 +152,12 @@ export const SandwichViewContainer = memo((ownProps: SandwichViewContainerProps)
const {callerCallee} = sandwichViewState
const theme = useTheme()
const dispatch = useDispatch()
const setSelectedFrame = useCallback(
(selectedFrame: Frame | null) => {
dispatch(
actions.sandwichView.setSelectedFrame({
profileIndex: index,
args: selectedFrame,
}),
)
},
[dispatch, index],
)
const setSelectedFrame = useCallback((selectedFrame: Frame | null) => {
profileGroupAtom.setSelectedFrame(selectedFrame)
}, [])
const profile = activeProfileState.profile
const tableSortMethod = useAppSelector(state => state.tableSortMethod, [])
const tableSortMethod = useAtom(tableSortMethodAtom)
const profileSearchResults = useContext(ProfileSearchContext)
const selectedFrame = callerCallee ? callerCallee.selectedFrame : null
+1 -1
View File
@@ -142,7 +142,7 @@ export const ScrollableListView = ({
</div>
</div>
)
}, [rangeResult?.invisiblePrefixSize, visibleItems, totalSize])
}, [rangeResult, visibleItems, totalSize])
return (
<div className={className} ref={viewportCallback} onScroll={onViewportScroll}>
+9 -11
View File
@@ -5,10 +5,10 @@ import {memo} from 'preact/compat'
import {Sizes, FontSize} from './style'
import {ProfileSearchResults} from '../lib/profile-search'
import {Profile} from '../lib/profile'
import {useActiveProfileState, useAppSelector} from '../store'
import {useActionCreator} from '../lib/preact-redux'
import {actions} from '../store/actions'
import {useActiveProfileState} from '../app-state/active-profile-state'
import {useTheme, withTheme} from './themes/theme'
import {searchIsActiveAtom, searchQueryAtom} from '../app-state'
import {useAtom} from '../lib/atom'
function stopPropagation(ev: Event) {
ev.stopPropagation()
@@ -19,8 +19,8 @@ export const ProfileSearchContext = createContext<ProfileSearchResults | null>(n
export const ProfileSearchContextProvider = ({children}: {children: ComponentChildren}) => {
const activeProfileState = useActiveProfileState()
const profile: Profile | null = activeProfileState ? activeProfileState.profile : null
const searchIsActive = useAppSelector(state => state.searchIsActive, [])
const searchQuery = useAppSelector(state => state.searchQuery, [])
const searchIsActive = useAtom(searchIsActiveAtom)
const searchQuery = useAtom(searchQueryAtom)
const searchResults = useMemo(() => {
if (!profile || !searchIsActive || searchQuery.length === 0) {
@@ -34,8 +34,6 @@ export const ProfileSearchContextProvider = ({children}: {children: ComponentChi
)
}
const {setSearchQuery: setSearchQueryAction, setSearchIsActive: setSearchIsActiveAction} = actions
interface SearchViewProps {
resultIndex: number | null
numResults: number | null
@@ -47,10 +45,10 @@ export const SearchView = memo(
({numResults, resultIndex, selectNext, selectPrev}: SearchViewProps) => {
const theme = useTheme()
const style = getStyle(theme)
const searchQuery = useAppSelector(state => state.searchQuery, [])
const searchIsActive = useAppSelector(state => state.searchIsActive, [])
const setSearchQuery = useActionCreator(setSearchQueryAction, [])
const setSearchIsActive = useActionCreator(setSearchIsActiveAction, [])
const searchIsActive = useAtom(searchIsActiveAtom)
const searchQuery = useAtom(searchQueryAtom)
const setSearchQuery = searchQueryAtom.set
const setSearchIsActive = searchIsActiveAtom.set
const onInput = useCallback(
(ev: Event) => {
+3 -36
View File
@@ -1,8 +1,9 @@
import {h, ComponentChildren, createContext} from 'preact'
import {useCallback, useContext, useEffect, useState} from 'preact/hooks'
import {ColorScheme, colorSchemeAtom} from '../../app-state/color-scheme'
import {useAtom} from '../../lib/atom'
import {Color} from '../../lib/color'
import {memoizeByReference} from '../../lib/utils'
import {ColorScheme, useAppSelector} from '../../store'
import {darkTheme} from './dark-theme'
import {lightTheme} from './light-theme'
@@ -58,40 +59,6 @@ export function colorSchemeToString(scheme: ColorScheme): string {
}
}
export function nextColorScheme(scheme: ColorScheme): ColorScheme {
const systemPrefersDarkMode = matchMediaDarkColorScheme().matches
// We'll use a different cycling order for changing the color scheme depending
// on what the *current* system preference is. This should guarantee that when
// a user interacts with the color scheme toggle for the first time, it always
// changes the color scheme.
if (systemPrefersDarkMode) {
switch (scheme) {
case ColorScheme.SYSTEM: {
return ColorScheme.LIGHT
}
case ColorScheme.LIGHT: {
return ColorScheme.DARK
}
case ColorScheme.DARK: {
return ColorScheme.SYSTEM
}
}
} else {
switch (scheme) {
case ColorScheme.SYSTEM: {
return ColorScheme.DARK
}
case ColorScheme.DARK: {
return ColorScheme.LIGHT
}
case ColorScheme.LIGHT: {
return ColorScheme.SYSTEM
}
}
}
}
function getTheme(colorScheme: ColorScheme, systemPrefersDarkMode: boolean) {
switch (colorScheme) {
case ColorScheme.SYSTEM: {
@@ -126,7 +93,7 @@ export function ThemeProvider(props: {children: ComponentChildren}) {
}
}, [matchMediaListener])
const colorScheme = useAppSelector(s => s.colorScheme, [])
const colorScheme = useAtom(colorSchemeAtom)
const theme = getTheme(colorScheme, systemPrefersDarkMode)
return <ThemeContext.Provider value={theme} children={props.children} />
}
+11 -14
View File
@@ -1,16 +1,17 @@
import {ApplicationProps} from './application'
import {useAppSelector, ViewMode} from '../store'
import {h, JSX, Fragment} from 'preact'
import {useCallback, useState, useEffect} from 'preact/hooks'
import {StyleSheet, css} from 'aphrodite'
import {Sizes, FontFamily, FontSize, Duration} from './style'
import {ProfileSelect} from './profile-select'
import {ProfileGroupState} from '../store/profiles-state'
import {Profile} from '../lib/profile'
import {objectsHaveShallowEquality} from '../lib/utils'
import {colorSchemeToString, nextColorScheme, useTheme, withTheme} from './themes/theme'
import {useActionCreator} from '../lib/preact-redux'
import {actions} from '../store/actions'
import {colorSchemeToString, useTheme, withTheme} from './themes/theme'
import {ViewMode} from '../lib/view-mode'
import {viewModeAtom} from '../app-state'
import {ProfileGroupState} from '../app-state/profile-group'
import {colorSchemeAtom} from '../app-state/color-scheme'
import {useAtom} from '../lib/atom'
interface ToolbarProps extends ApplicationProps {
browseForFile(): void
@@ -23,9 +24,9 @@ function useSetViewMode(setViewMode: (viewMode: ViewMode) => void, viewMode: Vie
function ToolbarLeftContent(props: ToolbarProps) {
const style = getStyle(useTheme())
const setChronoFlameChart = useSetViewMode(props.setViewMode, ViewMode.CHRONO_FLAME_CHART)
const setLeftHeavyFlameGraph = useSetViewMode(props.setViewMode, ViewMode.LEFT_HEAVY_FLAME_GRAPH)
const setSandwichView = useSetViewMode(props.setViewMode, ViewMode.SANDWICH_VIEW)
const setChronoFlameChart = useSetViewMode(viewModeAtom.set, ViewMode.CHRONO_FLAME_CHART)
const setLeftHeavyFlameGraph = useSetViewMode(viewModeAtom.set, ViewMode.LEFT_HEAVY_FLAME_GRAPH)
const setSandwichView = useSetViewMode(viewModeAtom.set, ViewMode.SANDWICH_VIEW)
if (!props.activeProfileState) return null
@@ -157,7 +158,7 @@ function ToolbarCenterContent(props: ToolbarProps): JSX.Element {
function ToolbarRightContent(props: ToolbarProps) {
const style = getStyle(useTheme())
const colorScheme = useAppSelector(s => s.colorScheme, [])
const colorScheme = useAtom(colorSchemeAtom)
const exportFile = (
<div className={css(style.toolbarTab)} onClick={props.saveFile}>
@@ -169,13 +170,9 @@ function ToolbarRightContent(props: ToolbarProps) {
<span className={css(style.emoji)}></span>Import
</div>
)
const toggleColorScheme = useActionCreator(
() => actions.setColorScheme(nextColorScheme(colorScheme)),
[colorScheme],
)
const colorSchemeToggle = (
<div className={css(style.toolbarTab)} onClick={toggleColorScheme}>
<div className={css(style.toolbarTab)} onClick={colorSchemeAtom.cycleToNextColorScheme}>
<span className={css(style.emoji)}>🎨</span>
<span className={css(style.toolbarTabColorSchemeToggle)}>
{colorSchemeToString(colorScheme)}