Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33a8f3f313 | ||
|
|
7ae545a6c3 | ||
|
|
48d692c2a3 | ||
|
|
ca8fcb48cc | ||
|
|
63f3bc0395 | ||
|
|
1ac88cc09a | ||
|
|
9a2c2a270b | ||
|
|
229d48eca5 | ||
|
|
103db681d2 | ||
|
|
21167e69d8 | ||
|
|
e37f6fa7c3 | ||
|
|
6d02bf510f | ||
|
|
b71cef5db4 | ||
|
|
e6351a3c22 | ||
|
|
36aebfbda6 | ||
|
|
246fc3dd5d | ||
|
|
d6f5efa06c | ||
|
|
03a5104317 | ||
|
|
7dcfab1bbc | ||
|
|
24b60dfd4a |
@@ -3,7 +3,7 @@ name: Node.js CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
+2
-1
@@ -2,4 +2,5 @@ node_modules
|
||||
.cache
|
||||
dist
|
||||
.idea
|
||||
coverage
|
||||
coverage
|
||||
.vscode
|
||||
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,126 @@
|
||||
简体中文 | [English](./README.md)
|
||||
# 🔬speedscope
|
||||
|
||||
一个快速,交互式,基于网络的性能分析工具。 [FlameGraphs][1](火焰图)的另一个替代品。它可以轻松显示数兆的配置文件并且不会使浏览器崩溃。
|
||||
|
||||
给定原始分析数据,你就可以交互式的探索数据,了解应用程序中什么部分速度较慢,或者分配所有内存,或者对任何数据进行分析。
|
||||
|
||||

|
||||
|
||||
[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
|
||||

|
||||
|
||||
在 "Time Order" 页面 (默认),调用堆栈按照它们在输入文件中出现的顺序从左到右排列,这通常是安排它们被记录的时间顺序。这个视图对于理解应用程序随时间变化的行为非常有帮助,比如 "首次从数据库获取到数据,然后为序列化准备数据,数据被序列化为JSON"。
|
||||
|
||||
水平轴表示每个堆栈的“权重”(最常见的是CPU时间),垂直轴显示在运行期间处于活动状态的堆栈。如果你点击其中一个框,你将能够看到关于它的统计摘要。
|
||||
|
||||
|
||||
### ⬅️Left Heavy
|
||||

|
||||
|
||||
在 "Left Heavy" 页面,将相同的堆栈分组在一,不管它们是否按顺序记录。然后,对堆栈进行排序,使每个父堆栈中最重的堆栈位于左侧——因此称为“左权重”。 这个视图对于理解在其他调用栈之间有成百上千个函数交错调用的情况下,时间都花费在了哪里很有用。
|
||||
|
||||
### 🥪 Sandwich
|
||||

|
||||
|
||||
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) 关于设置开发环境的说明。
|
||||
@@ -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
|
||||

|
||||
|
||||
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
@@ -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`)
|
||||
|
||||
Generated
+15377
-143
File diff suppressed because it is too large
Load Diff
+6
-7
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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"`;
|
||||
|
||||
@@ -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
@@ -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",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)",
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import {Component} from 'preact'
|
||||
|
||||
export type VoidState = {
|
||||
__dummyField: void
|
||||
}
|
||||
export abstract class StatelessComponent<P> extends Component<P, VoidState> {}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
})
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export const enum ViewMode {
|
||||
CHRONO_FLAME_CHART,
|
||||
LEFT_HEAVY_FLAME_GRAPH,
|
||||
SANDWICH_VIEW,
|
||||
}
|
||||
+3
-11
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -138,7 +138,7 @@ export const FlamechartSearchView = memo(() => {
|
||||
selectAndZoomToMatch(result)
|
||||
},
|
||||
}
|
||||
}, [numResults, resultIndex, searchResults, searchResults?.at, selectAndZoomToMatch])
|
||||
}, [numResults, resultIndex, searchResults, selectAndZoomToMatch])
|
||||
|
||||
return (
|
||||
<SearchView
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -142,7 +142,7 @@ export const ScrollableListView = ({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [rangeResult?.invisiblePrefixSize, visibleItems, totalSize])
|
||||
}, [rangeResult, visibleItems, totalSize])
|
||||
|
||||
return (
|
||||
<div className={className} ref={viewportCallback} onScroll={onViewportScroll}>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user