Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a4f38a8cb | ||
|
|
9361a6baf2 | ||
|
|
ede9c74d50 | ||
|
|
f758130455 | ||
|
|
f3a1c09c9b | ||
|
|
069c0194a6 | ||
|
|
64fe369c42 | ||
|
|
f55c53f699 | ||
|
|
a0b3fe8420 | ||
|
|
9452aeae82 | ||
|
|
b26cdb5be4 | ||
|
|
1c5bdba36e | ||
|
|
c3b35d7b0f | ||
|
|
dfaefe54fd | ||
|
|
7514f4c0c9 | ||
|
|
ff447c2719 | ||
|
|
668bb032ba | ||
|
|
9ed1eb192c | ||
|
|
8620432cbc | ||
|
|
dead3f9ad9 | ||
|
|
80b747a55e | ||
|
|
351994972d | ||
|
|
ca1abfdd32 | ||
|
|
dee9e5ade4 | ||
|
|
2077a905a9 | ||
|
|
e969178e65 | ||
|
|
56f6459af9 | ||
|
|
3f79e0fe96 | ||
|
|
d30bb2ef7e | ||
|
|
fd4195da10 | ||
|
|
707462e9cf | ||
|
|
375040e892 | ||
|
|
5ae9abcf1d | ||
|
|
bdd9301c59 | ||
|
|
cc9750923a | ||
|
|
c3074b7343 | ||
|
|
b15a08b3ff | ||
|
|
68683aa054 | ||
|
|
eb0e1ce731 | ||
|
|
bd546ca893 | ||
|
|
0c1e477f35 | ||
|
|
30ca6291ca | ||
|
|
66a9e5d1cf | ||
|
|
a2022a07a2 | ||
|
|
b7806a1c5f | ||
|
|
7f19a13012 | ||
|
|
abd74be9fa | ||
|
|
c706bdfe04 | ||
|
|
cfc8fe8f6e | ||
|
|
ddc61302e8 | ||
|
|
864c065053 | ||
|
|
7cca1a76bc | ||
|
|
8c574d1c92 | ||
|
|
fbc8946caa | ||
|
|
8cddf3fe81 | ||
|
|
c15ca263d3 |
+11
-4
@@ -1,13 +1,20 @@
|
||||
module.exports = {
|
||||
parser: 'typescript-eslint-parser',
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
plugins: ['prettier'],
|
||||
plugins: ['prettier', '@typescript-eslint', 'react-hooks'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': [
|
||||
'error',
|
||||
{
|
||||
additionalHooks: '(useSelector|useAppSelector|useActionCreator)',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Coveralls Parallel
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
flag-name: run-${{ matrix.node-version }}
|
||||
parallel: true
|
||||
|
||||
finish:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
parallel-finished: true
|
||||
@@ -1,3 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '9'
|
||||
+126
-31
@@ -1,120 +1,215 @@
|
||||
## Unreleased
|
||||
|
||||
## [1.10.0] - 2020-09-29
|
||||
|
||||
### Added
|
||||
|
||||
- Support for importing profiles from Safari [[#300](https://github.com/jlfwong/speedscope/pull/300)] (by [@radex](https://github.com/radex))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed browser not opening on Windows when using the CLI [[#307](https://github.com/jlfwong/speedscope/pull/307)] (by [@spillerrec](https://github.com/spillerrec))
|
||||
- Fixed import of UTF-16 encoded files w/ BOM [[#314](https://github.com/jlfwong/speedscope/pull/314)]
|
||||
- Removed accidental dependency on React [[#315](https://github.com/jlfwong/speedscope/pull/315)]
|
||||
|
||||
## [1.9.0] - 2020-08-05
|
||||
|
||||
### Added
|
||||
|
||||
- Provide prev/next buttons to cycle through search results, make search results more visually prominen [[#304](https://github.com/jlfwong/speedscope/pull/304)]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix accumulated errors in Chrome profile imports caused by zeroed negative timeDeltas [[#305](https://github.com/jlfwong/speedscope/pull/305)] (by [@taneliang](https://github.com/taneliang))
|
||||
|
||||
## [1.8.0] - 2020-07-19
|
||||
|
||||
### Added
|
||||
|
||||
- Added search highlighting in time order & left heavy views [[#297](https://github.com/jlfwong/speedscope/pull/297)]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix performance issues for the caller/callee flamegraphs in the sandwich view [[#296](https://github.com/jlfwong/speedscope/pull/296)]
|
||||
|
||||
## [1.7.0] - 2020-07-13
|
||||
|
||||
### Added
|
||||
|
||||
- Introduced filtering via Ctrl+F/Cmd+F into the sandwich view [[#293](https://github.com/jlfwong/speedscope/pull/293)]
|
||||
|
||||
## [1.6.0] - 2020-05-30
|
||||
|
||||
### Added
|
||||
|
||||
- Improved profile/thread selection UI [[#282](https://github.com/jlfwong/speedscope/pull/282)]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Crash instead of incorrectly interpreting profiles with incorrectly ordered events [[#273](https://github.com/jlfwong/speedscope/pull/273)]
|
||||
- A large refactor to upgrade to Preact X was performed [[#267](https://github.com/jlfwong/speedscope/pull/267)]
|
||||
|
||||
## [1.5.3] - 2020-01-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bump dependency versions to unbreak build [[#253](https://github.com/jlfwong/speedscope/pull/253)] (by [@jlfwong](https://github.com/jlfwong), with changes from [@Archerlly](https://github.com/Archerlly)'s [#215](https://github.com/jlfwong/speedscope/pull/215))
|
||||
- Trace event: Prevent event re-ordering from generating incorrect flamegraphs ([#252](https://github.com/jlfwong/speedscope/pull/252), with changes from [@hwajaywang](https://github.com/hwajaywang)'s [#249](https://github.com/jlfwong/speedscope/pull/249))
|
||||
- Make tooltip width wider [[#239](https://github.com/jlfwong/speedscope/pull/239)] (by [@miso11](https://github.com/miso11))
|
||||
|
||||
## [1.5.2] - 2019-10-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix emscripten remapping when symbols are hex-escaped, like `a\20b` [[#233](https://github.com/jlfwong/speedscope/pull/233)] (by [@jyc](https://github.com/jyc))
|
||||
|
||||
## [1.5.1] - 2019-06-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed import of trace event files which contain unmatched "E" events ([#222](https://github.com/jlfwong/speedscope/pull/222)) (by [@jlfwong](https://github.com/jlfwong))
|
||||
|
||||
## [1.5.0] - 2019-02-17
|
||||
|
||||
### Added
|
||||
|
||||
- Support importing unterminated JSON in simple cases ([#208](https://github.com/jlfwong/speedscope/pull/208)) (by [@jlfwong](https://github.com/jlfwong))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix crash when importing from stackprof without raw_timestamp_deltas ([#207](https://github.com/jlfwong/speedscope/pull/207)) (by [@jlfwong](https://github.com/jlfwong))
|
||||
- Alert instead of crash when importing a file containing no profiles ([#205](https://github.com/jlfwong/speedscope/pull/205)) (by [@jlfwong](https://github.com/jlfwong))
|
||||
- Fixed import of multithreaded profiles from Chrome 66 ([#206](https://github.com/jlfwong/speedscope/pull/206)) (by [@jlfwong](https://github.com/jlfwong))
|
||||
- Fixed import of instruments trace files with missing run number ([#203](https://github.com/jlfwong/speedscope/pull/203)) (by [@Archerlly](https://github.com/Archerlly))
|
||||
|
||||
## [1.4.1] - 2019-01-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix importing of Trace Event Format files with no ts field on M events [[#198](https://github.com/jlfwong/speedscope/pull/198)] (by [@jlfwong](https://github.com/jlfwong))
|
||||
|
||||
## [1.4.0] - 2019-01-22
|
||||
|
||||
### Added
|
||||
|
||||
- Import v8 cpu profile (old format) [[#177](https://github.com/jlfwong/speedscope/pull/177)] (by [@vmarchaud](https://github.com/vmarchaud))
|
||||
- Import basic "Trace Event Format" profiles [[#197](https://github.com/jlfwong/speedscope/pull/197)] (by [@jlfwong](https://github.com/jlfwong))
|
||||
|
||||
## [1.3.2] - 2018-12-03
|
||||
|
||||
* Fixed import of multithreaded Chrome profiles [#19] (by @jlfwong)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed import of multithreaded Chrome profiles [[#19](https://github.com/jlfwong/speedscope/pull/19)] (by [@jlfwong](https://github.com/jlfwong))
|
||||
|
||||
## [1.3.1] - 2018-11-08
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed a file import performance regression by using TextDecoder [#188] (by @jlfwong)
|
||||
- Fixed a file import performance regression by using TextDecoder [[#188](https://github.com/jlfwong/speedscope/pull/188)] (by [@jlfwong](https://github.com/jlfwong))
|
||||
|
||||
## [1.3.0] - 2018-10-29
|
||||
|
||||
### Added
|
||||
|
||||
* Support import from Haskell GHC JSON format support [#183] (by @trishume)
|
||||
- Support import from Haskell GHC JSON format support [[#183](https://github.com/jlfwong/speedscope/pull/183)] (by [@trishume](https://github.com/trishume))
|
||||
|
||||
### Fixed
|
||||
|
||||
* Make the wasd keymappings work on azerty keyboards [#184] (by @vrischmann)
|
||||
* Fix import of binary formats via profileURL [#179] (by @f-hj)
|
||||
- Make the wasd keymappings work on azerty keyboards [[#184](https://github.com/jlfwong/speedscope/pull/184)] (by [@vrischmann](https://github.com/vrischmann))
|
||||
- Fix import of binary formats via profileURL [[#179](https://github.com/jlfwong/speedscope/pull/179)] (by [@f](https://github.com/f)-hj)
|
||||
|
||||
## [1.2.0] - 2018-10-08
|
||||
|
||||
### Added
|
||||
|
||||
* Add import of v8 heap allocation profile [#170] (by @vmarchaud)
|
||||
- Add import of v8 heap allocation profile [[#170](https://github.com/jlfwong/speedscope/pull/170)] (by [@vmarchaud](https://github.com/vmarchaud))
|
||||
|
||||
## [1.1.0] - 2018-09-26
|
||||
|
||||
### Added
|
||||
|
||||
* Add go tool pprof import support [#165]
|
||||
- Add go tool pprof import support [[#165](https://github.com/jlfwong/speedscope/pull/165)]
|
||||
|
||||
## [1.0.4] - 2018-09-12
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix import from Chrome < 69 when there are multiple profiles [#161]
|
||||
- Fix import from Chrome < 69 when there are multiple profiles [[#161](https://github.com/jlfwong/speedscope/pull/161)]
|
||||
|
||||
## [1.0.3] - 2018-09-10
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix import for Chrome 69, support leading idle time before first call [#160]
|
||||
- Fix import for Chrome 69, support leading idle time before first call [[#160](https://github.com/jlfwong/speedscope/pull/160)]
|
||||
|
||||
## [1.0.2] - 2018-09-04
|
||||
|
||||
### Fixed
|
||||
|
||||
* Allow optional CR before LF when probing collapsed stacks files [#154]
|
||||
* Fix import for Firefox 63 [#156]
|
||||
* Change time formatting for minutes from 1.50min to 1:30 [#153] (by @Alex-Diez)
|
||||
- Allow optional CR before LF when probing collapsed stacks files [[#154](https://github.com/jlfwong/speedscope/pull/154)]
|
||||
- Fix import for Firefox 63 [[#156](https://github.com/jlfwong/speedscope/pull/156)]
|
||||
- Change time formatting for minutes from 1.50min to 1:30 [[#153](https://github.com/jlfwong/speedscope/pull/153)] (by [@Alex](https://github.com/Alex)-Diez)
|
||||
|
||||
## [1.0.1] - 2018-08-23
|
||||
|
||||
* Fixed an issue where flamegraph bounds were not always being cleared correctly, leading to visual artifacts [#150]
|
||||
- Fixed an issue where flamegraph bounds were not always being cleared correctly, leading to visual artifacts [[#150](https://github.com/jlfwong/speedscope/pull/150)]
|
||||
|
||||
## [1.0.0] - 2018-08-23
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed rendering issues when switching between screens w/ different `devicePixelRatios` [#147]
|
||||
- Fixed rendering issues when switching between screens w/ different `devicePixelRatios` [[#147](https://github.com/jlfwong/speedscope/pull/147)]
|
||||
|
||||
## [0.7.1] - 2018-08-20
|
||||
|
||||
### Fixed
|
||||
|
||||
* Removed dependency on regl in order to allow speedscope to run in strict content-security-policy environments [#140]
|
||||
* Fixed text culling bug [#143]
|
||||
- Removed dependency on regl in order to allow speedscope to run in strict content-security-policy environments [[#140](https://github.com/jlfwong/speedscope/pull/140)]
|
||||
- Fixed text culling bug [[#143](https://github.com/jlfwong/speedscope/pull/143)]
|
||||
|
||||
## [0.7.0] - 2018-08-16
|
||||
|
||||
### Added
|
||||
|
||||
* Added support to import from linux `perf script` [#135]
|
||||
- Added support to import from linux `perf script` [[#135](https://github.com/jlfwong/speedscope/pull/135)]
|
||||
|
||||
## [0.6.0] - 2018-08-14
|
||||
|
||||
### Added
|
||||
|
||||
* Added support for multiple threads/processes [#130]
|
||||
* Import all runs & threads from Instruments .trace files instead of just main thread from selected run [#130]
|
||||
- Added support for multiple threads/processes [[#130](https://github.com/jlfwong/speedscope/pull/130)]
|
||||
- Import all runs & threads from Instruments .trace files instead of just main thread from selected run [[#130](https://github.com/jlfwong/speedscope/pull/130)]
|
||||
|
||||
### Fixed
|
||||
|
||||
* Ensure the JSON schema has actual contents [#133]
|
||||
- Ensure the JSON schema has actual contents [[#133](https://github.com/jlfwong/speedscope/pull/133)]
|
||||
|
||||
## [0.5.1] - 2018-08-09
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed broken CLI
|
||||
- Fixed broken CLI
|
||||
|
||||
## [0.5.0] - 2018-08-09
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix emscripten remapping when symbols contain dashes, like `527:i32s-div` [#129]
|
||||
* Improved firefox import speed and fixed bugs in it [#128]
|
||||
* Prevent non-contiguous blocks in the time ordered flamechart from appearing as a single node for selection [#123]
|
||||
* Prevent dragging from changing selection [#122]
|
||||
* Clamp zoom to prevent floating point issues [#121]
|
||||
* Preserve view state when switching tabs [#100]
|
||||
- Fix emscripten remapping when symbols contain dashes, like `527:i32s-div` [[#129](https://github.com/jlfwong/speedscope/pull/129)]
|
||||
- Improved firefox import speed and fixed bugs in it [[#128](https://github.com/jlfwong/speedscope/pull/128)]
|
||||
- Prevent non-contiguous blocks in the time ordered flamechart from appearing as a single node for selection [[#123](https://github.com/jlfwong/speedscope/pull/123)]
|
||||
- Prevent dragging from changing selection [[#122](https://github.com/jlfwong/speedscope/pull/122)]
|
||||
- Clamp zoom to prevent floating point issues [[#121](https://github.com/jlfwong/speedscope/pull/121)]
|
||||
- Preserve view state when switching tabs [[#100](https://github.com/jlfwong/speedscope/pull/100)]
|
||||
|
||||
## [0.4.0] - 2018-07-21
|
||||
|
||||
### Added
|
||||
|
||||
* Support for importing v8 logs from node [#98]
|
||||
* Optionally read from stdin via cli [#99]
|
||||
- Support for importing v8 logs from node [[#98](https://github.com/jlfwong/speedscope/pull/98)]
|
||||
- Optionally read from stdin via cli [[#99](https://github.com/jlfwong/speedscope/pull/99)]
|
||||
|
||||
## [0.3.0] - 2018-07-18
|
||||
|
||||
### Added
|
||||
|
||||
* Support for remapping profiles using a wasm symbol file [#93]
|
||||
- Support for remapping profiles using a wasm symbol file [[#93](https://github.com/jlfwong/speedscope/pull/93)]
|
||||
|
||||
@@ -36,15 +36,24 @@ speedscope is designed to ingest profiles from a variety of different profilers
|
||||
- JavaScript
|
||||
- [Importing from Chrome](https://github.com/jlfwong/speedscope/wiki/Importing-from-Chrome)
|
||||
- [Importing from Firefox](https://github.com/jlfwong/speedscope/wiki/Importing-from-Firefox)
|
||||
- [Importing from Safari](https://github.com/jlfwong/speedscope/wiki/Importing-from-Safari)
|
||||
- [Importing from Node.js](https://github.com/jlfwong/speedscope/wiki/Importing-from-Node.js)
|
||||
- Ruby
|
||||
- [Importing from stackprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-stackprof-(ruby))
|
||||
- [Importing from rbspy](https://github.com/jlfwong/speedscope/wiki/Importing-from-rbspy-(ruby))
|
||||
- [Importing from ruby-prof](https://github.com/jlfwong/speedscope/wiki/Importing-from-ruby-prof)
|
||||
- Python
|
||||
- [Importing from py-spy](https://github.com/jlfwong/speedscope/wiki/Importing-from-py-spy-(python))
|
||||
- [pyspeedscope](https://github.com/windelbouwman/pyspeedscope)
|
||||
- [Importing from Austin](https://github.com/p403n1x87/austin#speedscope)
|
||||
- Go
|
||||
- [Importing from pprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-pprof-(go))
|
||||
- Rust
|
||||
- [flamescope](https://github.com/coolreader18/flamescope)
|
||||
- Native code
|
||||
- [Importing from Instruments.app](https://github.com/jlfwong/speedscope/wiki/Importing-from-Instruments.app) (macOS)
|
||||
- [Importing from `perf`](https://github.com/jlfwong/speedscope/wiki/Importing-from-perf-(linux)) (linux)
|
||||
- [Importing from .NET Core](https://github.com/jlfwong/speedscope/wiki/Importing-from-.NET-Core)
|
||||
- [Importing from GHC (Haskell)](https://github.com/jlfwong/speedscope/wiki/Importing-from-Haskell)
|
||||
- [Importing from custom sources](https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources)
|
||||
|
||||
@@ -108,6 +117,8 @@ Once a profile has loaded, the main view is split into two: the top area is the
|
||||
* `Cmd+O`/`Ctrl+O` to open a new profile
|
||||
* `n`: Go to next profile/thread if one is available
|
||||
* `p`: Go to previous profile/thread if one is available
|
||||
* `t`: Open the profile/thread selector if available
|
||||
* `Cmd+F`/`Ctrl+F`: to open search. While open, `Enter` and `Shift+Enter` cycle through results
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
+6
-2
@@ -4,7 +4,7 @@ const fs = require('fs')
|
||||
const os = require('os')
|
||||
const stream = require('stream')
|
||||
|
||||
const opn = require('opn')
|
||||
const open = require('open')
|
||||
|
||||
const helpString = `Usage: speedscope [filepath]
|
||||
|
||||
@@ -89,7 +89,11 @@ async function main() {
|
||||
|
||||
console.log('Opening', urlToOpen, 'in your default browser')
|
||||
|
||||
await opn(urlToOpen, {wait: false})
|
||||
// We'd like to avoid blocking the terminal on the browsing closing,
|
||||
// but for some reason this doesn't work at all on Windows if we
|
||||
// don't use wait: true.
|
||||
const wait = process.platform === "win32";
|
||||
await open(urlToOpen, {wait})
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Generated
+7050
-8903
File diff suppressed because it is too large
Load Diff
+42
-20
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "speedscope",
|
||||
"version": "1.3.2",
|
||||
"version": "1.10.0",
|
||||
"description": "",
|
||||
"repository": "jlfwong/speedscope",
|
||||
"main": "index.js",
|
||||
@@ -13,48 +13,70 @@
|
||||
"prettier": "prettier --write 'src/**/*.ts' 'src/**/*.tsx'",
|
||||
"lint": "eslint 'src/**/*.ts' 'src/**/*.tsx'",
|
||||
"jest": "./scripts/test-setup.sh && jest --runInBand",
|
||||
"coverage": "npm run jest -- --coverage && coveralls < coverage/lcov.info",
|
||||
"test": "tsc --noEmit && npm run lint && npm run coverage",
|
||||
"coverage": "npm run jest -- --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "./scripts/ci.sh",
|
||||
"serve": "parcel assets/index.html --open --no-autoinstall"
|
||||
},
|
||||
"files": ["bin/cli.js", "dist/release/**", "!*.map"],
|
||||
"browserslist": ["last 2 Chrome versions", "last 2 Firefox versions"],
|
||||
"files": [
|
||||
"bin/cli.js",
|
||||
"dist/release/**",
|
||||
"!*.map"
|
||||
],
|
||||
"browserslist": [
|
||||
"last 2 Chrome versions",
|
||||
"last 2 Firefox versions"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "22.2.3",
|
||||
"@types/jszip": "3.1.4",
|
||||
"@types/node": "10.1.4",
|
||||
"@types/node": "14.0.1",
|
||||
"@types/pako": "1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "2.33.0",
|
||||
"@typescript-eslint/parser": "2.33.0",
|
||||
"acorn": "7.2.0",
|
||||
"aphrodite": "2.1.0",
|
||||
"coveralls": "3.0.1",
|
||||
"eslint": "4.19.1",
|
||||
"eslint": "6.0.0",
|
||||
"eslint-plugin-prettier": "2.6.0",
|
||||
"jest": "23.0.1",
|
||||
"eslint-plugin-react-hooks": "4.0.2",
|
||||
"jest": "24.3.0",
|
||||
"jsverify": "0.8.3",
|
||||
"jszip": "3.1.5",
|
||||
"pako": "1.0.6",
|
||||
"parcel-bundler": "1.9.2",
|
||||
"preact": "8.2.7",
|
||||
"preact-redux": "jlfwong/preact-redux#a56dcc4",
|
||||
"prettier": "1.12.0",
|
||||
"parcel-bundler": "1.12.4",
|
||||
"preact": "10.4.1",
|
||||
"prettier": "2.0.4",
|
||||
"protobufjs": "6.8.8",
|
||||
"quicktype": "15.0.45",
|
||||
"redux": "^4.0.0",
|
||||
"ts-jest": "22.4.6",
|
||||
"typescript": "2.8.1",
|
||||
"redux": "^4.0.5",
|
||||
"ts-jest": "24.3.0",
|
||||
"typescript": "3.9.2",
|
||||
"typescript-eslint-parser": "17.0.1",
|
||||
"typescript-json-schema": "0.42.0",
|
||||
"uglify-es": "3.2.2"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"setupFilesAfterEnv": [
|
||||
"./src/jest-setup.js"
|
||||
],
|
||||
"testRegex": "\\.test\\.tsx?$",
|
||||
"collectCoverageFrom": ["**/*.{ts,tsx}", "!**/*.d.{ts,tsx}"],
|
||||
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"]
|
||||
"collectCoverageFrom": [
|
||||
"**/*.{ts,tsx}",
|
||||
"!**/*.d.{ts,tsx}"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx",
|
||||
"json"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"opn": "5.3.0"
|
||||
"open": "7.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -4,4 +4,5 @@ module.exports = {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
||||
arrowParens: 'avoid'
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"head":{"functionName":"(root)","url":"","lineNumber":-1,"bailoutReason":"","id":1,"scriptId":0,"hitCount":0,"children":[{"functionName":"","url":"","lineNumber":0,"callUID":1,"bailoutReason":"","id":2,"scriptId":0,"hitCount":0,"children":[{"functionName":"a","url":"","lineNumber":0,"callUID":2,"bailoutReason":"","id":3,"scriptId":0,"hitCount":0,"children":[{"functionName":"b","url":"","lineNumber":5,"callUID":3,"bailoutReason":"","id":4,"scriptId":0,"hitCount":0,"children":[{"functionName":"d","url":"","lineNumber":13,"callUID":4,"bailoutReason":"","id":5,"scriptId":0,"hitCount":14,"children":[]}]},{"functionName":"c","url":"","lineNumber":9,"callUID":3,"bailoutReason":"","id":6,"scriptId":0,"hitCount":0,"children":[{"functionName":"d","url":"","lineNumber":13,"callUID":6,"bailoutReason":"","id":7,"scriptId":0,"hitCount":14,"children":[]}]}]}]}]},"startTime":163140,"endTime":163140,"samples":[2,5,5,5,5,5,5,5,5,5,5,5,5,5,5,7,7,7,7,7,7,7,7,7,7,7,7,7,7],"timestamps":[163140599286,163140610861,163140611921,163140612966,163140614236,163140615507,163140616694,163140617968,163140619238,163140620258,163140621507,163140622765,163140624037,163140625303,163140626378,163140627649,163140628923,163140630191,163140631457,163140632746,163140634032,163140635304,163140636440,163140637716,163140638990,163140640255,163140641520,163140642791,163140644063,163140645206]}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://www.speedscope.app/file-format-schema.json",
|
||||
"shared": {
|
||||
"frames": [{"name": "A"}]
|
||||
},
|
||||
"profiles": [
|
||||
{
|
||||
"type": "evented",
|
||||
"name": "p1",
|
||||
"unit": "none",
|
||||
"startValue": 0,
|
||||
"endValue": 100,
|
||||
"events": [{"type": "O", "frame": 0, "at": 0}]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://www.speedscope.app/file-format-schema.json",
|
||||
"shared": {
|
||||
"frames": [{"name": "A"}, {"name": "B"}]
|
||||
},
|
||||
"profiles": [
|
||||
{
|
||||
"type": "evented",
|
||||
"name": "p1",
|
||||
"unit": "none",
|
||||
"startValue": 0,
|
||||
"endValue": 100,
|
||||
"events": [
|
||||
{"type": "O", "frame": 0, "at": 0},
|
||||
{"type": "C", "frame": 0, "at": 1},
|
||||
{"type": "O", "frame": 1, "at": 2},
|
||||
{"type": "O", "frame": 0, "at": 2},
|
||||
{"type": "O", "frame": 0, "at": 3},
|
||||
{"type": "C", "frame": 0, "at": 4},
|
||||
{"type": "C", "frame": 1, "at": 4},
|
||||
{"type": "C", "frame": 0, "at": 5}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "M", "name": "process_name", "args": {"name": "p0"}},
|
||||
{"pid": 0, "tid": 0, "ph": "M", "name": "thread_name", "args": {"name": "p0t0"}},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "alpha", "ts": 0, "dur": 1},
|
||||
{"pid": 0, "tid": 1, "ph": "M", "name": "thread_name", "args": {"name": "p0t1"}},
|
||||
{"pid": 0, "tid": 1, "ph": "X", "name": "beta", "ts": 0, "dur": 1},
|
||||
|
||||
{"pid": 1, "tid": 0, "ph": "M", "name": "process_name", "args": {"name": "p1"}},
|
||||
{"pid": 1, "tid": 0, "ph": "M", "name": "thread_name", "args": {"name": "p1t0"}},
|
||||
{"pid": 1, "tid": 0, "ph": "X", "name": "gamma", "ts": 0, "dur": 1},
|
||||
{"pid": 1, "tid": 1, "ph": "M", "name": "thread_name", "args": {"name": "p1t1"}},
|
||||
{"pid": 1, "tid": 1, "ph": "X", "name": "delta", "ts": 0, "dur": 1},
|
||||
|
||||
{"pid": 2, "tid": 0, "ph": "M", "name": "thread_name", "args": {"name": "p2t0"}},
|
||||
{"pid": 2, "tid": 0, "ph": "X", "name": "epsilon", "ts": 0, "dur": 1},
|
||||
{"pid": 2, "tid": 1, "ph": "M", "name": "thread_name", "args": {"name": "p2t1"}},
|
||||
{"pid": 2, "tid": 1, "ph": "X", "name": "phi", "ts": 0, "dur": 1},
|
||||
|
||||
{"pid": 3, "tid": 0, "ph": "M", "name": "process_name", "args": {"name": "p3"}},
|
||||
{"pid": 3, "tid": 0, "ph": "X", "name": "zeta", "ts": 0, "dur": 1},
|
||||
{"pid": 3, "tid": 1, "ph": "X", "name": "eta", "ts": 0, "dur": 1}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{"tid": 1, "ph": "B", "pid": 0, "name": "A", "ts": 0},
|
||||
{"tid": 1, "ph": "B", "pid": 0, "name": "B", "ts": 0},
|
||||
{"tid": 1, "ph": "B", "pid": 0, "name": "C", "ts": 0},
|
||||
{"tid": 1, "ph": "E", "pid": 0, "name": "C", "ts": 1},
|
||||
{"tid": 1, "ph": "E", "pid": 0, "name": "B", "ts": 2},
|
||||
{"tid": 1, "ph": "E", "pid": 0, "name": "A", "ts": 3},
|
||||
{"tid": 1, "ph": "B", "pid": 0, "name": "A", "ts": 4},
|
||||
{"tid": 1, "ph": "B", "pid": 0, "name": "B", "ts": 5},
|
||||
{"tid": 1, "ph": "B", "pid": 0, "name": "C", "ts": 6},
|
||||
{"tid": 1, "ph": "E", "pid": 0, "name": "C", "ts": 7},
|
||||
{"tid": 1, "ph": "E", "pid": 0, "name": "B", "ts": 7},
|
||||
{"tid": 1, "ph": "E", "pid": 0, "name": "A", "ts": 7},
|
||||
{"tid": 1, "ph": "B", "pid": 0, "name": "X", "ts": 7},
|
||||
{"tid": 1, "ph": "E", "pid": 0, "name": "X", "ts": 10}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"traceEvents": [
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "ts": 7, "tdur": 4},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "gamma", "ts": 2, "dur": 5, "args": {"detail": "foobar"}},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "epsilon", "ts": 7, "tdur": 4},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14},
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "gamma", "ts": 2, "dur": 5, "args": {"detail": "foobar"}},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "epsilon", "ts": 7, "tdur": 4},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14},
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "gamma", "ts": 2, "dur": 5, "args": {"detail": "foobar"}},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "epsilon", "ts": 7, "tdur": 4},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14}
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "gamma", "ts": 2, "dur": 5, "args": {"detail": "foobar"}},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "epsilon", "ts": 7, "tdur": 4},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 13},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 14},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "gamma", "ts": 5},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "delta", "ts": 5}
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* This script is used to reconstruct an cpu profile from chrome with an old format
|
||||
* still used in nodejs with v8-profiler
|
||||
*/
|
||||
const fs = require('fs')
|
||||
|
||||
const data = JSON.parse(fs.readFileSync('../../profiles/Chrome/65/simple.cpuprofile').toString())
|
||||
|
||||
const _convertTimeDeltas = (profile) => {
|
||||
if (!profile.timeDeltas) return null
|
||||
let lastTimeUsec = profile.startTime
|
||||
const timestamps = new Array(profile.timeDeltas.length + 1)
|
||||
for (let i = 0; i < profile.timeDeltas.length; ++i) {
|
||||
timestamps[i] = lastTimeUsec
|
||||
lastTimeUsec += profile.timeDeltas[i]
|
||||
}
|
||||
timestamps[profile.timeDeltas.length] = lastTimeUsec
|
||||
return timestamps
|
||||
}
|
||||
|
||||
const reformatNode = node => {
|
||||
if (!node.children) node.children = []
|
||||
|
||||
node.children = node.children.map(childID => {
|
||||
if (typeof childID !== 'number') return childID
|
||||
const childNode = data.nodes.find(node => node.id === childID)
|
||||
if (typeof childNode !== 'object') return null
|
||||
childNode.callUID = node.id
|
||||
return childNode
|
||||
})
|
||||
return {
|
||||
functionName: node.callFrame.functionName,
|
||||
url: node.callFrame.url,
|
||||
lineNumber: node.callFrame.lineNumber,
|
||||
callUID: node.callUID,
|
||||
bailoutReason: '',
|
||||
id: node.id,
|
||||
scriptId: 0,
|
||||
hitCount: node.hitCount,
|
||||
children: node.children.map(reformatNode)
|
||||
}
|
||||
}
|
||||
|
||||
// reformat then only keep the root as top level node
|
||||
const nodes = data.nodes
|
||||
.map(reformatNode)
|
||||
.filter(node => node.functionName === '(root)')[0]
|
||||
|
||||
// since it can be undefined, create an array so execution still works
|
||||
if (!data.timeDeltas) {
|
||||
data.timeDeltas = []
|
||||
}
|
||||
|
||||
fs.writeFileSync('./new.cpuprofile', JSON.stringify({
|
||||
head: nodes,
|
||||
startTime: Math.floor(data.startTime / 1000000),
|
||||
endTime: Math.floor(data.endTime / 1000000),
|
||||
samples: data.samples,
|
||||
timestamps: _convertTimeDeltas(data)
|
||||
}))
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run coverage
|
||||
node scripts/generate-file-format-schema-json.js > /dev/null
|
||||
@@ -3,7 +3,7 @@ const child_process = require('child_process')
|
||||
|
||||
// Convert the file-format-spec.ts file into a json schema file
|
||||
let jsonSchema = child_process.execSync(
|
||||
'node_modules/.bin/quicktype --lang schema ./src/lib/file-format-spec.ts',
|
||||
'node_modules/.bin/typescript-json-schema ./src/lib/file-format-spec.ts --titles --required --topRef "*"',
|
||||
{
|
||||
encoding: 'utf8',
|
||||
},
|
||||
|
||||
@@ -24,9 +24,7 @@ export class CanvasContext {
|
||||
const webGLInfo = this.gl.getWebGLInfo()
|
||||
if (webGLInfo) {
|
||||
console.log(
|
||||
`WebGL initialized. renderer: ${webGLInfo.renderer}, vendor: ${
|
||||
webGLInfo.vendor
|
||||
}, version: ${webGLInfo.version}`,
|
||||
`WebGL initialized. renderer: ${webGLInfo.renderer}, vendor: ${webGLInfo.vendor}, version: ${webGLInfo.version}`,
|
||||
)
|
||||
}
|
||||
;(window as any)['testContextLoss'] = () => {
|
||||
|
||||
@@ -175,8 +175,8 @@ export class FlamechartRenderer {
|
||||
// and the blue channel to indicate the color bucket to render.
|
||||
// We add one to each so we have zero reserved for the background color.
|
||||
const color = new Color(
|
||||
(1 + i % 255) / 256,
|
||||
(1 + stackDepth % 255) / 256,
|
||||
(1 + (i % 255)) / 256,
|
||||
(1 + (stackDepth % 255)) / 256,
|
||||
(1 + this.flamechart.getColorBucketForFrame(frame.node.frame)) / 256,
|
||||
)
|
||||
batch.addRect(configSpaceBounds, color)
|
||||
@@ -288,10 +288,10 @@ export class FlamechartRenderer {
|
||||
const configSpaceContentWidth = this.flamechart.getTotalWeight()
|
||||
const numAtlasEntriesPerLayer = Math.pow(2, zoomLevel)
|
||||
const left = Math.floor(
|
||||
numAtlasEntriesPerLayer * configSpaceSrcRect.left() / configSpaceContentWidth,
|
||||
(numAtlasEntriesPerLayer * configSpaceSrcRect.left()) / configSpaceContentWidth,
|
||||
)
|
||||
const right = Math.ceil(
|
||||
numAtlasEntriesPerLayer * configSpaceSrcRect.right() / configSpaceContentWidth,
|
||||
(numAtlasEntriesPerLayer * configSpaceSrcRect.right()) / configSpaceContentWidth,
|
||||
)
|
||||
|
||||
const nLayers = this.flamechart.getLayers().length
|
||||
|
||||
+8
-2
@@ -455,7 +455,8 @@ export namespace WebGL {
|
||||
widthInAppUnits: number,
|
||||
heightInAppUnits: number,
|
||||
) {
|
||||
const bounds = this._gl.canvas.getBoundingClientRect()
|
||||
let canvas = this._gl.canvas as HTMLCanvasElement
|
||||
const bounds = canvas.getBoundingClientRect()
|
||||
|
||||
if (
|
||||
this._width === widthInAppUnits &&
|
||||
@@ -467,7 +468,6 @@ export namespace WebGL {
|
||||
return
|
||||
}
|
||||
|
||||
let canvas = this._gl.canvas
|
||||
let style = canvas.style
|
||||
canvas.width = widthInPixels
|
||||
canvas.height = heightInPixels
|
||||
@@ -1086,11 +1086,17 @@ export namespace WebGL {
|
||||
|
||||
_compileShader(gl: WebGLRenderingContext, type: GLenum, source: string) {
|
||||
let shader = gl.createShader(type)
|
||||
if (!shader) {
|
||||
throw new Error('Failed to create shader')
|
||||
}
|
||||
gl.shaderSource(shader, source)
|
||||
gl.compileShader(shader)
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
throw new Error(`${gl.getShaderInfoLog(shader)}`)
|
||||
}
|
||||
if (!this._program) {
|
||||
throw new Error('Tried to attach shader before program was created')
|
||||
}
|
||||
gl.attachShader(this._program, shader)
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,12 @@ export class ViewportRectangleRenderer {
|
||||
private buffer: Graphics.VertexBuffer
|
||||
|
||||
constructor(private gl: Graphics.Context) {
|
||||
const vertices = [[-1, 1], [1, 1], [-1, -1], [1, -1]]
|
||||
const vertices = [
|
||||
[-1, 1],
|
||||
[1, 1],
|
||||
[-1, -1],
|
||||
[1, -1],
|
||||
]
|
||||
const floats: number[] = []
|
||||
for (let v of vertices) {
|
||||
floats.push(v[0])
|
||||
|
||||
@@ -45,7 +45,14 @@ export class RectangleBatch {
|
||||
return this.buffer
|
||||
}
|
||||
|
||||
const corners = [[0, 0], [1, 0], [0, 1], [1, 0], [0, 1], [1, 1]]
|
||||
const corners = [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
[1, 1],
|
||||
]
|
||||
|
||||
const bytes = new Uint8Array(vertexFormat.stride * corners.length * this.rects.length)
|
||||
const floats = new Float32Array(bytes.buffer)
|
||||
|
||||
@@ -104,6 +104,114 @@ exports[`importFromBGFlameGraph with CRLF: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromBGFlameGraph with CRLF: profileGroup.name 1`] = `"simple-crlf.txt"`;
|
||||
|
||||
exports[`importFromBGFlameGraph with UTF-16, Big Endian 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "a",
|
||||
"line": undefined,
|
||||
"name": "a",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 14,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "b",
|
||||
"line": undefined,
|
||||
"name": "b",
|
||||
"selfWeight": 5,
|
||||
"totalWeight": 14,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "c",
|
||||
"line": undefined,
|
||||
"name": "c",
|
||||
"selfWeight": 5,
|
||||
"totalWeight": 5,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "d",
|
||||
"line": undefined,
|
||||
"name": "d",
|
||||
"selfWeight": 4,
|
||||
"totalWeight": 4,
|
||||
},
|
||||
],
|
||||
"name": "simple-utf16-be.txt",
|
||||
"stacks": Array [
|
||||
"a;b;c 2",
|
||||
"a;b;d 4",
|
||||
"a;b;c 3",
|
||||
"a;b 5",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromBGFlameGraph with UTF-16, Big Endian: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromBGFlameGraph with UTF-16, Big Endian: profileGroup.name 1`] = `"simple-utf16-be.txt"`;
|
||||
|
||||
exports[`importFromBGFlameGraph with UTF-16, Little Endian 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "a",
|
||||
"line": undefined,
|
||||
"name": "a",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 14,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "b",
|
||||
"line": undefined,
|
||||
"name": "b",
|
||||
"selfWeight": 5,
|
||||
"totalWeight": 14,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "c",
|
||||
"line": undefined,
|
||||
"name": "c",
|
||||
"selfWeight": 5,
|
||||
"totalWeight": 5,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "d",
|
||||
"line": undefined,
|
||||
"name": "d",
|
||||
"selfWeight": 4,
|
||||
"totalWeight": 4,
|
||||
},
|
||||
],
|
||||
"name": "simple-utf16-le.txt",
|
||||
"stacks": Array [
|
||||
"a;b;c 2",
|
||||
"a;b;d 4",
|
||||
"a;b;c 3",
|
||||
"a;b 5",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromBGFlameGraph with UTF-16, Little Endian: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromBGFlameGraph with UTF-16, Little Endian: profileGroup.name 1`] = `"simple-utf16-le.txt"`;
|
||||
|
||||
exports[`importFromBGFlameGraph: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromBGFlameGraph: profileGroup.name 1`] = `"simple.txt"`;
|
||||
|
||||
@@ -515,6 +515,408 @@ exports[`importFromChromeTimeline Chrome 69: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromChromeTimeline Chrome 69: profileGroup.name 1`] = `"simple.json"`;
|
||||
|
||||
exports[`importFromChromeTimeline Workers Chrome 66 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": -1,
|
||||
"file": "",
|
||||
"key": "(program)::-1:-1",
|
||||
"line": -1,
|
||||
"name": "(program)",
|
||||
"selfWeight": 106601,
|
||||
"totalWeight": 108131,
|
||||
},
|
||||
Frame {
|
||||
"col": 8,
|
||||
"file": "",
|
||||
"key": "(anonymous)::247:8",
|
||||
"line": 247,
|
||||
"name": "(anonymous)",
|
||||
"selfWeight": 551,
|
||||
"totalWeight": 2766,
|
||||
},
|
||||
Frame {
|
||||
"col": -1,
|
||||
"file": "",
|
||||
"key": "addEventListener::-1:-1",
|
||||
"line": -1,
|
||||
"name": "addEventListener",
|
||||
"selfWeight": 178,
|
||||
"totalWeight": 2215,
|
||||
},
|
||||
Frame {
|
||||
"col": 17,
|
||||
"file": "",
|
||||
"key": "onLoaded::866:17",
|
||||
"line": 866,
|
||||
"name": "onLoaded",
|
||||
"selfWeight": 372,
|
||||
"totalWeight": 5158,
|
||||
},
|
||||
Frame {
|
||||
"col": 0,
|
||||
"file": "",
|
||||
"key": "(anonymous)::0:0",
|
||||
"line": 0,
|
||||
"name": "(anonymous)",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 25649,
|
||||
},
|
||||
Frame {
|
||||
"col": 17,
|
||||
"file": "",
|
||||
"key": "dispatch::845:17",
|
||||
"line": 845,
|
||||
"name": "dispatch",
|
||||
"selfWeight": 1103,
|
||||
"totalWeight": 25649,
|
||||
},
|
||||
Frame {
|
||||
"col": 20,
|
||||
"file": "",
|
||||
"key": "setPlatform::839:20",
|
||||
"line": 839,
|
||||
"name": "setPlatform",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 902,
|
||||
},
|
||||
Frame {
|
||||
"col": -1,
|
||||
"file": "",
|
||||
"key": "add::-1:-1",
|
||||
"line": -1,
|
||||
"name": "add",
|
||||
"selfWeight": 521,
|
||||
"totalWeight": 902,
|
||||
},
|
||||
Frame {
|
||||
"col": 14,
|
||||
"file": "",
|
||||
"key": "reset::433:14",
|
||||
"line": 433,
|
||||
"name": "reset",
|
||||
"selfWeight": 911,
|
||||
"totalWeight": 2438,
|
||||
},
|
||||
Frame {
|
||||
"col": -1,
|
||||
"file": "",
|
||||
"key": "getContext::-1:-1",
|
||||
"line": -1,
|
||||
"name": "getContext",
|
||||
"selfWeight": 598,
|
||||
"totalWeight": 598,
|
||||
},
|
||||
Frame {
|
||||
"col": 20,
|
||||
"file": "",
|
||||
"key": "resetCanvas::423:20",
|
||||
"line": 423,
|
||||
"name": "resetCanvas",
|
||||
"selfWeight": 370,
|
||||
"totalWeight": 553,
|
||||
},
|
||||
Frame {
|
||||
"col": -1,
|
||||
"file": "",
|
||||
"key": "scale::-1:-1",
|
||||
"line": -1,
|
||||
"name": "scale",
|
||||
"selfWeight": 183,
|
||||
"totalWeight": 183,
|
||||
},
|
||||
Frame {
|
||||
"col": 8,
|
||||
"file": "http://172.30.3.176:7777/worker.html",
|
||||
"key": "(anonymous):http://172.30.3.176:7777/worker.html:0:8",
|
||||
"line": 0,
|
||||
"name": "(anonymous)",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 2524,
|
||||
},
|
||||
Frame {
|
||||
"col": -1,
|
||||
"file": "",
|
||||
"key": "Worker::-1:-1",
|
||||
"line": -1,
|
||||
"name": "Worker",
|
||||
"selfWeight": 806,
|
||||
"totalWeight": 2524,
|
||||
},
|
||||
Frame {
|
||||
"col": 29,
|
||||
"file": "http://172.30.3.176:7777/worker.html",
|
||||
"key": "worker.onmessage:http://172.30.3.176:7777/worker.html:19:29",
|
||||
"line": 19,
|
||||
"name": "worker.onmessage",
|
||||
"selfWeight": 1221,
|
||||
"totalWeight": 60859,
|
||||
},
|
||||
Frame {
|
||||
"col": 16,
|
||||
"file": "http://172.30.3.176:7777/worker.html",
|
||||
"key": "apple:http://172.30.3.176:7777/worker.html:9:16",
|
||||
"line": 9,
|
||||
"name": "apple",
|
||||
"selfWeight": 5474,
|
||||
"totalWeight": 58863,
|
||||
},
|
||||
Frame {
|
||||
"col": 17,
|
||||
"file": "http://172.30.3.176:7777/worker.html",
|
||||
"key": "banana:http://172.30.3.176:7777/worker.html:1:17",
|
||||
"line": 1,
|
||||
"name": "banana",
|
||||
"selfWeight": 50635,
|
||||
"totalWeight": 53816,
|
||||
},
|
||||
Frame {
|
||||
"col": -1,
|
||||
"file": "",
|
||||
"key": "postMessage::-1:-1",
|
||||
"line": -1,
|
||||
"name": "postMessage",
|
||||
"selfWeight": 348,
|
||||
"totalWeight": 348,
|
||||
},
|
||||
Frame {
|
||||
"col": -1,
|
||||
"file": "",
|
||||
"key": "(garbage collector)::-1:-1",
|
||||
"line": -1,
|
||||
"name": "(garbage collector)",
|
||||
"selfWeight": 1530,
|
||||
"totalWeight": 1530,
|
||||
},
|
||||
],
|
||||
"name": "worker.json - CrRendererMain",
|
||||
"stacks": Array [
|
||||
" 28.66ms",
|
||||
"(program) 315.00µs",
|
||||
" 108.00µs",
|
||||
"(program) 433.00µs",
|
||||
" 116.00µs",
|
||||
"(program) 436.00µs",
|
||||
" 368.00µs",
|
||||
"(program) 108.00µs",
|
||||
" 30.76ms",
|
||||
"(program) 1.48ms",
|
||||
" 167.00µs",
|
||||
"(program) 1.68ms",
|
||||
" 6.96ms",
|
||||
"(program) 2.03ms",
|
||||
" 4.64ms",
|
||||
"(program) 3.86ms",
|
||||
" 14.05ms",
|
||||
"(program) 15.35ms",
|
||||
"(anonymous) 551.00µs",
|
||||
"(anonymous);addEventListener 178.00µs",
|
||||
"(anonymous);addEventListener;(program) 2.04ms",
|
||||
"onLoaded 372.00µs",
|
||||
"onLoaded;(program) 4.79ms",
|
||||
"(anonymous);dispatch;setPlatform;add 521.00µs",
|
||||
"(anonymous);dispatch;setPlatform;add;(program) 381.00µs",
|
||||
"(anonymous);dispatch 927.00µs",
|
||||
"(anonymous);dispatch;reset 185.00µs",
|
||||
"(anonymous);dispatch;reset;getContext 598.00µs",
|
||||
"(anonymous);dispatch;reset 131.00µs",
|
||||
"(anonymous);dispatch;reset;resetCanvas 370.00µs",
|
||||
"(anonymous);dispatch;reset;resetCanvas;scale 183.00µs",
|
||||
"(anonymous);dispatch;reset 595.00µs",
|
||||
"(anonymous);dispatch;reset;(program) 376.00µs",
|
||||
"(anonymous);dispatch 176.00µs",
|
||||
"(anonymous);dispatch;(program) 21.21ms",
|
||||
" 606.00µs",
|
||||
"(program) 11.20ms",
|
||||
" 296.00µs",
|
||||
"(program) 1.90ms",
|
||||
"(anonymous);Worker 806.00µs",
|
||||
"(anonymous);Worker;(program) 1.72ms",
|
||||
" 670.00µs",
|
||||
"(program) 28.45ms",
|
||||
" 353.00µs",
|
||||
"(program) 187.00µs",
|
||||
" 185.00µs",
|
||||
"(program) 4.29ms",
|
||||
" 607.00µs",
|
||||
"(program) 192.00µs",
|
||||
" 407.00µs",
|
||||
"(program) 215.00µs",
|
||||
" 28.98ms",
|
||||
"(program) 770.00µs",
|
||||
"worker.onmessage;apple 185.00µs",
|
||||
"worker.onmessage;apple;banana 180.00µs",
|
||||
"worker.onmessage;apple 367.00µs",
|
||||
"worker.onmessage;apple;banana 182.00µs",
|
||||
"worker.onmessage;apple 183.00µs",
|
||||
"worker.onmessage;apple;banana 546.00µs",
|
||||
"worker.onmessage;apple 365.00µs",
|
||||
"worker.onmessage;apple;banana 182.00µs",
|
||||
"worker.onmessage;apple 365.00µs",
|
||||
"worker.onmessage;apple;banana 188.00µs",
|
||||
"worker.onmessage;apple 912.00µs",
|
||||
"worker.onmessage;apple;banana 189.00µs",
|
||||
"worker.onmessage;apple 2.51ms",
|
||||
"worker.onmessage;apple;banana 8.48ms",
|
||||
"worker.onmessage;postMessage 170.00µs",
|
||||
" 3.95ms",
|
||||
"worker.onmessage;apple;banana 433.00µs",
|
||||
"worker.onmessage 187.00µs",
|
||||
"worker.onmessage;apple;banana 3.30ms",
|
||||
" 3.90ms",
|
||||
"(program) 187.00µs",
|
||||
"worker.onmessage;apple;banana 1.47ms",
|
||||
"worker.onmessage;apple 187.00µs",
|
||||
"worker.onmessage;apple;banana 2.05ms",
|
||||
" 4.01ms",
|
||||
"worker.onmessage;apple;banana 3.77ms",
|
||||
"worker.onmessage;postMessage 178.00µs",
|
||||
" 3.87ms",
|
||||
"worker.onmessage;apple;banana 3.74ms",
|
||||
"worker.onmessage 233.00µs",
|
||||
" 3.88ms",
|
||||
"(program) 187.00µs",
|
||||
"worker.onmessage;apple;banana 744.00µs",
|
||||
"worker.onmessage;apple 196.00µs",
|
||||
"worker.onmessage;apple;banana 2.08ms",
|
||||
"worker.onmessage 188.00µs",
|
||||
"worker.onmessage;apple;banana 582.00µs",
|
||||
" 4.46ms",
|
||||
"(program) 187.00µs",
|
||||
"worker.onmessage;apple;banana 3.84ms",
|
||||
"worker.onmessage;apple;banana;(program) 278.00µs",
|
||||
" 4.76ms",
|
||||
"worker.onmessage;apple;banana 5.01ms",
|
||||
"worker.onmessage;apple;banana;(program) 214.00µs",
|
||||
" 4.67ms",
|
||||
"(program) 193.00µs",
|
||||
"worker.onmessage;apple;banana 2.44ms",
|
||||
"worker.onmessage 186.00µs",
|
||||
"worker.onmessage;apple;banana 1.02ms",
|
||||
"worker.onmessage;banana 209.00µs",
|
||||
"worker.onmessage;apple;banana 207.00µs",
|
||||
"worker.onmessage;apple 207.00µs",
|
||||
"worker.onmessage;apple;banana 634.00µs",
|
||||
"worker.onmessage;apple;banana;(program) 237.00µs",
|
||||
" 4.92ms",
|
||||
"worker.onmessage;apple;banana 3.05ms",
|
||||
"worker.onmessage;banana 218.00µs",
|
||||
"worker.onmessage;apple;banana 876.00µs",
|
||||
"worker.onmessage 219.00µs",
|
||||
"worker.onmessage;apple;banana 219.00µs",
|
||||
"worker.onmessage;apple;banana;(program) 706.00µs",
|
||||
" 4.40ms",
|
||||
"worker.onmessage;apple;banana 2.71ms",
|
||||
"worker.onmessage 208.00µs",
|
||||
"worker.onmessage;apple;banana 2.08ms",
|
||||
"worker.onmessage;apple;banana;(program) 216.00µs",
|
||||
"worker.onmessage;apple;banana;(program);(garbage collector) 1.53ms",
|
||||
" 469.53ms",
|
||||
"(program) 382.00µs",
|
||||
" 262.13ms",
|
||||
"(program) 200.00µs",
|
||||
" 4.51ms",
|
||||
"(program) 221.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromChromeTimeline Workers Chrome 66 2`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": -1,
|
||||
"file": "",
|
||||
"key": "(program)::-1:-1",
|
||||
"line": -1,
|
||||
"name": "(program)",
|
||||
"selfWeight": 819120,
|
||||
"totalWeight": 819120,
|
||||
},
|
||||
Frame {
|
||||
"col": 20,
|
||||
"file": "http://172.30.3.176:7777/worker.js",
|
||||
"key": "onmessage:http://172.30.3.176:7777/worker.js:14:20",
|
||||
"line": 14,
|
||||
"name": "onmessage",
|
||||
"selfWeight": 639,
|
||||
"totalWeight": 857719,
|
||||
},
|
||||
Frame {
|
||||
"col": 14,
|
||||
"file": "http://172.30.3.176:7777/worker.js",
|
||||
"key": "alpha:http://172.30.3.176:7777/worker.js:8:14",
|
||||
"line": 8,
|
||||
"name": "alpha",
|
||||
"selfWeight": 19976,
|
||||
"totalWeight": 835149,
|
||||
},
|
||||
Frame {
|
||||
"col": 14,
|
||||
"file": "http://172.30.3.176:7777/worker.js",
|
||||
"key": "gamma:http://172.30.3.176:7777/worker.js:0:14",
|
||||
"line": 0,
|
||||
"name": "gamma",
|
||||
"selfWeight": 22197,
|
||||
"totalWeight": 30000,
|
||||
},
|
||||
Frame {
|
||||
"col": -1,
|
||||
"file": "",
|
||||
"key": "postMessage::-1:-1",
|
||||
"line": -1,
|
||||
"name": "postMessage",
|
||||
"selfWeight": 1278,
|
||||
"totalWeight": 21931,
|
||||
},
|
||||
],
|
||||
"name": "worker.json - DedicatedWorker thread",
|
||||
"stacks": Array [
|
||||
" 9.07ms",
|
||||
"(program) 5.49ms",
|
||||
"onmessage;alpha;gamma 3.69ms",
|
||||
"onmessage;postMessage 212.00µs",
|
||||
"onmessage;postMessage;(program) 3.97ms",
|
||||
"onmessage;alpha;gamma 3.82ms",
|
||||
"onmessage;postMessage 414.00µs",
|
||||
"onmessage;postMessage;(program) 3.71ms",
|
||||
"onmessage;alpha;gamma 1.13ms",
|
||||
"onmessage 188.00µs",
|
||||
"onmessage;alpha;gamma 2.48ms",
|
||||
"onmessage;alpha;gamma;(program) 3.89ms",
|
||||
"onmessage;alpha;gamma 373.00µs",
|
||||
"onmessage;alpha 184.00µs",
|
||||
"onmessage;alpha;gamma 3.36ms",
|
||||
"onmessage;alpha;gamma;(program) 3.91ms",
|
||||
"onmessage;alpha;gamma 3.94ms",
|
||||
"onmessage;postMessage 205.00µs",
|
||||
"onmessage;postMessage;(program) 3.91ms",
|
||||
"onmessage 451.00µs",
|
||||
"onmessage;alpha;gamma 566.00µs",
|
||||
"onmessage;alpha 194.00µs",
|
||||
"onmessage;alpha;gamma 2.09ms",
|
||||
"onmessage;alpha 189.00µs",
|
||||
"onmessage;alpha;gamma 746.00µs",
|
||||
"onmessage;postMessage 211.00µs",
|
||||
"onmessage;postMessage;(program) 4.00ms",
|
||||
"onmessage;alpha 4.80ms",
|
||||
"onmessage;alpha;(program) 5.28ms",
|
||||
"onmessage;alpha 4.70ms",
|
||||
"onmessage;postMessage 236.00µs",
|
||||
"onmessage;postMessage;(program) 5.06ms",
|
||||
"onmessage;alpha 5.07ms",
|
||||
"onmessage;alpha;(program) 4.81ms",
|
||||
"onmessage;alpha 4.84ms",
|
||||
"onmessage;alpha;(program) 775.08ms",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromChromeTimeline Workers Chrome 66: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromChromeTimeline Workers Chrome 66: profileGroup.name 1`] = `"worker.json"`;
|
||||
|
||||
exports[`importFromChromeTimeline Workers Chrome 70 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
@@ -903,7 +1305,7 @@ Object {
|
||||
"line": 0,
|
||||
"name": "(anonymous)",
|
||||
"selfWeight": 542,
|
||||
"totalWeight": 6218,
|
||||
"totalWeight": 6115,
|
||||
},
|
||||
Frame {
|
||||
"col": 25,
|
||||
@@ -912,7 +1314,7 @@ Object {
|
||||
"line": 30,
|
||||
"name": "insertTextScript",
|
||||
"selfWeight": 392,
|
||||
"totalWeight": 977,
|
||||
"totalWeight": 874,
|
||||
},
|
||||
Frame {
|
||||
"col": 25,
|
||||
@@ -920,8 +1322,8 @@ Object {
|
||||
"key": "insertHeaderNode:chrome-extension://denbgaamihkadbghdceggmchnflmhpmk/contentScript.js:57:25",
|
||||
"line": 57,
|
||||
"name": "insertHeaderNode",
|
||||
"selfWeight": 292,
|
||||
"totalWeight": 585,
|
||||
"selfWeight": 189,
|
||||
"totalWeight": 482,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
@@ -1156,7 +1558,7 @@ Object {
|
||||
"(anonymous);(anonymous);(anonymous);(program) 27.49ms",
|
||||
"(anonymous) 542.00µs",
|
||||
"(anonymous);insertTextScript 392.00µs",
|
||||
"(anonymous);insertTextScript;insertHeaderNode 292.00µs",
|
||||
"(anonymous);insertTextScript;insertHeaderNode 189.00µs",
|
||||
"(anonymous);insertTextScript;insertHeaderNode;appendChild;(anonymous);(anonymous) 148.00µs",
|
||||
"(anonymous);insertTextScript;insertHeaderNode;appendChild 145.00µs",
|
||||
"(anonymous);listenForMessage;get webstore 148.00µs",
|
||||
@@ -1357,3 +1759,66 @@ exports[`importFromChromeTimeline Workers Chrome 70: profileGroup.name 1`] = `"w
|
||||
exports[`importFromChromeTimeline: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromChromeTimeline: profileGroup.name 1`] = `"simple-timeline.json"`;
|
||||
|
||||
exports[`importFromV8Profiler Node 10 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": 0,
|
||||
"file": "",
|
||||
"key": "(anonymous)::0:0",
|
||||
"line": 0,
|
||||
"name": "(anonymous)",
|
||||
"selfWeight": 11575,
|
||||
"totalWeight": 44777,
|
||||
},
|
||||
Frame {
|
||||
"col": 0,
|
||||
"file": "",
|
||||
"key": "a::0:0",
|
||||
"line": 0,
|
||||
"name": "a",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 33202,
|
||||
},
|
||||
Frame {
|
||||
"col": 0,
|
||||
"file": "",
|
||||
"key": "b::5:0",
|
||||
"line": 5,
|
||||
"name": "b",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 16788,
|
||||
},
|
||||
Frame {
|
||||
"col": 0,
|
||||
"file": "",
|
||||
"key": "d::13:0",
|
||||
"line": 13,
|
||||
"name": "d",
|
||||
"selfWeight": 33202,
|
||||
"totalWeight": 33202,
|
||||
},
|
||||
Frame {
|
||||
"col": 0,
|
||||
"file": "",
|
||||
"key": "c::9:0",
|
||||
"line": 9,
|
||||
"name": "c",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 16414,
|
||||
},
|
||||
],
|
||||
"name": "example.cpuprofile",
|
||||
"stacks": Array [
|
||||
" 599.29ms",
|
||||
"(anonymous) 11.57ms",
|
||||
"(anonymous);a;b;d 16.79ms",
|
||||
"(anonymous);a;c;d 16.41ms",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromV8Profiler Node 10: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromV8Profiler Node 10: profileGroup.name 1`] = `"example.cpuprofile"`;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`importFromSafari 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": 13,
|
||||
"file": "__InjectedScript_InjectedScriptSource.js",
|
||||
"key": "injectModule:__InjectedScript_InjectedScriptSource.js:109:13",
|
||||
"line": 109,
|
||||
"name": "injectModule",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 0.001,
|
||||
},
|
||||
Frame {
|
||||
"col": 10,
|
||||
"file": "__InjectedScript_CommandLineAPIModuleSource.js",
|
||||
"key": ":__InjectedScript_CommandLineAPIModuleSource.js:2:10",
|
||||
"line": 2,
|
||||
"name": "(anonymous)",
|
||||
"selfWeight": 0.001,
|
||||
"totalWeight": 0.001,
|
||||
},
|
||||
Frame {
|
||||
"col": 1,
|
||||
"file": "file:///speedscope/sample/programs/javascript/simple.js",
|
||||
"key": "(program):file:///speedscope/sample/programs/javascript/simple.js:1:1",
|
||||
"line": 1,
|
||||
"name": "(program)",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 0.03248933597933502,
|
||||
},
|
||||
Frame {
|
||||
"col": 15,
|
||||
"file": "file:///speedscope/sample/programs/javascript/simple.js",
|
||||
"key": "alpha:file:///speedscope/sample/programs/javascript/simple.js:1:15",
|
||||
"line": 1,
|
||||
"name": "alpha",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 0.03248933597933502,
|
||||
},
|
||||
Frame {
|
||||
"col": 15,
|
||||
"file": "file:///speedscope/sample/programs/javascript/simple.js",
|
||||
"key": "delta:file:///speedscope/sample/programs/javascript/simple.js:14:15",
|
||||
"line": 14,
|
||||
"name": "delta",
|
||||
"selfWeight": 0.003094222474222382,
|
||||
"totalWeight": 0.020112446082445484,
|
||||
},
|
||||
Frame {
|
||||
"col": 15,
|
||||
"file": "file:///speedscope/sample/programs/javascript/simple.js",
|
||||
"key": "gamma:file:///speedscope/sample/programs/javascript/simple.js:20:15",
|
||||
"line": 20,
|
||||
"name": "gamma",
|
||||
"selfWeight": 0.029395113505112636,
|
||||
"totalWeight": 0.029395113505112636,
|
||||
},
|
||||
Frame {
|
||||
"col": 14,
|
||||
"file": "file:///speedscope/sample/programs/javascript/simple.js",
|
||||
"key": "beta:file:///speedscope/sample/programs/javascript/simple.js:8:14",
|
||||
"line": 8,
|
||||
"name": "beta",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 0.012376889896889526,
|
||||
},
|
||||
Frame {
|
||||
"col": 102,
|
||||
"file": "",
|
||||
"key": "firstOpenSearchURLString::4:102",
|
||||
"line": 4,
|
||||
"name": "firstOpenSearchURLString",
|
||||
"selfWeight": 0.0005174240213818848,
|
||||
"totalWeight": 0.0005174240213818848,
|
||||
},
|
||||
],
|
||||
"name": "Grabación de Control temporal 1",
|
||||
"stacks": Array [
|
||||
"injectModule;(anonymous) 1.00ms",
|
||||
" 39.93ms",
|
||||
"(program);alpha;delta;gamma 10.83ms",
|
||||
" 2.46ms",
|
||||
"(program);alpha;delta 3.09ms",
|
||||
"(program);alpha;beta;gamma 4.64ms",
|
||||
"(program);alpha;delta;gamma 1.55ms",
|
||||
"(program);alpha;beta;gamma 1.55ms",
|
||||
"(program);alpha;delta;gamma 3.09ms",
|
||||
"(program);alpha;beta;gamma 4.64ms",
|
||||
"(program);alpha;delta;gamma 1.55ms",
|
||||
"(program);alpha;beta;gamma 1.55ms",
|
||||
" 253.50ms",
|
||||
"firstOpenSearchURLString 517.42µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromSafari: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromSafari: profileGroup.name 1`] = `"Grabación de Control temporal 1"`;
|
||||
@@ -0,0 +1,528 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`importTraceEvents bad E events 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": 12,
|
||||
"totalWeight": 12,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"alpha 1.00µs",
|
||||
"alpha;beta 12.00µs",
|
||||
"alpha 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents bad E events: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents bad E events: profileGroup.name 1`] = `"too-many-end-events.json"`;
|
||||
|
||||
exports[`importTraceEvents event re-ordering 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "A",
|
||||
"line": undefined,
|
||||
"name": "A",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 6,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "B",
|
||||
"line": undefined,
|
||||
"name": "B",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 4,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "C",
|
||||
"line": undefined,
|
||||
"name": "C",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "X",
|
||||
"line": undefined,
|
||||
"name": "X",
|
||||
"selfWeight": 3,
|
||||
"totalWeight": 3,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 1",
|
||||
"stacks": Array [
|
||||
"A;B;C 1.00µs",
|
||||
"A;B 1.00µs",
|
||||
"A 1.00µs",
|
||||
" 1.00µs",
|
||||
"A 1.00µs",
|
||||
"A;B 1.00µs",
|
||||
"A;B;C 1.00µs",
|
||||
"X 3.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents event re-ordering: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents event re-ordering: profileGroup.name 1`] = `"must-retain-original-order.json"`;
|
||||
|
||||
exports[`importTraceEvents multiprocess 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "alpha",
|
||||
"line": undefined,
|
||||
"name": "alpha",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "p0 (pid 0), p0t0 (tid 0)",
|
||||
"stacks": Array [
|
||||
"alpha 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents multiprocess 2`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "beta",
|
||||
"line": undefined,
|
||||
"name": "beta",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "p0 (pid 0), p0t1 (tid 1)",
|
||||
"stacks": Array [
|
||||
"beta 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents multiprocess 3`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "gamma",
|
||||
"line": undefined,
|
||||
"name": "gamma",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "p1 (pid 1), p1t0 (tid 0)",
|
||||
"stacks": Array [
|
||||
"gamma 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents multiprocess 4`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "delta",
|
||||
"line": undefined,
|
||||
"name": "delta",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "p1 (pid 1), p1t1 (tid 1)",
|
||||
"stacks": Array [
|
||||
"delta 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents multiprocess 5`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "epsilon",
|
||||
"line": undefined,
|
||||
"name": "epsilon",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "p2t0 (pid 2, tid 0)",
|
||||
"stacks": Array [
|
||||
"epsilon 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents multiprocess 6`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "phi",
|
||||
"line": undefined,
|
||||
"name": "phi",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "p2t1 (pid 2, tid 1)",
|
||||
"stacks": Array [
|
||||
"phi 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents multiprocess 7`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "zeta",
|
||||
"line": undefined,
|
||||
"name": "zeta",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "p3 (pid 3, tid 0)",
|
||||
"stacks": Array [
|
||||
"zeta 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents multiprocess 8`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "eta",
|
||||
"line": undefined,
|
||||
"name": "eta",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "p3 (pid 3, tid 1)",
|
||||
"stacks": Array [
|
||||
"eta 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents multiprocess: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents multiprocess: profileGroup.name 1`] = `"multiprocess.json"`;
|
||||
|
||||
exports[`importTraceEvents partial json import 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 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: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents partial json import trailing comma: profileGroup.name 1`] = `"simple-partial-trailing-comma.json"`;
|
||||
|
||||
exports[`importTraceEvents partial json import whitespace padding 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: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents partial json import whitespace padding: profileGroup.name 1`] = `"simple-partial-whitespace.json"`;
|
||||
|
||||
exports[`importTraceEvents partial json import: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents partial json import: profileGroup.name 1`] = `"simple-partial.json"`;
|
||||
|
||||
exports[`importTraceEvents simple 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 simple object 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": 8,
|
||||
"totalWeight": 12,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "(unnamed)",
|
||||
"line": undefined,
|
||||
"name": "(unnamed)",
|
||||
"selfWeight": 4,
|
||||
"totalWeight": 4,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"alpha 1.00µs",
|
||||
"alpha;beta 6.00µs",
|
||||
"alpha;beta;(unnamed) 4.00µs",
|
||||
"alpha;beta 2.00µs",
|
||||
"alpha 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents simple object: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents simple object: profileGroup.name 1`] = `"simple-object.json"`;
|
||||
|
||||
exports[`importTraceEvents simple: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents simple: profileGroup.name 1`] = `"simple.json"`;
|
||||
@@ -7,3 +7,11 @@ test('importFromBGFlameGraph', async () => {
|
||||
test('importFromBGFlameGraph with CRLF', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-crlf.txt')
|
||||
})
|
||||
|
||||
test('importFromBGFlameGraph with UTF-16, Little Endian', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-utf16-le.txt')
|
||||
})
|
||||
|
||||
test('importFromBGFlameGraph with UTF-16, Big Endian', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple-utf16-be.txt')
|
||||
})
|
||||
|
||||
@@ -12,6 +12,14 @@ test('importFromChromeTimeline Chrome 69', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Chrome/69/simple.json')
|
||||
})
|
||||
|
||||
test('importFromV8Profiler Node 10', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/node/10.11.0/example.cpuprofile')
|
||||
})
|
||||
|
||||
test('importFromChromeTimeline Workers Chrome 66', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Chrome/66/worker.json')
|
||||
})
|
||||
|
||||
test('importFromChromeTimeline Workers Chrome 70', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Chrome/70/worker.json')
|
||||
})
|
||||
|
||||
+33
-23
@@ -1,6 +1,7 @@
|
||||
import {Profile, FrameInfo, CallTreeProfileBuilder, ProfileGroup} from '../lib/profile'
|
||||
import {getOrInsert, lastOf, sortBy, itForEach} from '../lib/utils'
|
||||
import {TimeFormatter} from '../lib/value-formatters'
|
||||
import {chromeTreeToNodes, OldCPUProfile} from './v8cpuFormatter'
|
||||
|
||||
// See: https://github.com/v8/v8/blob/master/src/inspector/js_protocol.json
|
||||
|
||||
@@ -31,7 +32,7 @@ interface CPUProfileCallFrame {
|
||||
url: string
|
||||
}
|
||||
|
||||
interface CPUProfileNode {
|
||||
export interface CPUProfileNode {
|
||||
callFrame: CPUProfileCallFrame
|
||||
hitCount: number
|
||||
id: number
|
||||
@@ -40,7 +41,7 @@ interface CPUProfileNode {
|
||||
parent?: CPUProfileNode
|
||||
}
|
||||
|
||||
interface CPUProfile {
|
||||
export interface CPUProfile {
|
||||
startTime: number
|
||||
endTime: number
|
||||
nodes: CPUProfileNode[]
|
||||
@@ -78,19 +79,17 @@ export function importFromChromeTimeline(events: TimelineEvent[], fileName: stri
|
||||
// that they are.
|
||||
sortBy(events, e => e.ts)
|
||||
|
||||
const DEFAULT_ID = '(default id)'
|
||||
|
||||
for (let event of events) {
|
||||
if (event.name === 'CpuProfile') {
|
||||
cpuProfileByID.set(event.id || DEFAULT_ID, event.args.data.cpuProfile as CPUProfile)
|
||||
|
||||
if (event.id) {
|
||||
pidTidById.set(event.id, `${event.pid}:${event.tid}`)
|
||||
}
|
||||
const pidTid = `${event.pid}:${event.tid}`
|
||||
const id = event.id || pidTid
|
||||
cpuProfileByID.set(id, event.args.data.cpuProfile as CPUProfile)
|
||||
pidTidById.set(id, pidTid)
|
||||
}
|
||||
|
||||
if (event.name === 'Profile') {
|
||||
cpuProfileByID.set(event.id || DEFAULT_ID, {
|
||||
const pidTid = `${event.pid}:${event.tid}`
|
||||
cpuProfileByID.set(event.id || pidTid, {
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
nodes: [],
|
||||
@@ -109,7 +108,8 @@ export function importFromChromeTimeline(events: TimelineEvent[], fileName: stri
|
||||
}
|
||||
|
||||
if (event.name === 'ProfileChunk') {
|
||||
const cpuProfile = cpuProfileByID.get(event.id || DEFAULT_ID)
|
||||
const pidTid = `${event.pid}:${event.tid}`
|
||||
const cpuProfile = cpuProfileByID.get(event.id || pidTid)
|
||||
if (cpuProfile) {
|
||||
const chunk = event.args.data
|
||||
if (chunk.cpuProfile) {
|
||||
@@ -130,9 +130,7 @@ export function importFromChromeTimeline(events: TimelineEvent[], fileName: stri
|
||||
cpuProfile.endTime = chunk.endTime
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`Ignoring ProfileChunk for undeclared Profile with id ${event.id || DEFAULT_ID}`,
|
||||
)
|
||||
console.warn(`Ignoring ProfileChunk for undeclared Profile with id ${event.id || pidTid}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +224,10 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
|
||||
// Ref: https://github.com/v8/v8/blob/44bd8fd7/src/inspector/js_protocol.json#L1485
|
||||
let elapsed = chromeProfile.timeDeltas[0]
|
||||
|
||||
// Prevents negative time deltas from causing bad data. See
|
||||
// https://github.com/jlfwong/speedscope/pull/305 for details.
|
||||
let lastValidElapsed = elapsed
|
||||
|
||||
let lastNodeId = NaN
|
||||
|
||||
// The chrome CPU profile format doesn't collapse identical samples. We'll do that
|
||||
@@ -234,22 +236,26 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
|
||||
const nodeId = chromeProfile.samples[i]
|
||||
if (nodeId != lastNodeId) {
|
||||
samples.push(nodeId)
|
||||
sampleTimes.push(elapsed)
|
||||
if (elapsed < lastValidElapsed) {
|
||||
sampleTimes.push(lastValidElapsed)
|
||||
} else {
|
||||
sampleTimes.push(elapsed)
|
||||
lastValidElapsed = elapsed
|
||||
}
|
||||
}
|
||||
|
||||
if (i === chromeProfile.samples.length - 1) {
|
||||
if (!isNaN(lastNodeId)) {
|
||||
samples.push(lastNodeId)
|
||||
sampleTimes.push(elapsed)
|
||||
if (elapsed < lastValidElapsed) {
|
||||
sampleTimes.push(lastValidElapsed)
|
||||
} else {
|
||||
sampleTimes.push(elapsed)
|
||||
lastValidElapsed = elapsed
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let timeDelta = chromeProfile.timeDeltas[i + 1]
|
||||
if (timeDelta < 0) {
|
||||
// This is super noisy, but can be helpful when debugging strange data
|
||||
// console.warn('Substituting zero for unexpected time delta:', timeDelta, 'at index', i)
|
||||
timeDelta = 0
|
||||
}
|
||||
|
||||
const timeDelta = chromeProfile.timeDeltas[i + 1]
|
||||
elapsed += timeDelta
|
||||
lastNodeId = nodeId
|
||||
}
|
||||
@@ -312,3 +318,7 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
|
||||
profile.setValueFormatter(new TimeFormatter('microseconds'))
|
||||
return profile.build()
|
||||
}
|
||||
|
||||
export function importFromOldV8CPUProfile(content: OldCPUProfile): Profile {
|
||||
return importFromChromeCPUProfile(chromeTreeToNodes(content))
|
||||
}
|
||||
|
||||
@@ -5,4 +5,8 @@ test('importProfileGroup', async () => {
|
||||
expect(await importProfileGroupFromText('unknown', '')).toBe(null)
|
||||
expect(await importProfileGroupFromText('unknown', 'Hello world')).toBe(null)
|
||||
expect(await importProfileGroupFromText('unknown', 'Hello\n\nWorld')).toBe(null)
|
||||
|
||||
// Importing from a version of stackprof which was missing raw_timestamp_deltas should return null
|
||||
const oldStackprof = `{"version":1.2,"mode":"wall","interval":1000,"samples":0,"gc_samples":0,"missed_samples":0,"frames":{}}`
|
||||
expect(await importProfileGroupFromText('unknown', oldStackprof)).toBe(null)
|
||||
})
|
||||
|
||||
+44
-8
@@ -1,7 +1,12 @@
|
||||
import {Profile, ProfileGroup} from '../lib/profile'
|
||||
import {FileSystemDirectoryEntry} from './file-system-entry'
|
||||
|
||||
import {importFromChromeCPUProfile, importFromChromeTimeline, isChromeTimeline} from './chrome'
|
||||
import {
|
||||
importFromChromeCPUProfile,
|
||||
importFromChromeTimeline,
|
||||
isChromeTimeline,
|
||||
importFromOldV8CPUProfile,
|
||||
} from './chrome'
|
||||
import {importFromStackprof} from './stackprof'
|
||||
import {importFromInstrumentsDeepCopy, importFromInstrumentsTrace} from './instruments'
|
||||
import {importFromBGFlameGraph} from './bg-flamegraph'
|
||||
@@ -10,10 +15,12 @@ import {importSpeedscopeProfiles} from '../lib/file-format'
|
||||
import {importFromV8ProfLog} from './v8proflog'
|
||||
import {importFromLinuxPerf} from './linux-tools-perf'
|
||||
import {importFromHaskell} from './haskell'
|
||||
import {importFromSafari} from './safari'
|
||||
import {ProfileDataSource, TextProfileDataSource, MaybeCompressedDataReader} from './utils'
|
||||
import {importAsPprofProfile} from './pprof'
|
||||
import {decodeBase64} from '../lib/utils'
|
||||
import {importFromChromeHeapProfile} from './v8heapalloc'
|
||||
import {isTraceEventFormatted, importTraceEvents} from './trace-event'
|
||||
|
||||
export async function importProfileGroupFromText(
|
||||
fileName: string,
|
||||
@@ -65,6 +72,26 @@ function toGroup(profile: Profile | null): ProfileGroup | null {
|
||||
return {name: profile.getName(), indexToView: 0, profiles: [profile]}
|
||||
}
|
||||
|
||||
function fixUpJSON(content: string): string {
|
||||
// This code is similar to the code from here:
|
||||
// https://github.com/catapult-project/catapult/blob/27e047e0494df162022be6aa8a8862742a270232/tracing/tracing/extras/importer/trace_event_importer.html#L197-L208
|
||||
//
|
||||
// If the event data begins with a [, then we know it should end with a ]. The
|
||||
// reason we check for this is because some tracing implementations cannot
|
||||
// guarantee that a ']' gets written to the trace file. So, we are forgiving
|
||||
// and if this is obviously the case, we fix it up before throwing the string
|
||||
// at JSON.parse.
|
||||
//
|
||||
content = content.trim()
|
||||
if (content[0] === '[') {
|
||||
content = content.replace(/,\s*$/, '')
|
||||
if (content[content.length - 1] !== ']') {
|
||||
content += ']'
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
async function _importProfileGroup(dataSource: ProfileDataSource): Promise<ProfileGroup | null> {
|
||||
const fileName = await dataSource.name()
|
||||
|
||||
@@ -84,9 +111,6 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
|
||||
if (fileName.endsWith('.speedscope.json')) {
|
||||
console.log('Importing as speedscope json file')
|
||||
return importSpeedscopeProfiles(JSON.parse(contents))
|
||||
} else if (fileName.endsWith('.cpuprofile')) {
|
||||
console.log('Importing as Chrome CPU Profile')
|
||||
return toGroup(importFromChromeCPUProfile(JSON.parse(contents)))
|
||||
} 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)
|
||||
@@ -108,12 +132,15 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
|
||||
} else if (fileName.endsWith('.heapprofile')) {
|
||||
console.log('Importing as Chrome Heap Profile')
|
||||
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
|
||||
} else if (fileName.endsWith('-recording.json')) {
|
||||
console.log('Importing as Safari profile')
|
||||
return toGroup(importFromSafari(JSON.parse(contents)))
|
||||
}
|
||||
|
||||
// Second pass: Try to guess what file format it is based on structure
|
||||
let parsed: any
|
||||
try {
|
||||
parsed = JSON.parse(contents)
|
||||
parsed = JSON.parse(fixUpJSON(contents))
|
||||
} catch (e) {}
|
||||
if (parsed) {
|
||||
if (parsed['$schema'] === 'https://www.speedscope.app/file-format-schema.json') {
|
||||
@@ -123,12 +150,18 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
|
||||
console.log('Importing as Firefox profile')
|
||||
return toGroup(importFromFirefox(parsed))
|
||||
} else if (isChromeTimeline(parsed)) {
|
||||
console.log('Importing as Chrome CPU Profile')
|
||||
console.log('Importing as Chrome Timeline')
|
||||
return importFromChromeTimeline(parsed, fileName)
|
||||
} else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) {
|
||||
console.log('Importing as Chrome Timeline')
|
||||
console.log('Importing as Chrome CPU Profile')
|
||||
return toGroup(importFromChromeCPUProfile(parsed))
|
||||
} else if ('mode' in parsed && 'frames' in parsed) {
|
||||
} else if (isTraceEventFormatted(parsed)) {
|
||||
console.log('Importing as Trace Event Format profile')
|
||||
return importTraceEvents(parsed)
|
||||
} else if ('head' in parsed && 'samples' in parsed && 'timestamps' in parsed) {
|
||||
console.log('Importing as Chrome CPU Profile (old format)')
|
||||
return toGroup(importFromOldV8CPUProfile(parsed))
|
||||
} else if ('mode' in parsed && 'frames' in parsed && 'raw_timestamp_deltas' in parsed) {
|
||||
console.log('Importing as stackprof profile')
|
||||
return toGroup(importFromStackprof(parsed))
|
||||
} else if ('code' in parsed && 'functions' in parsed && 'ticks' in parsed) {
|
||||
@@ -140,6 +173,9 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
|
||||
} else if ('rts_arguments' in parsed && 'initial_capabilities' in parsed) {
|
||||
console.log('Importing as Haskell GHC JSON Profile')
|
||||
return importFromHaskell(parsed)
|
||||
} else if ('recording' in parsed && 'sampleStackTraces' in parsed.recording) {
|
||||
console.log('Importing as Safari profile')
|
||||
return toGroup(importFromSafari(JSON.parse(contents)))
|
||||
}
|
||||
} else {
|
||||
// Format is not JSON
|
||||
|
||||
@@ -98,4 +98,7 @@ describe('importFromInstrumentsTrace', () => {
|
||||
test('Instruments 9.3.1', async () => {
|
||||
await importFromTrace('./sample/profiles/Instruments/9.3.1/simple-time-profile.trace.zip')
|
||||
})
|
||||
test('Instruments 10.0', async () => {
|
||||
await importFromTrace('./sample/profiles/Instruments/10.0/simple-time-profile.trace.zip')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -361,7 +361,10 @@ async function readFormTemplate(tree: TraceDirectoryTree): Promise<FormTemplateD
|
||||
const archive = readInstrumentsKeyedArchive(await readAsArrayBuffer(formTemplate))
|
||||
|
||||
const version = archive['com.apple.xray.owner.template.version']
|
||||
const selectedRunNumber = archive['com.apple.xray.owner.template'].get('_selectedRunNumber')
|
||||
let selectedRunNumber = 1
|
||||
if ('com.apple.xray.owner.template' in archive) {
|
||||
selectedRunNumber = archive['com.apple.xray.owner.template'].get('_selectedRunNumber')
|
||||
}
|
||||
let instrument = archive['$1']
|
||||
if ('stubInfoByUUID' in archive) {
|
||||
instrument = Array.from(archive['stubInfoByUUID'].keys())[0]
|
||||
@@ -630,7 +633,7 @@ export function readInstrumentsKeyedArchive(buffer: ArrayBuffer): any {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export function decodeUTF8(bytes: Uint8Array): string {
|
||||
let text = String.fromCharCode.apply(String, bytes)
|
||||
let text = String.fromCharCode.apply(String, Array.from(bytes))
|
||||
if (text.slice(-1) === '\0') text = text.slice(0, -1) // Remove a single trailing null character if present
|
||||
return decodeURIComponent(escape(text))
|
||||
}
|
||||
@@ -737,7 +740,10 @@ function paternMatchObjectiveC(
|
||||
// Replace NSString with a string
|
||||
case 'NSString':
|
||||
case 'NSMutableString':
|
||||
return decodeUTF8(value['NS.bytes'])
|
||||
if (value['NS.string']) return value['NS.string']
|
||||
if (value['NS.bytes']) return decodeUTF8(value['NS.bytes'])
|
||||
console.warn(`Unexpected ${name} format: `, value)
|
||||
return null
|
||||
|
||||
// Replace NSArray with an Array
|
||||
case 'NSArray':
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
describe('importFromLinuxPerf', async () => {
|
||||
describe('importFromLinuxPerf', () => {
|
||||
test('simple.linux-perf.txt', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/linux-perf/simple.linux-perf.txt')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
test('importFromSafari', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Safari/13.1/simple.html-recording.json')
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
|
||||
import {TimeFormatter} from '../lib/value-formatters'
|
||||
|
||||
interface Record {
|
||||
type: string
|
||||
eventType?: string
|
||||
startTime?: number
|
||||
endTime?: number
|
||||
// timeline-record-type-cpu
|
||||
timestamp?: number
|
||||
usage?: number
|
||||
threads?: any[]
|
||||
// timeline-record-type-script
|
||||
details?: number | string | any
|
||||
extraDetails?: null | any
|
||||
// timeline-record-type-network
|
||||
archiveStartTime?: number
|
||||
entry?: any
|
||||
// timeline-record-type-layout
|
||||
quad?: number[]
|
||||
}
|
||||
|
||||
interface ExprLocation {
|
||||
line: number
|
||||
column: number
|
||||
}
|
||||
|
||||
interface StackFrame {
|
||||
sourceID: string
|
||||
name: string
|
||||
line: number
|
||||
column: number
|
||||
url: string
|
||||
expressionLocation?: ExprLocation
|
||||
}
|
||||
|
||||
interface Sample {
|
||||
timestamp: number
|
||||
stackFrames: StackFrame[]
|
||||
}
|
||||
|
||||
interface Recording {
|
||||
displayName: string
|
||||
startTime: number
|
||||
endTime: number
|
||||
discontinuities: any[]
|
||||
instrumentTypes: string[]
|
||||
records: Record[]
|
||||
markers: any[]
|
||||
memoryPressureEvents: any[]
|
||||
sampleStackTraces: Sample[]
|
||||
sampleDurations: number[]
|
||||
}
|
||||
|
||||
interface Overview {
|
||||
secondsPerPixel: number
|
||||
scrollStartTime: number
|
||||
selectionStartTime: number
|
||||
selectionDuration: number
|
||||
}
|
||||
|
||||
interface SafariProfile {
|
||||
version: number
|
||||
recording: Recording
|
||||
overview: Overview
|
||||
}
|
||||
|
||||
function makeStack(frames: StackFrame[]): FrameInfo[] {
|
||||
return frames
|
||||
.map(({name, url, line, column}) => ({
|
||||
key: `${name}:${url}:${line}:${column}`,
|
||||
file: url,
|
||||
line,
|
||||
col: column,
|
||||
name: name || '(anonymous)',
|
||||
}))
|
||||
.reverse()
|
||||
}
|
||||
|
||||
export function importFromSafari(contents: SafariProfile): Profile | null {
|
||||
if (contents.version !== 1) {
|
||||
console.warn(`Unknown Safari profile version ${contents.version}... Might be incompatible.`)
|
||||
}
|
||||
|
||||
const {recording} = contents
|
||||
const {sampleStackTraces, sampleDurations} = recording
|
||||
|
||||
const count = sampleStackTraces.length
|
||||
if (count < 1) {
|
||||
console.warn('Empty profile')
|
||||
return null
|
||||
}
|
||||
|
||||
const profileDuration =
|
||||
sampleStackTraces[count - 1].timestamp - sampleStackTraces[0].timestamp + sampleDurations[0]
|
||||
const profile = new StackListProfileBuilder(profileDuration)
|
||||
|
||||
let previousEndTime = Number.MAX_VALUE
|
||||
|
||||
sampleStackTraces.forEach((sample, i) => {
|
||||
const endTime = sample.timestamp
|
||||
const duration = sampleDurations[i]
|
||||
const startTime = endTime - duration
|
||||
const idleDurationBefore = startTime - previousEndTime
|
||||
|
||||
// FIXME: 2ms is a lot, but Safari's timestamps and durations don't line up very well and will create
|
||||
// phantom idle time
|
||||
if (idleDurationBefore > 0.002) {
|
||||
profile.appendSampleWithWeight([], idleDurationBefore)
|
||||
}
|
||||
|
||||
profile.appendSampleWithWeight(makeStack(sample.stackFrames), duration)
|
||||
|
||||
previousEndTime = endTime
|
||||
})
|
||||
|
||||
profile.setValueFormatter(new TimeFormatter('seconds'))
|
||||
profile.setName(recording.displayName)
|
||||
return profile.build()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
test('importTraceEvents simple', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/simple.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents simple object', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/simple-object.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents multiprocess', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/multiprocess.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents partial json import', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents partial json import trailing comma', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-trailing-comma.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents partial json import whitespace padding', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/simple-partial-whitespace.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents bad E events', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/too-many-end-events.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents event re-ordering', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/must-retain-original-order.json')
|
||||
})
|
||||
@@ -0,0 +1,323 @@
|
||||
import {sortBy, zeroPad, lastOf} from '../lib/utils'
|
||||
import {ProfileGroup, CallTreeProfileBuilder, FrameInfo} from '../lib/profile'
|
||||
import {TimeFormatter} from '../lib/value-formatters'
|
||||
|
||||
// This file concerns import from the "Trace Event Format", authored by Google
|
||||
// and used for Google's own chrome://trace.
|
||||
//
|
||||
// The file format is extremely general, and we only support the parts of it
|
||||
// that logically map onto speedscope's visualization capabilities.
|
||||
// Specifically, we only support the "B", "E", and "X" event types. Everything
|
||||
// else is ignored. We do, however, support import of profiles that are
|
||||
// multi-process/multi-threaded. Each process is split into a separate profile.
|
||||
//
|
||||
// Note that Chrome Developer Tools uses this format as well, but all the
|
||||
// relevant data used in those profiles is stored in events with the name
|
||||
// "CpuProfile", "Profile", or "ProfileChunk". If we detect those, we prioritize
|
||||
// importing the profile as a Chrome Developer Tools profile. Otherwise,
|
||||
// we try to import it as a "Trace Event Format" file.
|
||||
//
|
||||
// Spec: https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
|
||||
|
||||
interface TraceEvent {
|
||||
// The process ID for the process that output this event.
|
||||
pid: number
|
||||
|
||||
// The thread ID for the thread that output this event.
|
||||
tid: number
|
||||
|
||||
// The event type. This is a single character which changes depending on the type of event being output. The valid values are listed in the table below. We will discuss each phase type below.
|
||||
ph: string
|
||||
|
||||
// The tracing clock timestamp of the event. The timestamps are provided at microsecond granularity.
|
||||
ts: number
|
||||
|
||||
// The thread clock timestamp of the event. The timestamps are provided at microsecond granularity.
|
||||
tts?: number
|
||||
|
||||
// The name of the event, as displayed in Trace Viewer
|
||||
name?: string
|
||||
|
||||
// The event categories. This is a comma separated list of categories for the event. The categories can be used to hide events in the Trace Viewer UI.
|
||||
cat?: string
|
||||
|
||||
// Any arguments provided for the event. Some of the event types have required argument fields, otherwise, you can put any information you wish in here. The arguments are displayed in Trace Viewer when you view an event in the analysis section.
|
||||
args: any
|
||||
|
||||
// A fixed color name to associate with the event. If provided, cname must be one of the names listed in trace-viewer's base color scheme's reserved color names list
|
||||
cname?: string
|
||||
}
|
||||
|
||||
interface BTraceEvent extends TraceEvent {
|
||||
ph: 'B'
|
||||
}
|
||||
|
||||
interface ETraceEvent extends TraceEvent {
|
||||
ph: 'E'
|
||||
}
|
||||
|
||||
interface XTraceEvent extends TraceEvent {
|
||||
ph: 'X'
|
||||
dur?: number
|
||||
tdur?: number
|
||||
}
|
||||
|
||||
// The trace format supports a number of event types that we ignore.
|
||||
type ImportableTraceEvent = BTraceEvent | ETraceEvent | XTraceEvent
|
||||
type DurationEvent = BTraceEvent | ETraceEvent
|
||||
|
||||
function filterIgnoredEventTypes(events: TraceEvent[]): ImportableTraceEvent[] {
|
||||
const ret: ImportableTraceEvent[] = []
|
||||
for (let ev of events) {
|
||||
switch (ev.ph) {
|
||||
case 'B':
|
||||
case 'E':
|
||||
case 'X':
|
||||
ret.push(ev as ImportableTraceEvent)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
function convertToDurationEvents(events: ImportableTraceEvent[]): DurationEvent[] {
|
||||
const ret: DurationEvent[] = []
|
||||
for (let ev of events) {
|
||||
switch (ev.ph) {
|
||||
case 'B':
|
||||
ret.push(ev)
|
||||
break
|
||||
|
||||
case 'E':
|
||||
ret.push(ev)
|
||||
break
|
||||
|
||||
case 'X':
|
||||
let dur: number | null = null
|
||||
if (ev.dur != null) dur = ev.dur
|
||||
else if (ev.tdur != null) dur = ev.tdur
|
||||
|
||||
if (dur == null) {
|
||||
console.warn('Found a complete event (X) with no duration. Skipping: ', ev)
|
||||
continue
|
||||
}
|
||||
|
||||
ret.push({...ev, ph: 'B'} as BTraceEvent)
|
||||
ret.push({...ev, ph: 'E', ts: ev.ts + dur} as ETraceEvent)
|
||||
break
|
||||
|
||||
default:
|
||||
const _exhaustiveCheck: never = ev
|
||||
return _exhaustiveCheck
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
function getProcessNamesByPid(events: TraceEvent[]): Map<number, string> {
|
||||
const processNamesByPid = new Map<number, string>()
|
||||
for (let ev of events) {
|
||||
if (ev.ph === 'M' && ev.name === 'process_name' && ev.args && ev.args.name) {
|
||||
processNamesByPid.set(ev.pid, ev.args.name)
|
||||
}
|
||||
}
|
||||
return processNamesByPid
|
||||
}
|
||||
|
||||
function getThreadNamesByPidTid(events: TraceEvent[]): Map<string, string> {
|
||||
const threadNameByPidTid = new Map<string, string>()
|
||||
|
||||
for (let ev of events) {
|
||||
if (ev.ph === 'M' && ev.name === 'thread_name' && ev.args && ev.args.name) {
|
||||
const key = `${ev.pid}:${ev.tid}`
|
||||
threadNameByPidTid.set(key, ev.args.name)
|
||||
}
|
||||
}
|
||||
return threadNameByPidTid
|
||||
}
|
||||
|
||||
function keyForEvent(event: TraceEvent): string {
|
||||
let name = `${event.name || '(unnamed)'}`
|
||||
if (event.args) {
|
||||
name += ` ${JSON.stringify(event.args)}`
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
type TraceEventProfileState = {profile: CallTreeProfileBuilder; eventStack: BTraceEvent[]}
|
||||
|
||||
function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
|
||||
const stateByPidTid = new Map<string, TraceEventProfileState>()
|
||||
|
||||
const importableEvents = filterIgnoredEventTypes(events)
|
||||
const durationEvents = convertToDurationEvents(importableEvents)
|
||||
|
||||
const processNamesByPid = getProcessNamesByPid(events)
|
||||
const threadNamesByPidTid = getThreadNamesByPidTid(events)
|
||||
|
||||
durationEvents.sort((a, b) => {
|
||||
if (a.ts < b.ts) return -1
|
||||
if (a.ts > b.ts) return 1
|
||||
if (a.pid < b.pid) return -1
|
||||
if (a.pid > b.pid) return 1
|
||||
if (a.tid < b.tid) return -1
|
||||
if (a.tid > b.tid) return 1
|
||||
|
||||
// We have to be careful with events that have the same timestamp
|
||||
// and the same pid/tid
|
||||
const aKey = keyForEvent(a)
|
||||
const bKey = keyForEvent(b)
|
||||
if (aKey === bKey) {
|
||||
// If the two elements have the same key, we need to process the begin
|
||||
// event before the end event. This will be a zero-duration event.
|
||||
if (a.ph === 'B' && b.ph === 'E') return -1
|
||||
if (a.ph === 'E' && b.ph === 'B') return 1
|
||||
} else {
|
||||
// If the two elements have *different* keys, we want to process
|
||||
// the end of an event before the beginning of the event to prevent
|
||||
// out-of-order push/pops from the call-stack.
|
||||
if (a.ph === 'B' && b.ph === 'E') return 1
|
||||
if (a.ph === 'E' && b.ph === 'B') return -1
|
||||
}
|
||||
|
||||
// In all other cases, retain the original sort order.
|
||||
return 0
|
||||
})
|
||||
|
||||
if (durationEvents.length > 0) {
|
||||
const firstTs = durationEvents[0].ts
|
||||
for (let ev of durationEvents) {
|
||||
ev.ts -= firstTs
|
||||
}
|
||||
}
|
||||
|
||||
function getOrCreateProfileState(pid: number, tid: number): TraceEventProfileState {
|
||||
// We zero-pad the PID and TID to make sorting them by pid/tid pair later easier.
|
||||
const pidTid = `${zeroPad('' + pid, 10)}:${zeroPad('' + tid, 10)}`
|
||||
|
||||
let state = stateByPidTid.get(pidTid)
|
||||
if (state != null) return state
|
||||
let profile = new CallTreeProfileBuilder()
|
||||
state = {profile, eventStack: []}
|
||||
profile.setValueFormatter(new TimeFormatter('microseconds'))
|
||||
stateByPidTid.set(pidTid, state)
|
||||
|
||||
const processName = processNamesByPid.get(pid)
|
||||
const threadName = threadNamesByPidTid.get(`${pid}:${tid}`)
|
||||
|
||||
if (processName != null && threadName != null) {
|
||||
profile.setName(`${processName} (pid ${pid}), ${threadName} (tid ${tid})`)
|
||||
} else if (processName != null) {
|
||||
profile.setName(`${processName} (pid ${pid}, tid ${tid})`)
|
||||
} else if (threadName != null) {
|
||||
profile.setName(`${threadName} (pid ${pid}, tid ${tid})`)
|
||||
} else {
|
||||
profile.setName(`pid ${pid}, tid ${tid}`)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
for (let ev of durationEvents) {
|
||||
const {profile, eventStack} = getOrCreateProfileState(ev.pid, ev.tid)
|
||||
const key = keyForEvent(ev)
|
||||
const frameInfo: FrameInfo = {
|
||||
key: key,
|
||||
name: key,
|
||||
}
|
||||
switch (ev.ph) {
|
||||
case 'B':
|
||||
eventStack.push(ev)
|
||||
profile.enterFrame(frameInfo, ev.ts)
|
||||
break
|
||||
|
||||
case 'E':
|
||||
const lastEvent = lastOf(eventStack)
|
||||
if (lastEvent != null && lastEvent.name === ev.name) {
|
||||
profile.leaveFrame(frameInfo, ev.ts)
|
||||
eventStack.pop()
|
||||
} else {
|
||||
console.warn(
|
||||
'Event discarded because it did not match top-of-stack. Discarded event:',
|
||||
ev,
|
||||
'Top of stack:',
|
||||
lastEvent,
|
||||
)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
const _exhaustiveCheck: never = ev
|
||||
return _exhaustiveCheck
|
||||
}
|
||||
}
|
||||
|
||||
// For now, we just sort processes by pid & tid.
|
||||
// TODO: The standard specifies that metadata events with the name
|
||||
// "process_sort_index" and "thread_sort_index" can be used to influence the
|
||||
// order, but for simplicity we'll ignore that until someone complains :)
|
||||
const profilePairs = Array.from(stateByPidTid.entries())
|
||||
sortBy(profilePairs, p => p[0])
|
||||
|
||||
return {name: '', indexToView: 0, profiles: profilePairs.map(p => p[1].profile)}
|
||||
}
|
||||
|
||||
function isTraceEventList(maybeEventList: any): maybeEventList is TraceEvent[] {
|
||||
if (!Array.isArray(maybeEventList)) return false
|
||||
if (maybeEventList.length === 0) return false
|
||||
|
||||
// Both ph and ts should be provided for every event. In theory, many other
|
||||
// fields are mandatory, but without these fields, we won't usefully be able
|
||||
// to import the data, so we'll rely upon these.
|
||||
for (let el of maybeEventList) {
|
||||
if (!('ph' in el)) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch (el.ph) {
|
||||
case 'B':
|
||||
case 'E':
|
||||
case 'X':
|
||||
// All B, E, and X events must have a timestamp specified, otherwise we
|
||||
// won't be able to import correctly.
|
||||
if (!('ts' in el)) {
|
||||
return false
|
||||
}
|
||||
|
||||
case 'M':
|
||||
// It's explicitly okay for "M" (metadata) events not to specify a "ts"
|
||||
// field, since usually there is no logical timestamp for them to have
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function isTraceEventObject(
|
||||
maybeTraceEventObject: any,
|
||||
): maybeTraceEventObject is {traceEvents: TraceEvent[]} {
|
||||
if (!('traceEvents' in maybeTraceEventObject)) return false
|
||||
return isTraceEventList(maybeTraceEventObject['traceEvents'])
|
||||
}
|
||||
|
||||
export function isTraceEventFormatted(
|
||||
rawProfile: any,
|
||||
): rawProfile is {traceEvents: TraceEvent[]} | TraceEvent[] {
|
||||
// We're only going to support the JSON formatted profiles for now.
|
||||
// The spec also discusses support for data embedded in ftrace supported data: https://lwn.net/Articles/365835/.
|
||||
|
||||
return isTraceEventObject(rawProfile) || isTraceEventList(rawProfile)
|
||||
}
|
||||
|
||||
export function importTraceEvents(
|
||||
rawProfile: {traceEvents: TraceEvent[]} | TraceEvent[],
|
||||
): ProfileGroup {
|
||||
if (isTraceEventObject(rawProfile)) {
|
||||
return eventListToProfileGroup(rawProfile.traceEvents)
|
||||
} else if (isTraceEventList(rawProfile)) {
|
||||
return eventListToProfileGroup(rawProfile)
|
||||
} else {
|
||||
const _exhaustiveCheck: never = rawProfile
|
||||
return _exhaustiveCheck
|
||||
}
|
||||
}
|
||||
+20
-5
@@ -51,15 +51,30 @@ export class MaybeCompressedDataReader implements ProfileDataSource {
|
||||
|
||||
async readAsText(): Promise<string> {
|
||||
const buffer = await this.readAsArrayBuffer()
|
||||
let ret: string = ''
|
||||
|
||||
// By default, we assume the file is utf-8 encoded.
|
||||
let encoding = 'utf-8'
|
||||
|
||||
const array = new Uint8Array(buffer)
|
||||
if (array.length > 2) {
|
||||
if (array[0] === 0xff && array[1] === 0xfe) {
|
||||
// UTF-16, Little Endian encoding
|
||||
encoding = 'utf-16le'
|
||||
} else if (array[0] === 0xfe && array[1] === 0xff) {
|
||||
// UTF-16, Big Endian encoding
|
||||
encoding = 'utf-16be'
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') {
|
||||
const decoder = new TextDecoder()
|
||||
const decoder = new TextDecoder(encoding)
|
||||
return decoder.decode(buffer)
|
||||
} else {
|
||||
// JavaScript strings are UTF-16 encoded, but we're reading data
|
||||
// from disk that we're going to asusme is UTF-8 encoded.
|
||||
const array = new Uint8Array(buffer)
|
||||
// JavaScript strings are UTF-16 encoded, but we're reading data from disk
|
||||
// that we're going to blindly assume it's ASCII encoded. This codepath
|
||||
// only exists for older browser support.
|
||||
console.warn('This browser does not support TextDecoder. Decoding text as ASCII.')
|
||||
let ret: string = ''
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
ret += String.fromCharCode(array[i])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import {CPUProfile, CPUProfileNode} from './chrome'
|
||||
|
||||
/**
|
||||
* This importer handles an old format used by the C++ API of V8. This format is still used by v8-profiler-node8.
|
||||
* There are two differences between the two formats:
|
||||
* - Nodes are a tree in the old format and a flat array in the new format
|
||||
* - Weights are timestamps in the old format and deltas in the new format.
|
||||
*
|
||||
* For more information, see https://github.com/hyj1991/v8-profiler-node8
|
||||
*/
|
||||
|
||||
interface OldCPUProfileNode {
|
||||
functionName: string
|
||||
lineNumber: number
|
||||
scriptId: string
|
||||
url: string
|
||||
hitCount: number
|
||||
bailoutReason: string
|
||||
id: number
|
||||
children: OldCPUProfileNode[]
|
||||
}
|
||||
|
||||
export interface OldCPUProfile {
|
||||
startTime: number
|
||||
endTime: number
|
||||
head: OldCPUProfileNode
|
||||
samples: number[]
|
||||
timestamps: number[]
|
||||
}
|
||||
|
||||
function treeToArray(root: OldCPUProfileNode): CPUProfileNode[] {
|
||||
const nodes: CPUProfileNode[] = []
|
||||
function visit(node: OldCPUProfileNode) {
|
||||
nodes.push({
|
||||
id: node.id,
|
||||
callFrame: {
|
||||
columnNumber: 0,
|
||||
functionName: node.functionName,
|
||||
lineNumber: node.lineNumber,
|
||||
scriptId: node.scriptId,
|
||||
url: node.url,
|
||||
},
|
||||
hitCount: node.hitCount,
|
||||
children: node.children.map(child => child.id),
|
||||
})
|
||||
node.children.forEach(visit)
|
||||
}
|
||||
visit(root)
|
||||
return nodes
|
||||
}
|
||||
|
||||
function timestampsToDeltas(timestamps: number[], startTime: number): number[] {
|
||||
return timestamps.map((timestamp, index) => {
|
||||
const lastTimestamp = index === 0 ? startTime * 1000000 : timestamps[index - 1]
|
||||
return timestamp - lastTimestamp
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the old tree-based format to the new flat-array based format
|
||||
*/
|
||||
export function chromeTreeToNodes(content: OldCPUProfile): CPUProfile {
|
||||
// Note that both startTime and endTime are now in microseconds
|
||||
return {
|
||||
samples: content.samples,
|
||||
startTime: content.startTime * 1000000,
|
||||
endTime: content.endTime * 1000000,
|
||||
nodes: treeToArray(content.head),
|
||||
timeDeltas: timestampsToDeltas(content.timestamps, content.startTime),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Versions of node before 10 had an unstable sort. This isn't really an issue in browsers
|
||||
// that speedscope supports, but for the purposes of supporting node 10, we'll polyfill
|
||||
// a stable sort to make the tests pass.
|
||||
//
|
||||
// See:
|
||||
// - https://v8.dev/features/stable-sort
|
||||
// - https://v8.dev/blog/array-sort
|
||||
// - https://github.com/jlfwong/speedscope/pull/254#issuecomment-575116995
|
||||
//
|
||||
// Once we stop supporting node 10, this can be removed.
|
||||
//
|
||||
// An alternative would be to change our sort implementation to be stable by definition
|
||||
// rather than relying upon native sort being stable. I don't want to do that because
|
||||
// we'd take a perf hit.
|
||||
//
|
||||
// Because we're not going to use this in our actual build, it's okay for this
|
||||
// to be inefficient.
|
||||
|
||||
;(function () {
|
||||
const nodeVersion = process.versions.node
|
||||
const versionParts = nodeVersion.split('.')
|
||||
const majorVersion = parseInt(versionParts[0], 10)
|
||||
|
||||
if (majorVersion > 10) {
|
||||
// Don't need to do the patch for newer node versions
|
||||
return
|
||||
}
|
||||
|
||||
const defaultCompareFunction = (a, b) => {
|
||||
const sa = '' + a
|
||||
const sb = '' + b
|
||||
if (sa < sb) return -1
|
||||
if (sa > sb) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
const originalSort = Array.prototype.sort
|
||||
Array.prototype.sort = function (compareFunction) {
|
||||
const arrayWithIndices = this.map((x, i) => [x, i])
|
||||
originalSort.call(arrayWithIndices, (a, b) => {
|
||||
if (!compareFunction) {
|
||||
compareFunction = defaultCompareFunction
|
||||
}
|
||||
const res = compareFunction(a[0], b[0])
|
||||
if (res !== 0) return res
|
||||
return a[1] < b[1] ? -1 : 1
|
||||
})
|
||||
this.splice(0, this.length, ...arrayWithIndices.map(x => x[0]))
|
||||
return this
|
||||
}
|
||||
})()
|
||||
;(function () {
|
||||
// TextDecoder is a global API in browsers, but an imported API in node.
|
||||
//
|
||||
// Let's emulate it being a global API during tests.
|
||||
global.TextDecoder = require('util').TextDecoder
|
||||
})()
|
||||
@@ -211,3 +211,7 @@ Object {
|
||||
exports[`importSpeedscopeProfiles 0.6.0 multiple profiles: indexToView 1`] = `1`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.6.0 multiple profiles: profileGroup.name 1`] = `"Two Samples"`;
|
||||
|
||||
exports[`importSpeedscopeProfiles invalid due to incomplete trace 1`] = `"Tried to complete profile construction with a non-empty stack"`;
|
||||
|
||||
exports[`importSpeedscopeProfiles invalid due to out of order events 1`] = `"Tried to leave frame \\"B\\" while frame \\"A\\" was at the top at 4"`;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// This file contains a collection of classes which make it easier to perform
|
||||
// batch rendering of Canvas2D primitives. The advantage of this over just doing
|
||||
// ctx.beginPath() ... ctx.rect(...) ... ctx.endPath() is that you can construct
|
||||
// several different batch renderers are the same time, then decide on their
|
||||
// paint order at the end.
|
||||
//
|
||||
// See FlamechartPanZoomView.renderOverlays for an example of how this is used.
|
||||
|
||||
export interface TextArgs {
|
||||
text: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export class BatchCanvasTextRenderer {
|
||||
private argsBatch: TextArgs[] = []
|
||||
|
||||
text(args: TextArgs) {
|
||||
this.argsBatch.push(args)
|
||||
}
|
||||
|
||||
fill(ctx: CanvasRenderingContext2D, color: string) {
|
||||
if (this.argsBatch.length === 0) return
|
||||
ctx.fillStyle = color
|
||||
for (let args of this.argsBatch) {
|
||||
ctx.fillText(args.text, args.x, args.y)
|
||||
}
|
||||
this.argsBatch = []
|
||||
}
|
||||
}
|
||||
|
||||
export interface RectArgs {
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
export class BatchCanvasRectRenderer {
|
||||
private argsBatch: RectArgs[] = []
|
||||
|
||||
rect(args: RectArgs) {
|
||||
this.argsBatch.push(args)
|
||||
}
|
||||
|
||||
private drawPath(ctx: CanvasRenderingContext2D) {
|
||||
ctx.beginPath()
|
||||
for (let args of this.argsBatch) {
|
||||
ctx.rect(args.x, args.y, args.w, args.h)
|
||||
}
|
||||
ctx.closePath()
|
||||
this.argsBatch = []
|
||||
}
|
||||
|
||||
fill(ctx: CanvasRenderingContext2D, color: string) {
|
||||
if (this.argsBatch.length === 0) return
|
||||
ctx.fillStyle = color
|
||||
this.drawPath(ctx)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
stroke(ctx: CanvasRenderingContext2D, color: string, lineWidth: number) {
|
||||
if (this.argsBatch.length === 0) return
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = lineWidth
|
||||
this.drawPath(ctx)
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -15,19 +15,19 @@ export class Color {
|
||||
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue
|
||||
|
||||
const hPrime = H / 60
|
||||
const X = C * (1 - Math.abs(hPrime % 2 - 1))
|
||||
const X = C * (1 - Math.abs((hPrime % 2) - 1))
|
||||
const [R1, G1, B1] =
|
||||
hPrime < 1
|
||||
? [C, X, 0]
|
||||
: hPrime < 2
|
||||
? [X, C, 0]
|
||||
: hPrime < 3
|
||||
? [0, C, X]
|
||||
: hPrime < 4
|
||||
? [0, X, C]
|
||||
: hPrime < 5
|
||||
? [X, 0, C]
|
||||
: [C, 0, X]
|
||||
? [X, C, 0]
|
||||
: hPrime < 3
|
||||
? [0, C, X]
|
||||
: hPrime < 4
|
||||
? [0, X, C]
|
||||
: hPrime < 5
|
||||
? [X, 0, C]
|
||||
: [C, 0, X]
|
||||
|
||||
const m = L - (0.3 * R1 + 0.59 * G1 + 0.11 * B1)
|
||||
|
||||
|
||||
@@ -11,7 +11,13 @@ test('importEmscriptenSymbolMap', () => {
|
||||
'c:C',
|
||||
].join('\n'),
|
||||
),
|
||||
).toEqual(new Map([['a', 'A'], ['b', 'B'], ['c', 'C']]))
|
||||
).toEqual(
|
||||
new Map([
|
||||
['a', 'A'],
|
||||
['b', 'B'],
|
||||
['c', 'C'],
|
||||
]),
|
||||
)
|
||||
|
||||
// Valid symbol map with trailing newline
|
||||
expect(
|
||||
@@ -25,7 +31,14 @@ test('importEmscriptenSymbolMap', () => {
|
||||
'',
|
||||
].join('\n'),
|
||||
),
|
||||
).toEqual(new Map([['a', 'A'], ['b', 'B'], ['c', 'C'], ['d', 'D-D']]))
|
||||
).toEqual(
|
||||
new Map([
|
||||
['a', 'A'],
|
||||
['b', 'B'],
|
||||
['c', 'C'],
|
||||
['d', 'D-D'],
|
||||
]),
|
||||
)
|
||||
|
||||
// Valid symbol map with non-alpha characters
|
||||
expect(importEmscriptenSymbolMap('u6:__ZN8tinyxml210XMLCommentD0Ev\n')).toEqual(
|
||||
@@ -41,6 +54,10 @@ test('importEmscriptenSymbolMap', () => {
|
||||
'1:B',
|
||||
'2:C',
|
||||
'3:D-D',
|
||||
'4:a\\20b',
|
||||
'5:a\\2',
|
||||
'6:a\\3z',
|
||||
'7:a\\20b\\20c',
|
||||
].join('\n'),
|
||||
),
|
||||
).toEqual(
|
||||
@@ -49,6 +66,10 @@ test('importEmscriptenSymbolMap', () => {
|
||||
['wasm-function[1]', 'B'],
|
||||
['wasm-function[2]', 'C'],
|
||||
['wasm-function[3]', 'D-D'],
|
||||
['wasm-function[4]', 'a b'],
|
||||
['wasm-function[5]', 'a\\2'],
|
||||
['wasm-function[6]', 'a\\3z'],
|
||||
['wasm-function[7]', 'a b c'],
|
||||
]),
|
||||
)
|
||||
|
||||
|
||||
+13
-3
@@ -1,5 +1,15 @@
|
||||
type EmscriptenSymbolMap = Map<string, string>
|
||||
|
||||
// Returns `input` with hex escapes expanded (e.g. `\20` becomes ` `.)
|
||||
//
|
||||
// NOTE: This will fail to ignore escaped backslahes (e.g. `\\20`).
|
||||
function unescapeHex(input: string): string {
|
||||
return input.replace(/\\([a-fA-F0-9]{2})/g, (_match, group) => {
|
||||
const scalar = parseInt(group, 16)
|
||||
return String.fromCharCode(scalar)
|
||||
})
|
||||
}
|
||||
|
||||
// This imports symbol maps generated by emscripten using the "--emit-symbol-map" flag.
|
||||
// It allows you to visualize a profile captured in a release build as long as you also
|
||||
// have the associated symbol map. To do this, first drop the profile into speedscope
|
||||
@@ -14,21 +24,21 @@ export function importEmscriptenSymbolMap(contents: string): EmscriptenSymbolMap
|
||||
if (!lines.length) return null
|
||||
|
||||
const map: EmscriptenSymbolMap = new Map()
|
||||
const intRegex = /^(\d+):([\$\w-]+)$/
|
||||
const intRegex = /^(\d+):(.+)$/
|
||||
const idRegex = /^([\$\w]+):([\$\w-]+)$/
|
||||
|
||||
for (const line of lines) {
|
||||
// Match lines like "103:__ZN8tinyxml210XMLCommentD0Ev"
|
||||
const intMatch = intRegex.exec(line)
|
||||
if (intMatch) {
|
||||
map.set(`wasm-function[${intMatch[1]}]`, intMatch[2])
|
||||
map.set(`wasm-function[${intMatch[1]}]`, unescapeHex(intMatch[2]))
|
||||
continue
|
||||
}
|
||||
|
||||
// Match lines like "u6:__ZN8tinyxml210XMLCommentD0Ev"
|
||||
const idMatch = idRegex.exec(line)
|
||||
if (idMatch) {
|
||||
map.set(idMatch[1], idMatch[2])
|
||||
map.set(idMatch[1], unescapeHex(idMatch[2]))
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {checkProfileSnapshot} from './test-utils'
|
||||
import {checkProfileSnapshot, expectImportFailure} from './test-utils'
|
||||
|
||||
describe('importSpeedscopeProfiles', async () => {
|
||||
describe('importSpeedscopeProfiles', () => {
|
||||
test('0.0.1 evented profile', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/speedscope/0.0.1/simple.speedscope.json')
|
||||
})
|
||||
@@ -12,4 +12,13 @@ describe('importSpeedscopeProfiles', async () => {
|
||||
test('0.6.0 multiple profiles', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/speedscope/0.6.0/two-sampled.speedscope.json')
|
||||
})
|
||||
|
||||
test('invalid due to out of order events', async () => {
|
||||
// See: https://github.com/jlfwong/speedscope/issues/272
|
||||
await expectImportFailure('./sample/profiles/speedscope/invalid/out-of-order-events.json')
|
||||
})
|
||||
|
||||
test('invalid due to incomplete trace', async () => {
|
||||
await expectImportFailure('./sample/profiles/speedscope/invalid/incomplete-trace.json')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -138,7 +138,10 @@ function importSpeedscopeProfile(
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const stack = samples[i]
|
||||
const weight = weights[i]
|
||||
profile.appendSampleWithWeight(stack.map(n => frameInfos[n]), weight)
|
||||
profile.appendSampleWithWeight(
|
||||
stack.map(n => frameInfos[n]),
|
||||
weight,
|
||||
)
|
||||
}
|
||||
|
||||
return profile.build()
|
||||
|
||||
+22
-1
@@ -1,7 +1,7 @@
|
||||
import {Frame, CallTreeNode} from './profile'
|
||||
|
||||
import {lastOf} from './utils'
|
||||
import {clamp} from './math'
|
||||
import {clamp, Rect, Vec2} from './math'
|
||||
|
||||
export interface FlamechartFrame {
|
||||
node: CallTreeNode
|
||||
@@ -90,6 +90,27 @@ export class Flamechart {
|
||||
return clamp(viewportWidth, minWidth, maxWidth)
|
||||
}
|
||||
|
||||
// Given a desired config-space viewport rectangle, clamp the rectangle so
|
||||
// that it fits within the given flamechart. This prevents the viewport from
|
||||
// extending past the bounds of the flamechart or zooming in too far.
|
||||
getClampedConfigSpaceViewportRect({
|
||||
configSpaceViewportRect,
|
||||
renderInverted,
|
||||
}: {
|
||||
configSpaceViewportRect: Rect
|
||||
renderInverted?: boolean
|
||||
}) {
|
||||
const configSpaceSize = new Vec2(this.getTotalWeight(), this.getLayers().length)
|
||||
const width = this.getClampedViewportWidth(configSpaceViewportRect.size.x)
|
||||
const size = configSpaceViewportRect.size.withX(width)
|
||||
const origin = Vec2.clamp(
|
||||
configSpaceViewportRect.origin,
|
||||
new Vec2(0, renderInverted ? 0 : -1),
|
||||
Vec2.max(Vec2.zero, configSpaceSize.minus(size).plus(new Vec2(0, 1))),
|
||||
)
|
||||
return new Rect(origin, configSpaceViewportRect.size.withX(width))
|
||||
}
|
||||
|
||||
constructor(private source: FlamechartDataSource) {
|
||||
const stack: FlamechartFrame[] = []
|
||||
const openFrame = (node: CallTreeNode, value: number) => {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import {fuzzyMatchStrings} from './fuzzy-find'
|
||||
import {sortBy} from './utils'
|
||||
|
||||
function assertMatches(texts: string[], pattern: string, expectedResults: string[]) {
|
||||
const results: {score: number; highlighted: string}[] = []
|
||||
|
||||
for (let text of texts) {
|
||||
const match = fuzzyMatchStrings(text, pattern)
|
||||
if (match == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
let highlighted = ''
|
||||
let last = 0
|
||||
for (let range of match.matchedRanges) {
|
||||
highlighted += `${text.slice(last, range[0])}[${text.slice(range[0], range[1])}]`
|
||||
last = range[1]
|
||||
}
|
||||
highlighted += text.slice(last)
|
||||
|
||||
results.push({score: match.score, highlighted})
|
||||
}
|
||||
|
||||
// Sort scores in descending order
|
||||
sortBy(results, r => -r.score)
|
||||
expect(results.map(r => r.highlighted)).toEqual(expectedResults)
|
||||
}
|
||||
|
||||
function assertMatch(text: string, pattern: string, expected: string) {
|
||||
assertMatches([text], pattern, [expected])
|
||||
}
|
||||
|
||||
function assertNoMatch(text: string, pattern: string) {
|
||||
assertMatches([text], pattern, [])
|
||||
}
|
||||
|
||||
describe('fuzzyMatchStrings', () => {
|
||||
test('no match', () => {
|
||||
assertNoMatch('a', 'b')
|
||||
assertNoMatch('aa', 'ab')
|
||||
assertNoMatch('a', 'aa')
|
||||
assertNoMatch('ca', 'ac')
|
||||
})
|
||||
|
||||
test('full text match', () => {
|
||||
assertMatch('hello', 'hello', '[hello]')
|
||||
assertMatch('multiple words', 'multiple words', '[multiple words]')
|
||||
})
|
||||
|
||||
test('case sensitivity', () => {
|
||||
assertMatch('HELLO', 'hello', '[HELLO]')
|
||||
assertMatch('Hello', 'hello', '[Hello]')
|
||||
assertNoMatch('hello', 'Hello')
|
||||
assertNoMatch('hello', 'HELLO')
|
||||
})
|
||||
|
||||
test('multiple occurrences', () => {
|
||||
assertMatch('hello hello', 'hello', '[hello] hello')
|
||||
assertMatch('hellohello', 'hello', '[hello]hello')
|
||||
})
|
||||
|
||||
test('prefer earlier matches', () => {
|
||||
assertMatches(['cab', 'ab'], 'ab', ['[ab]', 'c[ab]'])
|
||||
})
|
||||
|
||||
test('prefer shorter matches', () => {
|
||||
assertMatches(['abbc', 'abc', 'abbbc'], 'ac', ['[a]b[c]', '[a]bb[c]', '[a]bbb[c]'])
|
||||
})
|
||||
|
||||
test('prefer word boundaries', () => {
|
||||
assertMatches(['abc', 'a c'], 'ac', ['[a] [c]', '[a]b[c]'])
|
||||
})
|
||||
|
||||
test('prefer camelCase matches', () => {
|
||||
assertMatches(['downtown', 'OutNode'], 'n', ['Out[N]ode', 'dow[n]town'])
|
||||
})
|
||||
|
||||
test('prefer number prefix matches', () => {
|
||||
assertMatches(['211', 'a123'], '1', ['a[1]23', '2[1]1'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* This file contains an implementation of fuzzy string matching.
|
||||
*/
|
||||
|
||||
export interface FuzzyMatch {
|
||||
// List of [start, end] indices in the haystack string that match the needle string
|
||||
matchedRanges: [number, number][]
|
||||
|
||||
// The score of the match for relative ranking. Higher scores indicate
|
||||
// "better" matches.
|
||||
score: number
|
||||
}
|
||||
|
||||
export function fuzzyMatchStrings(text: string, pattern: string): FuzzyMatch | null {
|
||||
return fzfFuzzyMatchV1(text, pattern)
|
||||
}
|
||||
|
||||
// The implementation here is based on FuzzyMatchV1, as described here:
|
||||
// https://github.com/junegunn/fzf/blob/f81feb1e69e5cb75797d50817752ddfe4933cd68/src/algo/algo.go#L8-L15
|
||||
//
|
||||
// This is a hand-port to better understand what the code is doing and for added
|
||||
// clarity.
|
||||
//
|
||||
// Capitalized letters only match capitalized letters, but lower-case letters
|
||||
// match both.
|
||||
//
|
||||
// Note: fzf includes a normalization table for homoglyphs. I'm going to ignore that too
|
||||
// https://github.com/junegunn/fzf/blob/master/src/algo/normalize.go
|
||||
const charCodeLowerA = 'a'.charCodeAt(0)
|
||||
const charCodeLowerZ = 'z'.charCodeAt(0)
|
||||
const charCodeUpperA = 'A'.charCodeAt(0)
|
||||
const charCodeUpperZ = 'Z'.charCodeAt(0)
|
||||
const charCodeDigit0 = '0'.charCodeAt(0)
|
||||
const charCodeDigit9 = '9'.charCodeAt(0)
|
||||
|
||||
enum fzfCharClass {
|
||||
charNonWord,
|
||||
charLower,
|
||||
charUpper,
|
||||
charNumber,
|
||||
}
|
||||
|
||||
function fzfCharClassOf(char: string): fzfCharClass {
|
||||
const code = char.charCodeAt(0)
|
||||
if (charCodeLowerA <= code && code <= charCodeLowerZ) {
|
||||
return fzfCharClass.charLower
|
||||
} else if (charCodeUpperA <= code && code <= charCodeUpperZ) {
|
||||
return fzfCharClass.charUpper
|
||||
} else if (charCodeDigit0 <= code && code <= charCodeDigit9) {
|
||||
return fzfCharClass.charNumber
|
||||
}
|
||||
return fzfCharClass.charNonWord
|
||||
}
|
||||
|
||||
function charsMatch(textChar: string, patternChar: string): boolean {
|
||||
if (textChar === patternChar) return true
|
||||
|
||||
const patternCharCode = patternChar.charCodeAt(0)
|
||||
if (charCodeLowerA <= patternCharCode && patternCharCode <= charCodeLowerZ) {
|
||||
return textChar.charCodeAt(0) === patternCharCode - charCodeLowerA + charCodeUpperA
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function fzfFuzzyMatchV1(text: string, pattern: string): FuzzyMatch | null {
|
||||
if (pattern.length == 0) {
|
||||
return {matchedRanges: [], score: 0}
|
||||
}
|
||||
|
||||
// I removed the fzfAsciiFuzzyIndex code because it's not actually clear to
|
||||
// me that it's a very helpful optimization.
|
||||
|
||||
let pidx = 0
|
||||
let sidx = -1
|
||||
let eidx = -1
|
||||
|
||||
let lenRunes = text.length
|
||||
let lenPattern = pattern.length
|
||||
|
||||
// Forward pass: scan over the text pattern, identifying the earliest start
|
||||
// and the latest end to consider.
|
||||
for (let index = 0; index < lenRunes; index++) {
|
||||
let char = text[index]
|
||||
let pchar = pattern[pidx]
|
||||
if (charsMatch(char, pchar)) {
|
||||
if (sidx < 0) {
|
||||
sidx = index
|
||||
}
|
||||
pidx++
|
||||
if (pidx == lenPattern) {
|
||||
// We found the last character in the pattern! eidx is exclusive, so
|
||||
// we'll set it to the current index + 1.
|
||||
eidx = index + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (eidx == -1) {
|
||||
// We couldn't find all the characters in the pattern. No match.
|
||||
return null
|
||||
}
|
||||
|
||||
// Assuming we found all the characters in the pattern, perform the backwards
|
||||
// pass.
|
||||
pidx--
|
||||
for (let index = eidx - 1; index >= sidx; index--) {
|
||||
const char = text[index]
|
||||
const pchar = pattern[pidx]
|
||||
if (charsMatch(char, pchar)) {
|
||||
pidx--
|
||||
if (pidx < 0) {
|
||||
// We found the first character of the pattern, scanning
|
||||
// backwards. This *may* have narrowed the match further.
|
||||
// For example, for the following inputs:
|
||||
//
|
||||
// text = "xxx a b c abc xxx"
|
||||
// pattern = "abc"
|
||||
//
|
||||
// For the forward pass, you get:
|
||||
//
|
||||
// "xxx a b c abc xxx"
|
||||
// start^ ^end
|
||||
//
|
||||
// But after the backward pass, we can narrow this to:
|
||||
//
|
||||
// "xxx a b c abc xxx"
|
||||
// start^ ^end
|
||||
sidx = index
|
||||
return fzfCalculateScore(text, pattern, sidx, eidx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This should be unreachable.
|
||||
throw new Error('Implementation error. This must be a bug in fzfFuzzyMatchV1')
|
||||
}
|
||||
|
||||
const fzfScoreMatch = 16
|
||||
const fzfScoreGapStart = -3
|
||||
const fzfScoreGapExtension = -1
|
||||
const fzfBonusBoundary = fzfScoreMatch / 2
|
||||
const fzfBonusNonWord = fzfScoreMatch / 2
|
||||
const fzfBonusCamel123 = fzfBonusBoundary + fzfScoreGapExtension
|
||||
const fzfBonusConsecutive = -(fzfScoreGapStart + fzfScoreGapExtension)
|
||||
const fzfBonusFirstCharMultiplier = 2
|
||||
|
||||
function bonusFor(prevClass: fzfCharClass, curClass: fzfCharClass): number {
|
||||
if (prevClass === fzfCharClass.charNonWord && curClass !== fzfCharClass.charNonWord) {
|
||||
// Prefer matching at word boundaries
|
||||
//
|
||||
// This should prefer "a c" over "abc" for a pattern of "ac".
|
||||
return fzfBonusBoundary
|
||||
}
|
||||
|
||||
if (
|
||||
(prevClass === fzfCharClass.charLower && curClass == fzfCharClass.charUpper) ||
|
||||
(prevClass !== fzfCharClass.charNumber && curClass == fzfCharClass.charNumber)
|
||||
) {
|
||||
// Prefer matching at the transition point between lower & upper for camelCase,
|
||||
// and from transition from letter to number for identifiers like letter123.
|
||||
//
|
||||
// This should prefer "OutNode" over "phone" for a pattern of "n",
|
||||
// and "abc123" over "x211" for a pattern of "1".
|
||||
return fzfBonusCamel123
|
||||
}
|
||||
|
||||
if (curClass === fzfCharClass.charNonWord) {
|
||||
return fzfBonusNonWord
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function fzfCalculateScore(text: string, pattern: string, sidx: number, eidx: number): FuzzyMatch {
|
||||
let pidx = 0
|
||||
let score = 0
|
||||
let inGap = false
|
||||
let consecutive = 0
|
||||
let firstBonus = 0
|
||||
let pos: number[] = new Array(pattern.length)
|
||||
let prevClass = fzfCharClass.charNonWord
|
||||
|
||||
if (sidx > 0) {
|
||||
prevClass = fzfCharClassOf(text[sidx - 1])
|
||||
}
|
||||
for (let idx = sidx; idx < eidx; idx++) {
|
||||
let char = text[idx]
|
||||
let curClass = fzfCharClassOf(char)
|
||||
if (charsMatch(char, pattern[pidx])) {
|
||||
pos[pidx] = idx
|
||||
score += fzfScoreMatch
|
||||
let bonus = bonusFor(prevClass, curClass)
|
||||
if (consecutive == 0) {
|
||||
firstBonus = bonus
|
||||
} else {
|
||||
// Break consecutive chunk
|
||||
if (bonus === fzfBonusBoundary) {
|
||||
firstBonus = bonus
|
||||
}
|
||||
bonus = Math.max(bonus, firstBonus, fzfBonusConsecutive)
|
||||
}
|
||||
if (pidx === 0) {
|
||||
score += bonus * fzfBonusFirstCharMultiplier
|
||||
} else {
|
||||
score += bonus
|
||||
}
|
||||
inGap = false
|
||||
consecutive++
|
||||
pidx++
|
||||
} else {
|
||||
if (inGap) {
|
||||
// Penalize gaps (this bonus is negative)
|
||||
score += fzfScoreGapExtension
|
||||
} else {
|
||||
// Penalize the beginning of gaps more harshly
|
||||
score += fzfScoreGapStart
|
||||
}
|
||||
inGap = true
|
||||
consecutive = 0
|
||||
firstBonus = 0
|
||||
}
|
||||
prevClass = curClass
|
||||
}
|
||||
|
||||
if (pidx !== pattern.length) {
|
||||
throw new Error(
|
||||
'fzfCalculateScore should only be called when pattern is found between sidx and eidx',
|
||||
)
|
||||
}
|
||||
|
||||
let matchedRanges: [number, number][] = [[pos[0], pos[0] + 1]]
|
||||
for (let i = 1; i < pos.length; i++) {
|
||||
const curPos = pos[i]
|
||||
const curRange = matchedRanges[matchedRanges.length - 1]
|
||||
if (curRange[1] === curPos) {
|
||||
curRange[1] = curPos + 1
|
||||
} else {
|
||||
matchedRanges.push([curPos, curPos + 1])
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
score,
|
||||
matchedRanges,
|
||||
}
|
||||
}
|
||||
+25
-45
@@ -13,9 +13,10 @@ test('clamp', () => {
|
||||
// Change this to jsc.integer to debug failures more easily
|
||||
let numericType = jsc.number
|
||||
|
||||
const arbitraryVec2 = jsc
|
||||
.record({x: jsc.integer, y: numericType})
|
||||
.smap(v => new Vec2(v.x, v.y), v => v)
|
||||
const arbitraryVec2 = jsc.record({x: jsc.integer, y: numericType}).smap(
|
||||
v => new Vec2(v.x, v.y),
|
||||
v => v,
|
||||
)
|
||||
|
||||
const positiveVec2 = jsc.suchthat(arbitraryVec2, v => v.x > 0 && v.y > 0)
|
||||
|
||||
@@ -28,23 +29,25 @@ const arbitraryTransform = jsc
|
||||
m11: numericType,
|
||||
m12: numericType,
|
||||
})
|
||||
.smap(t => new AffineTransform(t.m00, t.m01, t.m02, t.m10, t.m11, t.m12), t => t)
|
||||
.smap(
|
||||
t => new AffineTransform(t.m00, t.m01, t.m02, t.m10, t.m11, t.m12),
|
||||
t => t,
|
||||
)
|
||||
|
||||
const invertibleTransform = jsc.suchthat(arbitraryTransform, t => t.det() != 0)
|
||||
|
||||
const simpleTransform = jsc.suchthat(
|
||||
jsc
|
||||
.record({scale: arbitraryVec2, translation: arbitraryVec2})
|
||||
.smap(
|
||||
t => AffineTransform.withScale(t.scale).withTranslation(t.translation),
|
||||
t => ({scale: t.getScale(), translation: t.getTranslation()}),
|
||||
),
|
||||
jsc.record({scale: arbitraryVec2, translation: arbitraryVec2}).smap(
|
||||
t => AffineTransform.withScale(t.scale).withTranslation(t.translation),
|
||||
t => ({scale: t.getScale(), translation: t.getTranslation()}),
|
||||
),
|
||||
t => t.det() != 0,
|
||||
)
|
||||
|
||||
const arbitraryRect = jsc
|
||||
.record({origin: arbitraryVec2, size: positiveVec2})
|
||||
.smap(r => new Rect(r.origin, r.size), r => r)
|
||||
const arbitraryRect = jsc.record({origin: arbitraryVec2, size: positiveVec2}).smap(
|
||||
r => new Rect(r.origin, r.size),
|
||||
r => r,
|
||||
)
|
||||
|
||||
describe('Vec2', () => {
|
||||
test('constructor', () => {
|
||||
@@ -263,51 +266,35 @@ describe('AffineTransform', () => {
|
||||
expect(new AffineTransform(0, 0, 0, 0, 0, 0).inverted()).toBe(null)
|
||||
|
||||
jsc.assertForall(invertibleTransform, t => {
|
||||
return t
|
||||
.inverted()!
|
||||
.inverted()!
|
||||
.approxEquals(t)
|
||||
return t.inverted()!.inverted()!.approxEquals(t)
|
||||
})
|
||||
})
|
||||
|
||||
test('translation', () => {
|
||||
jsc.assertForall(arbitraryTransform, arbitraryVec2, (t, v1) => {
|
||||
return t
|
||||
.withTranslation(v1)
|
||||
.getTranslation()
|
||||
.equals(v1)
|
||||
return t.withTranslation(v1).getTranslation().equals(v1)
|
||||
})
|
||||
|
||||
jsc.assertForall(arbitraryTransform, arbitraryVec2, (t, v1) => {
|
||||
const initialTranslation = t.getTranslation()
|
||||
return t
|
||||
.translatedBy(v1)
|
||||
.getTranslation()
|
||||
.approxEquals(initialTranslation.plus(v1))
|
||||
return t.translatedBy(v1).getTranslation().approxEquals(initialTranslation.plus(v1))
|
||||
})
|
||||
})
|
||||
|
||||
test('scale', () => {
|
||||
jsc.assertForall(arbitraryTransform, arbitraryVec2, (t, v1) => {
|
||||
return t
|
||||
.withScale(v1)
|
||||
.getScale()
|
||||
.equals(v1)
|
||||
return t.withScale(v1).getScale().equals(v1)
|
||||
})
|
||||
})
|
||||
|
||||
test('transformVector', () => {
|
||||
// Vector transformation are translation-invariant
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return AffineTransform.withTranslation(v1)
|
||||
.transformVector(v2)
|
||||
.approxEquals(v2)
|
||||
return AffineTransform.withTranslation(v1).transformVector(v2).approxEquals(v2)
|
||||
})
|
||||
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return AffineTransform.withScale(v1)
|
||||
.transformVector(v2)
|
||||
.approxEquals(v2.timesPointwise(v1))
|
||||
return AffineTransform.withScale(v1).transformVector(v2).approxEquals(v2.timesPointwise(v1))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -319,15 +306,11 @@ describe('AffineTransform', () => {
|
||||
|
||||
test('transformPosition', () => {
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return AffineTransform.withTranslation(v1)
|
||||
.transformPosition(v2)
|
||||
.approxEquals(v2.plus(v1))
|
||||
return AffineTransform.withTranslation(v1).transformPosition(v2).approxEquals(v2.plus(v1))
|
||||
})
|
||||
|
||||
jsc.assertForall(arbitraryVec2, arbitraryVec2, (v1, v2) => {
|
||||
return AffineTransform.withScale(v1)
|
||||
.transformPosition(v2)
|
||||
.approxEquals(v2.timesPointwise(v1))
|
||||
return AffineTransform.withScale(v1).transformPosition(v2).approxEquals(v2.timesPointwise(v1))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -359,10 +342,7 @@ describe('AffineTransform', () => {
|
||||
|
||||
test('times', () => {
|
||||
jsc.assertForall(invertibleTransform, invertibleTransform, (t1, t2) => {
|
||||
return t1
|
||||
.times(t2)
|
||||
.times(t2.inverted()!)
|
||||
.approxEquals(t1)
|
||||
return t1.times(t2).times(t2.inverted()!).approxEquals(t1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import {Profile, Frame, CallTreeNode} from './profile'
|
||||
import {FuzzyMatch, fuzzyMatchStrings} from './fuzzy-find'
|
||||
import {Flamechart, FlamechartFrame} from './flamechart'
|
||||
import {Rect, Vec2} from './math'
|
||||
|
||||
export enum FlamechartType {
|
||||
CHRONO_FLAME_CHART,
|
||||
LEFT_HEAVY_FLAME_GRAPH,
|
||||
}
|
||||
|
||||
// A utility class for storing cached search results to avoid recomputation when
|
||||
// the search results & profile did not change.
|
||||
export class ProfileSearchResults {
|
||||
constructor(readonly profile: Profile, readonly searchQuery: string) {}
|
||||
|
||||
private matches: Map<Frame, FuzzyMatch> | null = null
|
||||
getMatchForFrame(frame: Frame): FuzzyMatch | null {
|
||||
if (!this.matches) {
|
||||
this.matches = new Map()
|
||||
this.profile.forEachFrame(frame => {
|
||||
const match = fuzzyMatchStrings(frame.name, this.searchQuery)
|
||||
if (match == null) return
|
||||
this.matches!.set(frame, match)
|
||||
})
|
||||
}
|
||||
return this.matches.get(frame) || null
|
||||
}
|
||||
}
|
||||
|
||||
export interface FlamechartSearchMatch {
|
||||
configSpaceBounds: Rect
|
||||
node: CallTreeNode
|
||||
}
|
||||
|
||||
interface CachedFlamechartResult {
|
||||
matches: FlamechartSearchMatch[]
|
||||
indexForNode: Map<CallTreeNode, number>
|
||||
}
|
||||
|
||||
export class FlamechartSearchResults {
|
||||
constructor(readonly flamechart: Flamechart, readonly profileResults: ProfileSearchResults) {}
|
||||
|
||||
private matches: CachedFlamechartResult | null = null
|
||||
private getResults(): CachedFlamechartResult {
|
||||
if (this.matches == null) {
|
||||
const matches: FlamechartSearchMatch[] = []
|
||||
const indexForNode = new Map<CallTreeNode, number>()
|
||||
const visit = (frame: FlamechartFrame, depth: number) => {
|
||||
const {node} = frame
|
||||
if (this.profileResults.getMatchForFrame(node.frame)) {
|
||||
const configSpaceBounds = new Rect(
|
||||
new Vec2(frame.start, depth),
|
||||
new Vec2(frame.end - frame.start, 1),
|
||||
)
|
||||
indexForNode.set(node, matches.length)
|
||||
matches.push({configSpaceBounds, node})
|
||||
}
|
||||
|
||||
frame.children.forEach(child => {
|
||||
visit(child, depth + 1)
|
||||
})
|
||||
}
|
||||
|
||||
const layers = this.flamechart.getLayers()
|
||||
if (layers.length > 0) {
|
||||
layers[0].forEach(frame => visit(frame, 0))
|
||||
}
|
||||
|
||||
this.matches = {matches, indexForNode}
|
||||
}
|
||||
return this.matches
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return this.getResults().matches.length
|
||||
}
|
||||
|
||||
indexOf(node: CallTreeNode): number | null {
|
||||
const result = this.getResults().indexForNode.get(node)
|
||||
return result === undefined ? null : result
|
||||
}
|
||||
|
||||
at(index: number): FlamechartSearchMatch {
|
||||
const matches = this.getResults().matches
|
||||
if (index < 0 || index >= matches.length) {
|
||||
throw new Error(`Index ${index} out of bounds in list of ${matches.length} matches.`)
|
||||
}
|
||||
return matches[index]
|
||||
}
|
||||
}
|
||||
+49
-46
@@ -26,19 +26,22 @@ function toStackList(profile: Profile, grouped: boolean): string[] {
|
||||
const curStack: (number | string)[] = []
|
||||
let lastValue = 0
|
||||
|
||||
function openFrame(node: CallTreeNode, value: number) {
|
||||
function maybeEmit(value: number) {
|
||||
if (lastValue != value) {
|
||||
stackList.push(curStack.map(k => `${k}`).join(';'))
|
||||
stackList.push(
|
||||
curStack.map(k => `${k}`).join(';') + ` ${profile.formatValue(value - lastValue)}`,
|
||||
)
|
||||
lastValue = value
|
||||
}
|
||||
curStack.push(node.frame.key)
|
||||
}
|
||||
|
||||
function openFrame(node: CallTreeNode, value: number) {
|
||||
maybeEmit(value)
|
||||
curStack.push(node.frame.name)
|
||||
}
|
||||
|
||||
function closeFrame(node: CallTreeNode, value: number) {
|
||||
if (lastValue != value) {
|
||||
stackList.push(curStack.map(k => `${k}`).join(';'))
|
||||
lastValue = value
|
||||
}
|
||||
maybeEmit(value)
|
||||
curStack.pop()
|
||||
}
|
||||
|
||||
@@ -82,49 +85,49 @@ function verifyProfile(profile: Profile) {
|
||||
|
||||
expect(toStackList(profile, false)).toEqual([
|
||||
// prettier-ignore
|
||||
'a',
|
||||
'a;b',
|
||||
'a;b;d',
|
||||
'a;b;c',
|
||||
'',
|
||||
'a',
|
||||
'a;b',
|
||||
'a;b;b',
|
||||
'a;b;e',
|
||||
'a',
|
||||
'a 1',
|
||||
'a;b 2',
|
||||
'a;b;d 1',
|
||||
'a;b;c 1',
|
||||
' 1',
|
||||
'a 1',
|
||||
'a;b 1',
|
||||
'a;b;b 1',
|
||||
'a;b;e 1',
|
||||
'a 1',
|
||||
])
|
||||
|
||||
expect(toStackList(profile, true)).toEqual([
|
||||
// prettier-ignore
|
||||
'a;b;e',
|
||||
'a;b;b',
|
||||
'a;b;c',
|
||||
'a;b;d',
|
||||
'a;b',
|
||||
'a',
|
||||
'a;b;d 1',
|
||||
'a;b;c 1',
|
||||
'a;b;b 1',
|
||||
'a;b;e 1',
|
||||
'a;b 3',
|
||||
'a 3',
|
||||
])
|
||||
|
||||
const flattened = profile.getProfileWithRecursionFlattened()
|
||||
expect(toStackList(flattened, false)).toEqual([
|
||||
// prettier-ignore
|
||||
'a',
|
||||
'a;b',
|
||||
'a;b;d',
|
||||
'a;b;c',
|
||||
'',
|
||||
'a',
|
||||
'a;b',
|
||||
'a;b;e',
|
||||
'a',
|
||||
'a 1',
|
||||
'a;b 2',
|
||||
'a;b;d 1',
|
||||
'a;b;c 1',
|
||||
' 1',
|
||||
'a 1',
|
||||
'a;b 2',
|
||||
'a;b;e 1',
|
||||
'a 1',
|
||||
])
|
||||
|
||||
expect(toStackList(flattened, true)).toEqual([
|
||||
// prettier-ignore
|
||||
'a;b;e',
|
||||
'a;b;c',
|
||||
'a;b;d',
|
||||
'a;b',
|
||||
'a',
|
||||
'a;b;d 1',
|
||||
'a;b;c 1',
|
||||
'a;b;e 1',
|
||||
'a;b 4',
|
||||
'a 3',
|
||||
])
|
||||
}
|
||||
|
||||
@@ -259,9 +262,9 @@ test('getInvertedProfileForCallersOf', () => {
|
||||
|
||||
expect(toStackList(inverted, false)).toEqual([
|
||||
// prettier-ignore
|
||||
'b',
|
||||
'b;a',
|
||||
'b;d',
|
||||
'b 1',
|
||||
'b;a 3',
|
||||
'b;d 1',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -287,10 +290,10 @@ test('getProfileForCalleesOf', () => {
|
||||
|
||||
expect(toStackList(inverted, false)).toEqual([
|
||||
// prettier-ignore
|
||||
'b',
|
||||
'b;c',
|
||||
'b;d',
|
||||
'b',
|
||||
'b 2',
|
||||
'b;c 1',
|
||||
'b;d 1',
|
||||
'b 1',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -313,8 +316,8 @@ test('getProfileWithRecursionFlattened', () => {
|
||||
|
||||
expect(toStackList(inverted, false)).toEqual([
|
||||
// prettier-ignore
|
||||
'a',
|
||||
'a;b',
|
||||
'a 1',
|
||||
'a;b 3',
|
||||
])
|
||||
|
||||
const framesInProfile = new Set<string | number>()
|
||||
|
||||
+33
-14
@@ -116,6 +116,13 @@ export class Profile {
|
||||
|
||||
protected frames = new KeyedSet<Frame>()
|
||||
|
||||
// Profiles store two call-trees.
|
||||
//
|
||||
// The "append order" call tree is the one in which nodes are ordered in
|
||||
// whatever order they were appended to their parent.
|
||||
//
|
||||
// The "grouped" call tree is one in which each node has at most one child per
|
||||
// frame. Nodes are ordered in decreasing order of weight
|
||||
protected appendOrderCalltreeRoot = new CallTreeNode(Frame.root, null)
|
||||
protected groupedCalltreeRoot = new CallTreeNode(Frame.root, null)
|
||||
|
||||
@@ -169,6 +176,17 @@ export class Profile {
|
||||
return this.totalNonIdleWeight
|
||||
}
|
||||
|
||||
// This is private because it should only be called in the ProfileBuilder
|
||||
// classes. Once a Profile instance has been constructed, it should be treated
|
||||
// as immutable.
|
||||
protected sortGroupedCallTree() {
|
||||
function visit(node: CallTreeNode) {
|
||||
node.children.sort((a, b) => -(a.getTotalWeight() - b.getTotalWeight()))
|
||||
node.children.forEach(visit)
|
||||
}
|
||||
visit(this.groupedCalltreeRoot)
|
||||
}
|
||||
|
||||
forEachCallGrouped(
|
||||
openFrame: (node: CallTreeNode, value: number) => void,
|
||||
closeFrame: (node: CallTreeNode, value: number) => void,
|
||||
@@ -180,10 +198,7 @@ export class Profile {
|
||||
|
||||
let childTime = 0
|
||||
|
||||
const children = [...node.children]
|
||||
children.sort((a, b) => (a.getTotalWeight() > b.getTotalWeight() ? -1 : 1))
|
||||
|
||||
children.forEach(function(child) {
|
||||
node.children.forEach(function (child) {
|
||||
visit(child, start + childTime)
|
||||
childTime += child.getTotalWeight()
|
||||
})
|
||||
@@ -250,12 +265,6 @@ export class Profile {
|
||||
this.frames.forEach(fn)
|
||||
}
|
||||
|
||||
forEachSample(fn: (sample: CallTreeNode, weight: number) => void) {
|
||||
for (let i = 0; i < this.samples.length; i++) {
|
||||
fn(this.samples[i], this.weights[i])
|
||||
}
|
||||
}
|
||||
|
||||
getProfileWithRecursionFlattened(): Profile {
|
||||
const builder = new CallTreeProfileBuilder()
|
||||
|
||||
@@ -507,7 +516,11 @@ export class StackListProfileBuilder extends Profile {
|
||||
this.setValueFormatter(new RawValueFormatter())
|
||||
}
|
||||
}
|
||||
this.totalWeight = Math.max(this.totalWeight, this.weights.reduce((a, b) => a + b, 0))
|
||||
this.totalWeight = Math.max(
|
||||
this.totalWeight,
|
||||
this.weights.reduce((a, b) => a + b, 0),
|
||||
)
|
||||
this.sortGroupedCallTree()
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -557,9 +570,7 @@ export class CallTreeProfileBuilder extends Profile {
|
||||
this.weights.push(value - this.lastValue)
|
||||
} else if (delta < 0) {
|
||||
throw new Error(
|
||||
`Samples must be provided in increasing order of cumulative value. Last sample was ${
|
||||
this.lastValue
|
||||
}, this sample was ${value}`,
|
||||
`Samples must be provided in increasing order of cumulative value. Last sample was ${this.lastValue}, this sample was ${value}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -602,6 +613,13 @@ export class CallTreeProfileBuilder extends Profile {
|
||||
throw new Error(`Trying to leave a ${frame.key} before any have been entered`)
|
||||
}
|
||||
leavingStackTop.freeze()
|
||||
|
||||
if (leavingStackTop.frame.key !== frame.key) {
|
||||
throw new Error(
|
||||
`Tried to leave frame "${frame.name}" while frame "${leavingStackTop.frame.name}" was at the top at ${value}`,
|
||||
)
|
||||
}
|
||||
|
||||
const delta = value - this.lastValue
|
||||
if (delta > 0) {
|
||||
this.samples.push(leavingStackTop)
|
||||
@@ -643,6 +661,7 @@ export class CallTreeProfileBuilder extends Profile {
|
||||
if (this.appendOrderStack.length > 1 || this.groupedOrderStack.length > 1) {
|
||||
throw new Error('Tried to complete profile construction with a non-empty stack')
|
||||
}
|
||||
this.sortGroupedCallTree()
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ export class StatsPanel {
|
||||
this.msPanel.update(time - this.beginTime, 200)
|
||||
|
||||
if (time >= this.prevTime + 1000) {
|
||||
this.fpsPanel.update(this.frames * 1000 / (time - this.prevTime), 100)
|
||||
this.fpsPanel.update((this.frames * 1000) / (time - this.prevTime), 100)
|
||||
this.prevTime = time
|
||||
this.frames = 0
|
||||
}
|
||||
|
||||
@@ -97,3 +97,14 @@ export async function checkProfileSnapshot(filepath: string) {
|
||||
const reexported = exportProfileGroup(reimportedGroup)
|
||||
expect(exported).toEqual(reexported)
|
||||
}
|
||||
|
||||
export async function expectImportFailure(filepath: string) {
|
||||
const buffer = fs.readFileSync(filepath)
|
||||
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
|
||||
try {
|
||||
await importProfilesFromArrayBuffer(path.basename(filepath), arrayBuffer)
|
||||
fail('Expected import to fail but it succeeded')
|
||||
} catch (error) {
|
||||
expect(error.message).toMatchSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import {buildTrimmedText, ELLIPSIS, remapRangesToTrimmedText} from './text-utils'
|
||||
import {fuzzyMatchStrings} from './fuzzy-find'
|
||||
|
||||
function assertTrimmed(text: string, length: number, expectedTrimmed: string) {
|
||||
expect(buildTrimmedText(text, length).trimmedString).toEqual(
|
||||
expectedTrimmed.replace('...', ELLIPSIS),
|
||||
)
|
||||
}
|
||||
|
||||
test('buildTrimmedText', () => {
|
||||
assertTrimmed('hello world', 1, '...')
|
||||
assertTrimmed('hello world', 2, 'h...')
|
||||
assertTrimmed('hello world', 3, 'h...d')
|
||||
assertTrimmed('hello world', 4, 'he...d')
|
||||
assertTrimmed('hello world', 10, 'hello...orld')
|
||||
assertTrimmed('hello world', 11, 'hello world')
|
||||
assertTrimmed('hello world', 100, 'hello world')
|
||||
})
|
||||
|
||||
function highlightText(text: string, highlightedRanges: [number, number][]): string {
|
||||
let last = 0
|
||||
let highlighted = ''
|
||||
for (let range of highlightedRanges) {
|
||||
highlighted += `${text.slice(last, range[0])}[${text.slice(range[0], range[1])}]`
|
||||
last = range[1]
|
||||
}
|
||||
highlighted += text.slice(last)
|
||||
return highlighted
|
||||
}
|
||||
|
||||
function assertTrimmedHighlight({
|
||||
text,
|
||||
pattern,
|
||||
expectedHighlighted,
|
||||
length,
|
||||
expectedHighlightedTrimmed,
|
||||
}: {
|
||||
text: string
|
||||
pattern: string
|
||||
expectedHighlighted: string
|
||||
length: number
|
||||
expectedHighlightedTrimmed: string
|
||||
}) {
|
||||
const match = fuzzyMatchStrings(text, pattern)
|
||||
const trimmed = buildTrimmedText(text, length)
|
||||
|
||||
if (!match) {
|
||||
fail()
|
||||
return
|
||||
}
|
||||
|
||||
const matchedRangesForTrimmedText = remapRangesToTrimmedText(trimmed, match.matchedRanges)
|
||||
const highlighted = highlightText(text, match.matchedRanges)
|
||||
const highlightedTrimmed = highlightText(trimmed.trimmedString, matchedRangesForTrimmedText)
|
||||
|
||||
expect(highlighted).toEqual(expectedHighlighted)
|
||||
expect(highlightedTrimmed).toEqual(expectedHighlightedTrimmed.replace('...', ELLIPSIS))
|
||||
}
|
||||
|
||||
test('remapRangesToTrimmedText', () => {
|
||||
assertTrimmedHighlight({
|
||||
text: 'hello world',
|
||||
pattern: 'he',
|
||||
length: 4,
|
||||
expectedHighlighted: '[he]llo world',
|
||||
expectedHighlightedTrimmed: `[he]...d`,
|
||||
})
|
||||
|
||||
assertTrimmedHighlight({
|
||||
text: 'hello world',
|
||||
pattern: 'o w',
|
||||
length: 4,
|
||||
expectedHighlighted: 'hell[o w]orld',
|
||||
expectedHighlightedTrimmed: `he[...]d`,
|
||||
})
|
||||
|
||||
assertTrimmedHighlight({
|
||||
text: 'hello world',
|
||||
pattern: 'ow',
|
||||
length: 4,
|
||||
expectedHighlighted: 'hell[o] [w]orld',
|
||||
expectedHighlightedTrimmed: `he[...]d`,
|
||||
})
|
||||
|
||||
assertTrimmedHighlight({
|
||||
text: 'hello world',
|
||||
pattern: 'hello',
|
||||
length: 4,
|
||||
expectedHighlighted: '[hello] world',
|
||||
expectedHighlightedTrimmed: `[he...]d`,
|
||||
})
|
||||
|
||||
assertTrimmedHighlight({
|
||||
text: 'hello world',
|
||||
pattern: 'hello world',
|
||||
length: 4,
|
||||
expectedHighlighted: '[hello world]',
|
||||
expectedHighlightedTrimmed: `[he...d]`,
|
||||
})
|
||||
|
||||
assertTrimmedHighlight({
|
||||
text: 'hello world',
|
||||
pattern: 'helloworld',
|
||||
length: 4,
|
||||
expectedHighlighted: '[hello] [world]',
|
||||
expectedHighlightedTrimmed: `[he...][d]`,
|
||||
})
|
||||
|
||||
assertTrimmedHighlight({
|
||||
text: 'hello world',
|
||||
pattern: 'world',
|
||||
length: 4,
|
||||
expectedHighlighted: 'hello [world]',
|
||||
expectedHighlightedTrimmed: `he[...d]`,
|
||||
})
|
||||
})
|
||||
+174
-8
@@ -18,22 +18,188 @@ export function cachedMeasureTextWidth(ctx: CanvasRenderingContext2D, text: stri
|
||||
return measureTextCache.get(text)!
|
||||
}
|
||||
|
||||
function buildTrimmedText(text: string, length: number) {
|
||||
const prefixLength = Math.floor(length / 2)
|
||||
const prefix = text.substr(0, prefixLength)
|
||||
const suffix = text.substr(text.length - prefixLength, prefixLength)
|
||||
return prefix + ELLIPSIS + suffix
|
||||
interface TrimmedTextResult {
|
||||
trimmedString: string
|
||||
trimmedLength: number
|
||||
prefixLength: number
|
||||
suffixLength: number
|
||||
originalLength: number
|
||||
originalString: string
|
||||
}
|
||||
|
||||
export function trimTextMid(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
|
||||
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) return text
|
||||
// Trim text, placing an ellipsis in the middle, with a slight bias towards
|
||||
// keeping text from the beginning rather than the end
|
||||
export function buildTrimmedText(text: string, length: number): TrimmedTextResult {
|
||||
if (text.length <= length) {
|
||||
return {
|
||||
trimmedString: text,
|
||||
trimmedLength: text.length,
|
||||
prefixLength: text.length,
|
||||
suffixLength: 0,
|
||||
originalString: text,
|
||||
originalLength: text.length,
|
||||
}
|
||||
}
|
||||
|
||||
let prefixLength = Math.floor(length / 2)
|
||||
const suffixLength = length - prefixLength - 1
|
||||
const prefix = text.substr(0, prefixLength)
|
||||
const suffix = text.substr(text.length - suffixLength, suffixLength)
|
||||
const trimmedString = prefix + ELLIPSIS + suffix
|
||||
return {
|
||||
trimmedString,
|
||||
trimmedLength: trimmedString.length,
|
||||
prefixLength: prefix.length,
|
||||
suffixLength: suffix.length,
|
||||
originalString: text,
|
||||
originalLength: text.length,
|
||||
}
|
||||
}
|
||||
|
||||
// Trim text to fit within the given number of pixels on the canvas
|
||||
export function trimTextMid(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
maxWidth: number,
|
||||
): TrimmedTextResult {
|
||||
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) {
|
||||
return buildTrimmedText(text, text.length)
|
||||
}
|
||||
const [lo] = binarySearch(
|
||||
0,
|
||||
text.length,
|
||||
n => {
|
||||
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n))
|
||||
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n).trimmedString)
|
||||
},
|
||||
maxWidth,
|
||||
)
|
||||
return buildTrimmedText(text, lo)
|
||||
}
|
||||
|
||||
enum IndexTypeInTrimmed {
|
||||
IN_PREFIX,
|
||||
IN_SUFFIX,
|
||||
ELIDED,
|
||||
}
|
||||
|
||||
function getIndexTypeInTrimmed(result: TrimmedTextResult, index: number): IndexTypeInTrimmed {
|
||||
if (index < result.prefixLength) {
|
||||
return IndexTypeInTrimmed.IN_PREFIX
|
||||
} else if (index < result.originalLength - result.suffixLength) {
|
||||
return IndexTypeInTrimmed.ELIDED
|
||||
} else {
|
||||
return IndexTypeInTrimmed.IN_SUFFIX
|
||||
}
|
||||
}
|
||||
|
||||
export function remapRangesToTrimmedText(
|
||||
trimmedText: TrimmedTextResult,
|
||||
ranges: [number, number][],
|
||||
): [number, number][] {
|
||||
// We intentionally don't just re-run fuzzy matching on the trimmed
|
||||
// text, beacuse if the search query is "helloWorld", the frame name
|
||||
// is "application::helloWorld", and that gets trimmed down to
|
||||
// "appl...oWorld", we still want "oWorld" to be highlighted, even
|
||||
// though the string "appl...oWorld" is not matched by the query
|
||||
// "helloWorld".
|
||||
//
|
||||
// There's a weird case to consider here: what if the trimmedText is
|
||||
// also matched by the query, but results in a different match than
|
||||
// the original query? Consider, e.g. the search string of "ab". The
|
||||
// string "hello ab shabby" will be matched at the first "ab", but
|
||||
// may be trimmed to "hello...shabby". In this case, should we
|
||||
// highlight the "ab" hidden by the ellipsis, or the "ab" in
|
||||
// "shabby"? The code below highlights the ellipsis so that the
|
||||
// matched characters don't change as you zoom in and out.
|
||||
|
||||
const rangesToHighlightInTrimmedText: [number, number][] = []
|
||||
const lengthLoss = trimmedText.originalLength - trimmedText.trimmedLength
|
||||
let highlightedEllipsis = false
|
||||
|
||||
for (let [origStart, origEnd] of ranges) {
|
||||
let startPosType = getIndexTypeInTrimmed(trimmedText, origStart)
|
||||
let endPosType = getIndexTypeInTrimmed(trimmedText, origEnd - 1)
|
||||
|
||||
switch (startPosType) {
|
||||
case IndexTypeInTrimmed.IN_PREFIX: {
|
||||
switch (endPosType) {
|
||||
case IndexTypeInTrimmed.IN_PREFIX: {
|
||||
// The entire range fits in the prefix. Add it unmodified.
|
||||
rangesToHighlightInTrimmedText.push([origStart, origEnd])
|
||||
break
|
||||
}
|
||||
case IndexTypeInTrimmed.ELIDED: {
|
||||
// The range starts in the prefix, but ends in the elided
|
||||
// section. Add just the prefix + one char for the ellipsis.
|
||||
rangesToHighlightInTrimmedText.push([
|
||||
origStart,
|
||||
origStart + trimmedText.prefixLength + 1,
|
||||
])
|
||||
highlightedEllipsis = true
|
||||
break
|
||||
}
|
||||
case IndexTypeInTrimmed.IN_SUFFIX: {
|
||||
// The range crosses from the prefix to the suffix.
|
||||
// Highlight everything including the ellipsis.
|
||||
rangesToHighlightInTrimmedText.push([origStart, origEnd - lengthLoss])
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case IndexTypeInTrimmed.ELIDED: {
|
||||
switch (endPosType) {
|
||||
case IndexTypeInTrimmed.IN_PREFIX: {
|
||||
// This should be impossible
|
||||
throw new Error('Unexpected highlight range starts in elided and ends in prefix')
|
||||
}
|
||||
case IndexTypeInTrimmed.ELIDED: {
|
||||
// The match starts & ends within the elided section.
|
||||
if (!highlightedEllipsis) {
|
||||
rangesToHighlightInTrimmedText.push([
|
||||
trimmedText.prefixLength,
|
||||
trimmedText.prefixLength + 1,
|
||||
])
|
||||
highlightedEllipsis = true
|
||||
}
|
||||
break
|
||||
}
|
||||
case IndexTypeInTrimmed.IN_SUFFIX: {
|
||||
// The match starts in elided, but ends in suffix.
|
||||
if (highlightedEllipsis) {
|
||||
rangesToHighlightInTrimmedText.push([
|
||||
trimmedText.trimmedLength - trimmedText.suffixLength,
|
||||
origEnd - lengthLoss,
|
||||
])
|
||||
} else {
|
||||
rangesToHighlightInTrimmedText.push([trimmedText.prefixLength, origEnd - lengthLoss])
|
||||
highlightedEllipsis = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case IndexTypeInTrimmed.IN_SUFFIX: {
|
||||
switch (endPosType) {
|
||||
case IndexTypeInTrimmed.IN_PREFIX: {
|
||||
// This should be impossible
|
||||
throw new Error('Unexpected highlight range starts in suffix and ends in prefix')
|
||||
}
|
||||
case IndexTypeInTrimmed.ELIDED: {
|
||||
// This should be impossible
|
||||
throw new Error('Unexpected highlight range starts in suffix and ends in elided')
|
||||
break
|
||||
}
|
||||
case IndexTypeInTrimmed.IN_SUFFIX: {
|
||||
// Match starts & ends in suffix
|
||||
rangesToHighlightInTrimmedText.push([origStart - lengthLoss, origEnd - lengthLoss])
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return rangesToHighlightInTrimmedText
|
||||
}
|
||||
|
||||
+1
-27
@@ -1,6 +1,5 @@
|
||||
import {connect} from 'preact-redux'
|
||||
import * as redux from 'redux'
|
||||
import {ComponentConstructor, Component} from 'preact'
|
||||
import {Component} from 'preact'
|
||||
|
||||
export interface Action<TPayload> extends redux.Action<string> {
|
||||
payload: TPayload
|
||||
@@ -53,31 +52,6 @@ export function setter<T>(
|
||||
|
||||
export type Dispatch = redux.Dispatch<Action<any>>
|
||||
|
||||
// We make this into a single function invocation instead of the connect(map, map)(Component)
|
||||
// syntax to make better use of type inference.
|
||||
//
|
||||
// NOTE: To avoid this returning objects which do not compare shallow equal, it's the
|
||||
// responsibility of the caller to ensure that the props returned by map compare shallow
|
||||
// equal. This most importantly mean memoizing functions which wrap dispatch to avoid
|
||||
// all callback props from being regenerated on every call.
|
||||
export function createContainer<OwnProps, State, ComponentProps, ComponentType>(
|
||||
component: {
|
||||
new (props: ComponentProps): ComponentType
|
||||
},
|
||||
map: (state: State, dispatch: Dispatch, ownProps: OwnProps) => ComponentProps,
|
||||
): ComponentConstructor<OwnProps, {}> {
|
||||
const mapStateToProps = (state: State) => state
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({dispatch})
|
||||
const mergeProps = (
|
||||
stateProps: State,
|
||||
dispatchProps: {dispatch: Dispatch},
|
||||
ownProps: OwnProps,
|
||||
) => {
|
||||
return map(stateProps, dispatchProps.dispatch, ownProps)
|
||||
}
|
||||
return connect(mapStateToProps, mapDispatchToProps, mergeProps)(component)
|
||||
}
|
||||
|
||||
export type VoidState = {
|
||||
__dummyField: void
|
||||
}
|
||||
|
||||
+15
-13
@@ -4,7 +4,9 @@ export function lastOf<T>(ts: T[]): T | null {
|
||||
|
||||
export function sortBy<T>(ts: T[], key: (t: T) => number | string): void {
|
||||
function comparator(a: T, b: T) {
|
||||
return key(a) < key(b) ? -1 : 1
|
||||
const keyA = key(a)
|
||||
const keyB = key(b)
|
||||
return keyA < keyB ? -1 : keyA > keyB ? 1 : 0
|
||||
}
|
||||
ts.sort(comparator)
|
||||
}
|
||||
@@ -178,15 +180,17 @@ export function lazyStatic<T>(cb: () => T): () => T {
|
||||
}
|
||||
}
|
||||
|
||||
const base64lookupTable = lazyStatic((): Map<string, number> => {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
const ret = new Map<string, number>()
|
||||
for (let i = 0; i < alphabet.length; i++) {
|
||||
ret.set(alphabet.charAt(i), i)
|
||||
}
|
||||
ret.set('=', -1)
|
||||
return ret
|
||||
})
|
||||
const base64lookupTable = lazyStatic(
|
||||
(): Map<string, number> => {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
const ret = new Map<string, number>()
|
||||
for (let i = 0; i < alphabet.length; i++) {
|
||||
ret.set(alphabet.charAt(i), i)
|
||||
}
|
||||
ret.set('=', -1)
|
||||
return ret
|
||||
},
|
||||
)
|
||||
|
||||
// NOTE: There are probably simpler solutions to this problem, but I have this written already, so
|
||||
// until we run into problems with this, let's just use this.
|
||||
@@ -210,9 +214,7 @@ export function decodeBase64(encoded: string): Uint8Array {
|
||||
|
||||
if (encoded.length % 4 !== 0) {
|
||||
throw new Error(
|
||||
`Invalid length for base64 encoded string. Expected length % 4 = 0, got length = ${
|
||||
encoded.length
|
||||
}`,
|
||||
`Invalid length for base64 encoded string. Expected length % 4 = 0, got length = ${encoded.length}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import {h, render} from 'preact'
|
||||
import {createApplicationStore} from './store'
|
||||
import {Provider} from 'preact-redux'
|
||||
import {createAppStore} from './store'
|
||||
import {ApplicationContainer} from './views/application-container'
|
||||
import {Provider} from './lib/preact-redux'
|
||||
|
||||
console.log(`speedscope v${require('../package.json').version}`)
|
||||
|
||||
@@ -15,7 +15,7 @@ if (module.hot) {
|
||||
}
|
||||
|
||||
const lastStore: any = (window as any)['store']
|
||||
const store = createApplicationStore(lastStore ? lastStore.getState() : {})
|
||||
const store = lastStore ? createAppStore(lastStore.getState()) : createAppStore()
|
||||
;(window as any)['store'] = store
|
||||
|
||||
render(
|
||||
|
||||
+3
-20
@@ -8,40 +8,23 @@ import {HashParams} from '../lib/hash-params'
|
||||
import {actionCreatorWithIndex} from './profiles-state'
|
||||
|
||||
export namespace actions {
|
||||
// Set the top-level profile group from which other data will be derived
|
||||
export const setProfileGroup = actionCreator<ProfileGroup>('setProfileGroup')
|
||||
|
||||
// Set the index into the profile group to view
|
||||
export const setProfileIndexToView = actionCreator<number>('setProfileIndexToView')
|
||||
|
||||
export const setGLCanvas = actionCreator<HTMLCanvasElement | null>('setGLCanvas')
|
||||
|
||||
// Set which top-level view should be displayed
|
||||
export const setViewMode = actionCreator<ViewMode>('setViewMode')
|
||||
|
||||
// Set whether or not recursion should be flattened when viewing flamegraphs
|
||||
export const setFlattenRecursion = actionCreator<boolean>('setFlattenRecursion')
|
||||
|
||||
// Set whether a file drag is currently active. Used to indicate that the
|
||||
// application is a valid drop target.
|
||||
export const setSearchQuery = actionCreator<string>('setSearchQuery')
|
||||
export const setSearchIsActive = actionCreator<boolean>('setSearchIsActive')
|
||||
export const setDragActive = actionCreator<boolean>('setDragActive')
|
||||
|
||||
// Set whether the application is currently in a loading state. Used to
|
||||
// display a loading progress bar.
|
||||
export const setLoading = actionCreator<boolean>('setLoading')
|
||||
|
||||
// Set whether the application is in an errored state.
|
||||
export const setError = actionCreator<boolean>('setError')
|
||||
|
||||
// Set whether parameters defined by the URL encoded k=v pairs after the # in the URL
|
||||
export const setHashParams = actionCreator<HashParams>('setHashParams')
|
||||
|
||||
export namespace sandwichView {
|
||||
// Set the table sorting method used for the sandwich view.
|
||||
export const setTableSortMethod = actionCreator<SortMethod>('sandwichView.setTableSortMethod')
|
||||
|
||||
export const setSelectedFrame = actionCreatorWithIndex<Frame | null>(
|
||||
'sandwichView.setSelectedFarmr',
|
||||
'sandwichView.setSelectedFrame',
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+18
-23
@@ -40,32 +40,27 @@ export const getRowAtlas = memoizeByReference((canvasContext: CanvasContext) =>
|
||||
)
|
||||
})
|
||||
|
||||
export const getProfileWithRecursionFlattened = memoizeByReference((profile: Profile) =>
|
||||
profile.getProfileWithRecursionFlattened(),
|
||||
)
|
||||
|
||||
export const getProfileToView = memoizeByShallowEquality(
|
||||
({profile, flattenRecursion}: {profile: Profile; flattenRecursion: boolean}): Profile => {
|
||||
return flattenRecursion ? profile.getProfileWithRecursionFlattened() : profile
|
||||
},
|
||||
)
|
||||
export const getFrameToColorBucket = memoizeByReference((profile: Profile): Map<
|
||||
string | number,
|
||||
number
|
||||
> => {
|
||||
const frames: Frame[] = []
|
||||
profile.forEachFrame(f => frames.push(f))
|
||||
function key(f: Frame) {
|
||||
return (f.file || '') + f.name
|
||||
}
|
||||
function compare(a: Frame, b: Frame) {
|
||||
return key(a) > key(b) ? 1 : -1
|
||||
}
|
||||
frames.sort(compare)
|
||||
const frameToColorBucket = new Map<string | number, number>()
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
frameToColorBucket.set(frames[i].key, Math.floor(255 * i / frames.length))
|
||||
}
|
||||
export const getFrameToColorBucket = memoizeByReference(
|
||||
(profile: Profile): Map<string | number, number> => {
|
||||
const frames: Frame[] = []
|
||||
profile.forEachFrame(f => frames.push(f))
|
||||
function key(f: Frame) {
|
||||
return (f.file || '') + f.name
|
||||
}
|
||||
function compare(a: Frame, b: Frame) {
|
||||
return key(a) > key(b) ? 1 : -1
|
||||
}
|
||||
frames.sort(compare)
|
||||
const frameToColorBucket = new Map<string | number, number>()
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
frameToColorBucket.set(frames[i].key, Math.floor((255 * i) / frames.length))
|
||||
}
|
||||
|
||||
return frameToColorBucket
|
||||
})
|
||||
return frameToColorBucket
|
||||
},
|
||||
)
|
||||
|
||||
+67
-6
@@ -10,6 +10,11 @@ import {setter, Reducer} from '../lib/typed-redux'
|
||||
import {HashParams, getHashParams} from '../lib/hash-params'
|
||||
import {ProfileGroupState, profileGroup} from './profiles-state'
|
||||
import {SortMethod, SortField, SortDirection} from '../views/profile-table-view'
|
||||
import {useSelector} from '../lib/preact-redux'
|
||||
import {Profile} from '../lib/profile'
|
||||
import {FlamechartViewState} from './flamechart-view-state'
|
||||
import {SandwichViewState} from './sandwich-view-state'
|
||||
import {getProfileToView} from './getters'
|
||||
|
||||
export const enum ViewMode {
|
||||
CHRONO_FLAME_CHART,
|
||||
@@ -18,20 +23,43 @@ export const enum ViewMode {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
viewMode: ViewMode
|
||||
// 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
|
||||
|
||||
profileGroup: ProfileGroupState
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol
|
||||
@@ -41,9 +69,7 @@ const protocol = window.location.protocol
|
||||
// however, XHR will be unavailable to fetching files in adjacent directories.
|
||||
export const canUseXHR = protocol === 'http:' || protocol === 'https:'
|
||||
|
||||
export function createApplicationStore(
|
||||
initialState: Partial<ApplicationState>,
|
||||
): redux.Store<ApplicationState> {
|
||||
export function createAppStore(initialState?: ApplicationState): redux.Store<ApplicationState> {
|
||||
const hashParams = getHashParams()
|
||||
|
||||
const loading = canUseXHR && hashParams.profileURL != null
|
||||
@@ -57,6 +83,9 @@ export function createApplicationStore(
|
||||
|
||||
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),
|
||||
@@ -71,3 +100,35 @@ export function createApplicationStore(
|
||||
|
||||
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,
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ 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
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as fs from 'fs'
|
||||
|
||||
import {Store, AnyAction} from 'redux'
|
||||
import {ApplicationState, createApplicationStore} from '.'
|
||||
import {ApplicationState, createAppStore} from '.'
|
||||
import {importSpeedscopeProfiles} from '../lib/file-format'
|
||||
|
||||
export function storeTest(name: string, cb: (store: Store<ApplicationState, AnyAction>) => void) {
|
||||
const store = createApplicationStore({})
|
||||
const store = createAppStore()
|
||||
test(name, () => {
|
||||
cb(store)
|
||||
})
|
||||
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
declare module 'preact-redux' {
|
||||
import {VNode, Component} from 'preact'
|
||||
import {Store} from 'redux'
|
||||
|
||||
// We just export the bare minimum here because we're going
|
||||
// to implement an API for readability convenience elsewhere
|
||||
export function connect(...args: any[]): any
|
||||
|
||||
export class Provider extends Component<{store: Store<any>}, {}> {
|
||||
render(): VNode
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,45 @@
|
||||
import {createContainer, Dispatch, bindActionCreator, ActionCreator} from '../lib/typed-redux'
|
||||
import {Application, ActiveProfileState} from './application'
|
||||
import {ApplicationState} from '../store'
|
||||
import {getProfileToView, getCanvasContext} from '../store/getters'
|
||||
import {h} from 'preact'
|
||||
import {Application} from './application'
|
||||
import {getCanvasContext} from '../store/getters'
|
||||
import {actions} from '../store/actions'
|
||||
import {Graphics} from '../gl/graphics'
|
||||
import {useActionCreator} from '../lib/preact-redux'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useAppSelector, useActiveProfileState} from '../store'
|
||||
import {ProfileSearchContextProvider} from './search-view'
|
||||
|
||||
export const ApplicationContainer = createContainer(
|
||||
Application,
|
||||
(state: ApplicationState, dispatch: Dispatch) => {
|
||||
const {flattenRecursion, profileGroup} = state
|
||||
const {
|
||||
setLoading,
|
||||
setError,
|
||||
setProfileGroup,
|
||||
setDragActive,
|
||||
setViewMode,
|
||||
setGLCanvas,
|
||||
setFlattenRecursion,
|
||||
setProfileIndexToView,
|
||||
} = actions
|
||||
|
||||
let activeProfileState: ActiveProfileState | null = null
|
||||
if (profileGroup) {
|
||||
if (profileGroup.profiles.length > profileGroup.indexToView) {
|
||||
const index = profileGroup.indexToView
|
||||
const profileState = profileGroup.profiles[index]
|
||||
activeProfileState = {
|
||||
...profileGroup.profiles[profileGroup.indexToView],
|
||||
profile: getProfileToView({profile: profileState.profile, flattenRecursion}),
|
||||
index: profileGroup.indexToView,
|
||||
}
|
||||
}
|
||||
}
|
||||
export const ApplicationContainer = memo(() => {
|
||||
const appState = useAppSelector(state => state, [])
|
||||
const canvasContext = useAppSelector(
|
||||
state => (state.glCanvas ? getCanvasContext(state.glCanvas) : null),
|
||||
[],
|
||||
)
|
||||
|
||||
function wrapActionCreator<T>(actionCreator: ActionCreator<T>): (t: T) => void {
|
||||
return bindActionCreator(dispatch, actionCreator)
|
||||
}
|
||||
|
||||
// TODO(jlfwong): Cache this and resizeCanvas below to prevent re-renders
|
||||
// due to changing props.
|
||||
const setters = {
|
||||
setGLCanvas: wrapActionCreator(actions.setGLCanvas),
|
||||
setLoading: wrapActionCreator(actions.setLoading),
|
||||
setError: wrapActionCreator(actions.setError),
|
||||
setProfileGroup: wrapActionCreator(actions.setProfileGroup),
|
||||
setDragActive: wrapActionCreator(actions.setDragActive),
|
||||
setViewMode: wrapActionCreator(actions.setViewMode),
|
||||
setFlattenRecursion: wrapActionCreator(actions.setFlattenRecursion),
|
||||
setProfileIndexToView: wrapActionCreator(actions.setProfileIndexToView),
|
||||
}
|
||||
|
||||
return {
|
||||
activeProfileState,
|
||||
dispatch,
|
||||
canvasContext: state.glCanvas ? getCanvasContext(state.glCanvas) : null,
|
||||
resizeCanvas: (
|
||||
widthInPixels: number,
|
||||
heightInPixels: number,
|
||||
widthInAppUnits: number,
|
||||
heightInAppUnits: number,
|
||||
) => {
|
||||
if (state.glCanvas) {
|
||||
const gl = getCanvasContext(state.glCanvas).gl
|
||||
gl.resize(widthInPixels, heightInPixels, widthInAppUnits, heightInAppUnits)
|
||||
gl.clear(new Graphics.Color(1, 1, 1, 1))
|
||||
}
|
||||
},
|
||||
...setters,
|
||||
...state,
|
||||
}
|
||||
},
|
||||
)
|
||||
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, [])}
|
||||
{...appState}
|
||||
/>
|
||||
</ProfileSearchContextProvider>
|
||||
)
|
||||
})
|
||||
|
||||
+22
-249
@@ -1,19 +1,18 @@
|
||||
import {h, Component} from 'preact'
|
||||
import {h} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {FileSystemDirectoryEntry} from '../import/file-system-entry'
|
||||
|
||||
import {Profile, ProfileGroup} from '../lib/profile'
|
||||
import {FontFamily, FontSize, Colors, Sizes, Duration} from './style'
|
||||
import {ProfileGroup} from '../lib/profile'
|
||||
import {FontFamily, FontSize, Colors, Duration} from './style'
|
||||
import {importEmscriptenSymbolMap} from '../lib/emscripten'
|
||||
import {SandwichViewContainer} from './sandwich-view'
|
||||
import {saveToFile} from '../lib/file-format'
|
||||
import {ApplicationState, ViewMode, canUseXHR} from '../store'
|
||||
import {ApplicationState, ViewMode, canUseXHR, ActiveProfileState} from '../store'
|
||||
import {StatelessComponent} from '../lib/typed-redux'
|
||||
import {LeftHeavyFlamechartView, ChronoFlamechartView} from './flamechart-view-container'
|
||||
import {SandwichViewState} from '../store/sandwich-view-state'
|
||||
import {FlamechartViewState} from '../store/flamechart-view-state'
|
||||
import {CanvasContext} from '../gl/canvas-context'
|
||||
import {Graphics} from '../gl/graphics'
|
||||
import {Toolbar} from './toolbar'
|
||||
|
||||
const importModule = import('../import')
|
||||
// Force eager loading of the module
|
||||
@@ -50,155 +49,14 @@ async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntr
|
||||
declare function require(x: string): any
|
||||
const exampleProfileURL = require('../../sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt')
|
||||
|
||||
interface ToolbarProps extends ApplicationProps {
|
||||
browseForFile(): void
|
||||
saveFile(): void
|
||||
}
|
||||
|
||||
export class Toolbar extends StatelessComponent<ToolbarProps> {
|
||||
setTimeOrder = () => {
|
||||
this.props.setViewMode(ViewMode.CHRONO_FLAME_CHART)
|
||||
}
|
||||
|
||||
setLeftHeavyOrder = () => {
|
||||
this.props.setViewMode(ViewMode.LEFT_HEAVY_FLAME_GRAPH)
|
||||
}
|
||||
|
||||
setSandwichView = () => {
|
||||
this.props.setViewMode(ViewMode.SANDWICH_VIEW)
|
||||
}
|
||||
|
||||
renderLeftContent() {
|
||||
if (!this.props.activeProfileState) return null
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarLeft)}>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.CHRONO_FLAME_CHART && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setTimeOrder}
|
||||
>
|
||||
<span className={css(style.emoji)}>🕰</span>Time Order
|
||||
</div>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.LEFT_HEAVY_FLAME_GRAPH && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setLeftHeavyOrder}
|
||||
>
|
||||
<span className={css(style.emoji)}>⬅️</span>Left Heavy
|
||||
</div>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.SANDWICH_VIEW && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setSandwichView}
|
||||
>
|
||||
<span className={css(style.emoji)}>🥪</span>Sandwich
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderCenterContent() {
|
||||
const {activeProfileState, profileGroup} = this.props
|
||||
if (activeProfileState && profileGroup) {
|
||||
const {index} = activeProfileState
|
||||
if (profileGroup.profiles.length === 1) {
|
||||
return activeProfileState.profile.getName()
|
||||
} else {
|
||||
function makeNavButton(content: string, disabled: boolean, onClick: () => void) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={css(
|
||||
style.emoji,
|
||||
style.toolbarProfileNavButton,
|
||||
disabled && style.toolbarProfileNavButtonDisabled,
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const prevButton = makeNavButton('⬅️', index === 0, () =>
|
||||
this.props.setProfileIndexToView(index - 1),
|
||||
)
|
||||
const nextButton = makeNavButton('➡️', index >= profileGroup.profiles.length - 1, () =>
|
||||
this.props.setProfileIndexToView(index + 1),
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarCenter)}>
|
||||
{prevButton}
|
||||
{activeProfileState.profile.getName()}{' '}
|
||||
<span className={css(style.toolbarProfileIndex)}>
|
||||
({activeProfileState.index + 1}/{profileGroup.profiles.length})
|
||||
</span>
|
||||
{nextButton}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return '🔬speedscope'
|
||||
}
|
||||
|
||||
renderRightContent() {
|
||||
const importFile = (
|
||||
<div className={css(style.toolbarTab)} onClick={this.props.browseForFile}>
|
||||
<span className={css(style.emoji)}>⤵️</span>Import
|
||||
</div>
|
||||
)
|
||||
const help = (
|
||||
<div className={css(style.toolbarTab)}>
|
||||
<a
|
||||
href="https://github.com/jlfwong/speedscope#usage"
|
||||
className={css(style.noLinkStyle)}
|
||||
target="_blank"
|
||||
>
|
||||
<span className={css(style.emoji)}>❓</span>Help
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarRight)}>
|
||||
{this.props.activeProfileState && (
|
||||
<div className={css(style.toolbarTab)} onClick={this.props.saveFile}>
|
||||
<span className={css(style.emoji)}>⤴️</span>Export
|
||||
</div>
|
||||
)}
|
||||
{importFile}
|
||||
{help}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={css(style.toolbar)}>
|
||||
{this.renderLeftContent()}
|
||||
{this.renderCenterContent()}
|
||||
{this.renderRightContent()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface GLCanvasProps {
|
||||
canvasContext: CanvasContext | null
|
||||
setGLCanvas: (canvas: HTMLCanvasElement | null) => void
|
||||
}
|
||||
export class GLCanvas extends Component<GLCanvasProps, void> {
|
||||
export class GLCanvas extends StatelessComponent<GLCanvasProps> {
|
||||
private canvas: HTMLCanvasElement | null = null
|
||||
|
||||
private ref = (canvas?: Element) => {
|
||||
private ref = (canvas: Element | null) => {
|
||||
if (canvas instanceof HTMLCanvasElement) {
|
||||
this.canvas = canvas
|
||||
} else {
|
||||
@@ -209,7 +67,7 @@ export class GLCanvas extends Component<GLCanvasProps, void> {
|
||||
}
|
||||
|
||||
private container: HTMLElement | null = null
|
||||
private containerRef = (container?: Element) => {
|
||||
private containerRef = (container: Element | null) => {
|
||||
if (container instanceof HTMLElement) {
|
||||
this.container = container
|
||||
} else {
|
||||
@@ -271,14 +129,6 @@ export class GLCanvas extends Component<GLCanvasProps, void> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ActiveProfileState {
|
||||
profile: Profile
|
||||
index: number
|
||||
chronoViewState: FlamechartViewState
|
||||
leftHeavyViewState: FlamechartViewState
|
||||
sandwichViewState: SandwichViewState
|
||||
}
|
||||
|
||||
export type ApplicationProps = ApplicationState & {
|
||||
setGLCanvas: (canvas: HTMLCanvasElement | null) => void
|
||||
setLoading: (loading: boolean) => void
|
||||
@@ -310,17 +160,21 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(jlfwong): Make these into nicer overlays
|
||||
if (profileGroup == null) {
|
||||
// TODO(jlfwong): Make this a nicer overlay
|
||||
alert('Unrecognized format! See documentation about supported formats.')
|
||||
this.props.setLoading(false)
|
||||
return
|
||||
} else if (profileGroup.profiles.length === 0) {
|
||||
alert("Successfully imported profile, but it's empty!")
|
||||
this.props.setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.props.hashParams.title) {
|
||||
profileGroup = {
|
||||
name: this.props.hashParams.title,
|
||||
...profileGroup,
|
||||
name: this.props.hashParams.title,
|
||||
}
|
||||
}
|
||||
document.title = `${profileGroup.name} - speedscope`
|
||||
@@ -398,6 +252,8 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
this.props.setDragActive(false)
|
||||
ev.preventDefault()
|
||||
|
||||
if (!ev.dataTransfer) return
|
||||
|
||||
const firstItem = ev.dataTransfer.items[0]
|
||||
if ('webkitGetAsEntry' in firstItem) {
|
||||
const webkitEntry: FileSystemDirectoryEntry = firstItem.webkitGetAsEntry()
|
||||
@@ -486,7 +342,9 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
|
||||
const pasted = (ev as ClipboardEvent).clipboardData.getData('text')
|
||||
const clipboardData = (ev as ClipboardEvent).clipboardData
|
||||
if (!clipboardData) return
|
||||
const pasted = clipboardData.getData('text')
|
||||
this.loadProfile(async () => {
|
||||
return await importProfilesFromText('From Clipboard', pasted)
|
||||
})
|
||||
@@ -607,7 +465,8 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
href="https://github.com/jlfwong/speedscope/issues"
|
||||
>
|
||||
report any issues on GitHub
|
||||
</a>.
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -669,7 +528,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
<Toolbar
|
||||
saveFile={this.saveFile}
|
||||
browseForFile={this.browseForFile}
|
||||
{...this.props as ApplicationProps}
|
||||
{...(this.props as ApplicationProps)}
|
||||
/>
|
||||
<div className={css(style.contentContainer)}>{this.renderContent()}</div>
|
||||
{this.props.dragActive && <div className={css(style.dragTarget)} />}
|
||||
@@ -781,90 +640,4 @@ const style = StyleSheet.create({
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
toolbar: {
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
flexShrink: 0,
|
||||
background: Colors.BLACK,
|
||||
color: Colors.WHITE,
|
||||
textAlign: 'center',
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
fontSize: FontSize.TITLE,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
userSelect: 'none',
|
||||
},
|
||||
toolbarLeft: {
|
||||
position: 'absolute',
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
top: 0,
|
||||
left: 0,
|
||||
marginRight: 2,
|
||||
textAlign: 'left',
|
||||
},
|
||||
toolbarCenter: {
|
||||
paddingTop: 1,
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
},
|
||||
toolbarRight: {
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
marginRight: 2,
|
||||
textAlign: 'right',
|
||||
},
|
||||
toolbarProfileIndex: {
|
||||
color: Colors.LIGHT_GRAY,
|
||||
},
|
||||
toolbarProfileNavButton: {
|
||||
opacity: 0.8,
|
||||
fontSize: FontSize.TITLE,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
':hover': {
|
||||
opacity: 1.0,
|
||||
},
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
marginLeft: '0.3em',
|
||||
marginRight: '0.3em',
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
},
|
||||
toolbarProfileNavButtonDisabled: {
|
||||
opacity: 0.5,
|
||||
':hover': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
toolbarTab: {
|
||||
background: Colors.DARK_GRAY,
|
||||
marginTop: Sizes.SEPARATOR_HEIGHT,
|
||||
height: Sizes.TOOLBAR_TAB_HEIGHT,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
paddingLeft: 2,
|
||||
paddingRight: 8,
|
||||
display: 'inline-block',
|
||||
marginLeft: 2,
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
':hover': {
|
||||
background: Colors.GRAY,
|
||||
},
|
||||
},
|
||||
toolbarTabActive: {
|
||||
background: Colors.BRIGHT_BLUE,
|
||||
':hover': {
|
||||
background: Colors.BRIGHT_BLUE,
|
||||
},
|
||||
},
|
||||
noLinkStyle: {
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
},
|
||||
emoji: {
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
paddingTop: '0px',
|
||||
marginRight: '0.3em',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import {memoizeByShallowEquality} from '../lib/utils'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {
|
||||
createMemoizedFlamechartRenderer,
|
||||
FlamechartViewContainerProps,
|
||||
createFlamechartSetters,
|
||||
} from './flamechart-view-container'
|
||||
import {createContainer, Dispatch} from '../lib/typed-redux'
|
||||
import {ApplicationState} from '../store'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
|
||||
const getCalleeProfile = memoizeByShallowEquality<
|
||||
{
|
||||
profile: Profile
|
||||
frame: Frame
|
||||
flattenRecursion: boolean
|
||||
},
|
||||
Profile
|
||||
>(({profile, frame, flattenRecursion}) => {
|
||||
let p = profile.getProfileForCalleesOf(frame)
|
||||
return flattenRecursion ? p.getProfileWithRecursionFlattened() : p
|
||||
})
|
||||
|
||||
const getCalleeFlamegraph = memoizeByShallowEquality<
|
||||
{
|
||||
calleeProfile: Profile
|
||||
getColorBucketForFrame: (frame: Frame) => number
|
||||
},
|
||||
Flamechart
|
||||
>(({calleeProfile, getColorBucketForFrame}) => {
|
||||
return new Flamechart({
|
||||
getTotalWeight: calleeProfile.getTotalNonIdleWeight.bind(calleeProfile),
|
||||
forEachCall: calleeProfile.forEachCallGrouped.bind(calleeProfile),
|
||||
formatValue: calleeProfile.formatValue.bind(calleeProfile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
})
|
||||
|
||||
const getCalleeFlamegraphRenderer = createMemoizedFlamechartRenderer()
|
||||
|
||||
export const CalleeFlamegraphView = createContainer(
|
||||
FlamechartWrapper,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
const {index, profile, sandwichViewState} = activeProfileState
|
||||
const {flattenRecursion, glCanvas} = state
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichViewState
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
|
||||
const flamechart = getCalleeFlamegraph({
|
||||
calleeProfile: getCalleeProfile({profile, frame: selectedFrame, flattenRecursion}),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getCalleeFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return {
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.SANDWICH_CALLEES, index),
|
||||
// This overrides the setSelectedNode specified in createFlamechartSettesr
|
||||
setSelectedNode: () => {},
|
||||
...callerCallee.calleeFlamegraph,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
import {memoizeByShallowEquality, noop} from '../lib/utils'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {
|
||||
createMemoizedFlamechartRenderer,
|
||||
FlamechartViewContainerProps,
|
||||
useFlamechartSetters,
|
||||
} from './flamechart-view-container'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
import {useAppSelector} from '../store'
|
||||
import {h} from 'preact'
|
||||
import {memo} from 'preact/compat'
|
||||
|
||||
const getCalleeProfile = memoizeByShallowEquality<
|
||||
{
|
||||
profile: Profile
|
||||
frame: Frame
|
||||
flattenRecursion: boolean
|
||||
},
|
||||
Profile
|
||||
>(({profile, frame, flattenRecursion}) => {
|
||||
let p = profile.getProfileForCalleesOf(frame)
|
||||
return flattenRecursion ? p.getProfileWithRecursionFlattened() : p
|
||||
})
|
||||
|
||||
const getCalleeFlamegraph = memoizeByShallowEquality<
|
||||
{
|
||||
calleeProfile: Profile
|
||||
getColorBucketForFrame: (frame: Frame) => number
|
||||
},
|
||||
Flamechart
|
||||
>(({calleeProfile, getColorBucketForFrame}) => {
|
||||
return new Flamechart({
|
||||
getTotalWeight: calleeProfile.getTotalNonIdleWeight.bind(calleeProfile),
|
||||
forEachCall: calleeProfile.forEachCallGrouped.bind(calleeProfile),
|
||||
formatValue: calleeProfile.formatValue.bind(calleeProfile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
})
|
||||
|
||||
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, [])
|
||||
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichViewState
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
|
||||
const flamechart = getCalleeFlamegraph({
|
||||
calleeProfile: getCalleeProfile({profile, frame: selectedFrame, flattenRecursion}),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getCalleeFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return (
|
||||
<FlamechartWrapper
|
||||
renderInverted={false}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
canvasContext={canvasContext}
|
||||
getCSSColorForFrame={getCSSColorForFrame}
|
||||
{...useFlamechartSetters(FlamechartID.SANDWICH_CALLEES, index)}
|
||||
{...callerCallee.calleeFlamegraph}
|
||||
// This overrides the setSelectedNode specified in useFlamechartSettesr
|
||||
setSelectedNode={noop}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import {h, Component} from 'preact'
|
||||
import {h} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {Colors, FontSize} from './style'
|
||||
|
||||
@@ -6,10 +6,8 @@ interface ColorChitProps {
|
||||
color: string
|
||||
}
|
||||
|
||||
export class ColorChit extends Component<ColorChitProps, {}> {
|
||||
render() {
|
||||
return <span className={css(style.stackChit)} style={{backgroundColor: this.props.color}} />
|
||||
}
|
||||
export function ColorChit(props: ColorChitProps) {
|
||||
return <span className={css(style.stackChit)} style={{backgroundColor: props.color}} />
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {StyleDeclarationValue, css} from 'aphrodite'
|
||||
import {h, Component} from 'preact'
|
||||
import {h, Component, JSX} from 'preact'
|
||||
import {style} from './flamechart-style'
|
||||
import {formatPercent} from '../lib/utils'
|
||||
import {Frame, CallTreeNode} from '../lib/profile'
|
||||
@@ -19,8 +19,8 @@ class StatisticsTable extends Component<StatisticsTableProps, {}> {
|
||||
render() {
|
||||
const total = this.props.formatter(this.props.selectedTotal)
|
||||
const self = this.props.formatter(this.props.selectedSelf)
|
||||
const totalPerc = 100.0 * this.props.selectedTotal / this.props.grandTotal
|
||||
const selfPerc = 100.0 * this.props.selectedSelf / this.props.grandTotal
|
||||
const totalPerc = (100.0 * this.props.selectedTotal) / this.props.grandTotal
|
||||
const selfPerc = (100.0 * this.props.selectedSelf) / this.props.grandTotal
|
||||
|
||||
return (
|
||||
<div className={css(style.statsTable)}>
|
||||
@@ -62,7 +62,7 @@ class StackTraceView extends Component<StackTraceViewProps, {}> {
|
||||
row.push(<ColorChit color={this.props.getFrameColor(frame)} />)
|
||||
|
||||
if (rows.length) {
|
||||
row.push(<span className={css(style.stackFileLine)}>> </span>)
|
||||
row.push(<span className={css(style.stackFileLine)}>> </span>)
|
||||
}
|
||||
row.push(frame.name)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ enum DraggingMode {
|
||||
|
||||
export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps, {}> {
|
||||
container: Element | null = null
|
||||
containerRef = (element?: Element) => {
|
||||
containerRef = (element: Element | null) => {
|
||||
this.container = element || null
|
||||
}
|
||||
|
||||
@@ -120,9 +120,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
const physicalViewSpaceFontSize = FontSize.LABEL * window.devicePixelRatio
|
||||
const labelPaddingPx = (physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2
|
||||
|
||||
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${
|
||||
FontFamily.MONOSPACE
|
||||
}`
|
||||
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${FontFamily.MONOSPACE}`
|
||||
ctx.textBaseline = 'top'
|
||||
|
||||
const minInterval = Math.pow(10, Math.floor(Math.log10(targetInterval)))
|
||||
@@ -396,7 +394,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
this.updateCursor(configSpaceMouse)
|
||||
}
|
||||
|
||||
private overlayCanvasRef = (element?: Element) => {
|
||||
private overlayCanvasRef = (element: Element | null) => {
|
||||
if (element) {
|
||||
this.overlayCanvas = element as HTMLCanvasElement
|
||||
this.overlayCtx = this.overlayCanvas.getContext('2d')
|
||||
|
||||
@@ -4,10 +4,17 @@ import {Flamechart, FlamechartFrame} from '../lib/flamechart'
|
||||
import {CanvasContext} from '../gl/canvas-context'
|
||||
import {FlamechartRenderer} from '../gl/flamechart-renderer'
|
||||
import {Sizes, FontSize, Colors, FontFamily, commonStyle} from './style'
|
||||
import {cachedMeasureTextWidth, ELLIPSIS, trimTextMid} from '../lib/text-utils'
|
||||
import {
|
||||
cachedMeasureTextWidth,
|
||||
ELLIPSIS,
|
||||
trimTextMid,
|
||||
remapRangesToTrimmedText,
|
||||
} from '../lib/text-utils'
|
||||
import {style} from './flamechart-style'
|
||||
import {h, Component} from 'preact'
|
||||
import {css} from 'aphrodite'
|
||||
import {ProfileSearchResults} from '../lib/profile-search'
|
||||
import {BatchCanvasTextRenderer, BatchCanvasRectRenderer} from '../lib/canvas-2d-batch-renderers'
|
||||
|
||||
interface FlamechartFrameLabel {
|
||||
configSpaceBounds: Rect
|
||||
@@ -47,12 +54,14 @@ export interface FlamechartPanZoomViewProps {
|
||||
setConfigSpaceViewportRect: (rect: Rect) => void
|
||||
|
||||
logicalSpaceViewportSize: Vec2
|
||||
setLogicalSpaceViewportBounds: (size: Vec2) => void
|
||||
setLogicalSpaceViewportSize: (size: Vec2) => void
|
||||
|
||||
searchResults: ProfileSearchResults | null
|
||||
}
|
||||
|
||||
export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps, {}> {
|
||||
private container: Element | null = null
|
||||
private containerRef = (element?: Element) => {
|
||||
private containerRef = (element: Element | null) => {
|
||||
this.container = element || null
|
||||
}
|
||||
|
||||
@@ -65,7 +74,7 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
this.props.setConfigSpaceViewportRect(r)
|
||||
}
|
||||
|
||||
private overlayCanvasRef = (element?: Element) => {
|
||||
private overlayCanvasRef = (element: Element | null) => {
|
||||
if (element) {
|
||||
this.overlayCanvas = element as HTMLCanvasElement
|
||||
this.overlayCtx = this.overlayCanvas.getContext('2d')
|
||||
@@ -163,29 +172,8 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
|
||||
ctx.clearRect(0, 0, physicalViewSize.x, physicalViewSize.y)
|
||||
|
||||
if (this.hoveredLabel) {
|
||||
let color = Colors.DARK_GRAY
|
||||
if (this.props.selectedNode === this.hoveredLabel.node) {
|
||||
color = Colors.DARK_BLUE
|
||||
}
|
||||
|
||||
ctx.lineWidth = 2 * devicePixelRatio
|
||||
ctx.strokeStyle = color
|
||||
|
||||
const physicalViewBounds = configToPhysical.transformRect(this.hoveredLabel.configSpaceBounds)
|
||||
ctx.strokeRect(
|
||||
Math.round(physicalViewBounds.left()),
|
||||
Math.round(physicalViewBounds.top()),
|
||||
Math.round(Math.max(0, physicalViewBounds.width())),
|
||||
Math.round(Math.max(0, physicalViewBounds.height())),
|
||||
)
|
||||
}
|
||||
|
||||
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${
|
||||
FontFamily.MONOSPACE
|
||||
}`
|
||||
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${FontFamily.MONOSPACE}`
|
||||
ctx.textBaseline = 'alphabetic'
|
||||
ctx.fillStyle = Colors.DARK_GRAY
|
||||
|
||||
const minWidthToRender = cachedMeasureTextWidth(ctx, 'M' + ELLIPSIS + 'M')
|
||||
const minConfigSpaceWidthToRender = (
|
||||
@@ -194,6 +182,13 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
|
||||
const LABEL_PADDING_PX = 5 * window.devicePixelRatio
|
||||
|
||||
const labelBatch = new BatchCanvasTextRenderer()
|
||||
const fadedLabelBatch = new BatchCanvasTextRenderer()
|
||||
const matchedTextHighlightBatch = new BatchCanvasRectRenderer()
|
||||
const directlySelectedOutlineBatch = new BatchCanvasRectRenderer()
|
||||
const indirectlySelectedOutlineBatch = new BatchCanvasRectRenderer()
|
||||
const matchedFrameBatch = new BatchCanvasRectRenderer()
|
||||
|
||||
const renderFrameLabelAndChildren = (frame: FlamechartFrame, depth = 0) => {
|
||||
const width = frame.end - frame.start
|
||||
const y = this.props.renderInverted ? this.configSpaceSize().y - 1 - depth : depth
|
||||
@@ -228,22 +223,58 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
}
|
||||
|
||||
if (physicalLabelBounds.width() > minWidthToRender) {
|
||||
const match = this.props.searchResults?.getMatchForFrame(frame.node.frame)
|
||||
|
||||
const trimmedText = trimTextMid(
|
||||
ctx,
|
||||
frame.node.frame.name,
|
||||
physicalLabelBounds.width() - 2 * LABEL_PADDING_PX,
|
||||
)
|
||||
|
||||
// Note that this is specifying the position of the starting text
|
||||
// baseline.
|
||||
ctx.fillText(
|
||||
trimmedText,
|
||||
physicalLabelBounds.left() + LABEL_PADDING_PX,
|
||||
Math.round(
|
||||
if (match) {
|
||||
const rangesToHighlightInTrimmedText = remapRangesToTrimmedText(
|
||||
trimmedText,
|
||||
match.matchedRanges,
|
||||
)
|
||||
|
||||
// Once we have the character ranges to highlight, we need to
|
||||
// actually do the highlighting.
|
||||
let lastEndIndex = 0
|
||||
let left = physicalLabelBounds.left() + LABEL_PADDING_PX
|
||||
|
||||
const padding = (physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2 - 2
|
||||
for (let [startIndex, endIndex] of rangesToHighlightInTrimmedText) {
|
||||
left += cachedMeasureTextWidth(
|
||||
ctx,
|
||||
trimmedText.trimmedString.substring(lastEndIndex, startIndex),
|
||||
)
|
||||
const highlightWidth = cachedMeasureTextWidth(
|
||||
ctx,
|
||||
trimmedText.trimmedString.substring(startIndex, endIndex),
|
||||
)
|
||||
matchedTextHighlightBatch.rect({
|
||||
x: left,
|
||||
y: physicalLabelBounds.top() + padding,
|
||||
w: highlightWidth,
|
||||
h: physicalViewSpaceFrameHeight - 2 * padding,
|
||||
})
|
||||
|
||||
left += highlightWidth
|
||||
lastEndIndex = endIndex
|
||||
}
|
||||
}
|
||||
|
||||
const batch = this.props.searchResults != null && !match ? fadedLabelBatch : labelBatch
|
||||
batch.text({
|
||||
text: trimmedText.trimmedString,
|
||||
|
||||
// This is specifying the position of the starting text baseline.
|
||||
x: physicalLabelBounds.left() + LABEL_PADDING_PX,
|
||||
y: Math.round(
|
||||
physicalLabelBounds.bottom() -
|
||||
(physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
for (let child of frame.children) {
|
||||
@@ -251,18 +282,14 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
}
|
||||
}
|
||||
|
||||
for (let frame of this.props.flamechart.getLayers()[0] || []) {
|
||||
renderFrameLabelAndChildren(frame)
|
||||
}
|
||||
|
||||
const frameOutlineWidth = 2 * window.devicePixelRatio
|
||||
ctx.strokeStyle = Colors.PALE_DARK_BLUE
|
||||
ctx.lineWidth = frameOutlineWidth
|
||||
const minConfigSpaceWidthToRenderOutline = (
|
||||
configToPhysical.inverseTransformVector(new Vec2(1, 0)) || new Vec2(0, 0)
|
||||
).x
|
||||
const renderIndirectlySelectedFrameOutlines = (frame: FlamechartFrame, depth = 0) => {
|
||||
if (!this.props.selectedNode) return
|
||||
|
||||
const renderSpecialFrameOutlines = (frame: FlamechartFrame, depth = 0) => {
|
||||
if (!this.props.selectedNode && this.props.searchResults == null) return
|
||||
const width = frame.end - frame.start
|
||||
const y = this.props.renderInverted ? this.configSpaceSize().y - 1 - depth : depth
|
||||
const configSpaceBounds = new Rect(new Vec2(frame.start, y), new Vec2(width, 1))
|
||||
@@ -273,44 +300,68 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
if (configSpaceBounds.top() > this.props.configSpaceViewportRect.bottom()) return
|
||||
|
||||
if (configSpaceBounds.hasIntersectionWith(this.props.configSpaceViewportRect)) {
|
||||
const physicalRectBounds = configToPhysical.transformRect(configSpaceBounds)
|
||||
if (this.props.searchResults?.getMatchForFrame(frame.node.frame)) {
|
||||
const physicalRectBounds = configToPhysical.transformRect(configSpaceBounds)
|
||||
matchedFrameBatch.rect({
|
||||
x: Math.round(physicalRectBounds.left() + frameOutlineWidth / 2),
|
||||
y: Math.round(physicalRectBounds.top() + frameOutlineWidth / 2),
|
||||
w: Math.round(Math.max(0, physicalRectBounds.width() - frameOutlineWidth)),
|
||||
h: Math.round(Math.max(0, physicalRectBounds.height() - frameOutlineWidth)),
|
||||
})
|
||||
}
|
||||
|
||||
if (frame.node.frame === this.props.selectedNode.frame) {
|
||||
if (frame.node === this.props.selectedNode) {
|
||||
if (ctx.strokeStyle !== Colors.DARK_BLUE) {
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = Colors.DARK_BLUE
|
||||
}
|
||||
} else {
|
||||
if (ctx.strokeStyle !== Colors.PALE_DARK_BLUE) {
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = Colors.PALE_DARK_BLUE
|
||||
}
|
||||
}
|
||||
if (this.props.selectedNode != null && frame.node.frame === this.props.selectedNode.frame) {
|
||||
let batch =
|
||||
frame.node === this.props.selectedNode
|
||||
? directlySelectedOutlineBatch
|
||||
: indirectlySelectedOutlineBatch
|
||||
|
||||
// Identify the flamechart frames with a function that matches the
|
||||
// selected flamechart frame.
|
||||
ctx.rect(
|
||||
Math.round(physicalRectBounds.left() + 1 + frameOutlineWidth / 2),
|
||||
Math.round(physicalRectBounds.top() + 1 + frameOutlineWidth / 2),
|
||||
Math.round(Math.max(0, physicalRectBounds.width() - 2 - frameOutlineWidth)),
|
||||
Math.round(Math.max(0, physicalRectBounds.height() - 2 - frameOutlineWidth)),
|
||||
)
|
||||
const physicalRectBounds = configToPhysical.transformRect(configSpaceBounds)
|
||||
batch.rect({
|
||||
x: Math.round(physicalRectBounds.left() + 1 + frameOutlineWidth / 2),
|
||||
y: Math.round(physicalRectBounds.top() + 1 + frameOutlineWidth / 2),
|
||||
w: Math.round(Math.max(0, physicalRectBounds.width() - 2 - frameOutlineWidth)),
|
||||
h: Math.round(Math.max(0, physicalRectBounds.height() - 2 - frameOutlineWidth)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (let child of frame.children) {
|
||||
renderIndirectlySelectedFrameOutlines(child, depth + 1)
|
||||
renderSpecialFrameOutlines(child, depth + 1)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.beginPath()
|
||||
for (let frame of this.props.flamechart.getLayers()[0] || []) {
|
||||
renderIndirectlySelectedFrameOutlines(frame)
|
||||
renderSpecialFrameOutlines(frame)
|
||||
}
|
||||
|
||||
for (let frame of this.props.flamechart.getLayers()[0] || []) {
|
||||
renderFrameLabelAndChildren(frame)
|
||||
}
|
||||
|
||||
matchedFrameBatch.fill(ctx, Colors.ORANGE)
|
||||
matchedTextHighlightBatch.fill(ctx, Colors.YELLOW)
|
||||
fadedLabelBatch.fill(ctx, Colors.LIGHT_GRAY)
|
||||
labelBatch.fill(ctx, Colors.BLACK)
|
||||
indirectlySelectedOutlineBatch.stroke(ctx, Colors.PALE_DARK_BLUE, frameOutlineWidth)
|
||||
directlySelectedOutlineBatch.stroke(ctx, Colors.DARK_BLUE, frameOutlineWidth)
|
||||
|
||||
if (this.hoveredLabel) {
|
||||
let color = Colors.DARK_GRAY
|
||||
if (this.props.selectedNode === this.hoveredLabel.node) {
|
||||
color = Colors.DARK_BLUE
|
||||
}
|
||||
|
||||
ctx.lineWidth = 2 * devicePixelRatio
|
||||
ctx.strokeStyle = color
|
||||
|
||||
const physicalViewBounds = configToPhysical.transformRect(this.hoveredLabel.configSpaceBounds)
|
||||
ctx.strokeRect(
|
||||
Math.round(physicalViewBounds.left()),
|
||||
Math.round(physicalViewBounds.top()),
|
||||
Math.round(Math.max(0, physicalViewBounds.width())),
|
||||
Math.round(Math.max(0, physicalViewBounds.height())),
|
||||
)
|
||||
}
|
||||
ctx.stroke()
|
||||
|
||||
this.renderTimeIndicators()
|
||||
}
|
||||
@@ -398,7 +449,11 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
),
|
||||
)
|
||||
}
|
||||
this.props.setLogicalSpaceViewportBounds(new Vec2(width, height))
|
||||
|
||||
const newSize = new Vec2(width, height)
|
||||
if (!newSize.equals(logicalSpaceViewportSize)) {
|
||||
this.props.setLogicalSpaceViewportSize(newSize)
|
||||
}
|
||||
}
|
||||
|
||||
onWindowResize = () => {
|
||||
@@ -688,6 +743,8 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
if (this.props.flamechart !== nextProps.flamechart) {
|
||||
this.hoveredLabel = null
|
||||
this.renderCanvas()
|
||||
} else if (this.props.searchResults !== nextProps.searchResults) {
|
||||
this.renderCanvas()
|
||||
} else if (this.props.selectedNode !== nextProps.selectedNode) {
|
||||
this.renderCanvas()
|
||||
} else if (this.props.configSpaceViewportRect !== nextProps.configSpaceViewportRect) {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import {memo} from 'preact/compat'
|
||||
import {useContext, useMemo, useCallback} from 'preact/hooks'
|
||||
import {SearchView, ProfileSearchContext} from './search-view'
|
||||
import {
|
||||
FlamechartSearchMatch,
|
||||
FlamechartSearchResults,
|
||||
ProfileSearchResults,
|
||||
} from '../lib/profile-search'
|
||||
import {Rect, Vec2} from '../lib/math'
|
||||
import {h, createContext, ComponentChildren} from 'preact'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {CallTreeNode} from '../lib/profile'
|
||||
|
||||
export const FlamechartSearchContext = createContext<FlamechartSearchData | null>(null)
|
||||
|
||||
export interface FlamechartSearchProps {
|
||||
flamechart: Flamechart
|
||||
selectedNode: CallTreeNode | null
|
||||
setSelectedNode: (node: CallTreeNode | null) => void
|
||||
configSpaceViewportRect: Rect
|
||||
setConfigSpaceViewportRect: (rect: Rect) => void
|
||||
children: ComponentChildren
|
||||
}
|
||||
|
||||
interface FlamechartSearchData {
|
||||
results: FlamechartSearchResults | null
|
||||
flamechart: Flamechart
|
||||
selectedNode: CallTreeNode | null
|
||||
setSelectedNode: (node: CallTreeNode | null) => void
|
||||
configSpaceViewportRect: Rect
|
||||
setConfigSpaceViewportRect: (rect: Rect) => void
|
||||
}
|
||||
|
||||
export const FlamechartSearchContextProvider = ({
|
||||
flamechart,
|
||||
selectedNode,
|
||||
setSelectedNode,
|
||||
configSpaceViewportRect,
|
||||
setConfigSpaceViewportRect,
|
||||
children,
|
||||
}: FlamechartSearchProps) => {
|
||||
const profileSearchResults: ProfileSearchResults | null = useContext(ProfileSearchContext)
|
||||
const flamechartSearchResults: FlamechartSearchResults | null = useMemo(() => {
|
||||
if (profileSearchResults == null) {
|
||||
return null
|
||||
}
|
||||
return new FlamechartSearchResults(flamechart, profileSearchResults)
|
||||
}, [flamechart, profileSearchResults])
|
||||
|
||||
return (
|
||||
<FlamechartSearchContext.Provider
|
||||
value={{
|
||||
results: flamechartSearchResults,
|
||||
flamechart,
|
||||
selectedNode,
|
||||
setSelectedNode,
|
||||
configSpaceViewportRect,
|
||||
setConfigSpaceViewportRect,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FlamechartSearchContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const FlamechartSearchView = memo(() => {
|
||||
const flamechartData = useContext(FlamechartSearchContext)
|
||||
|
||||
// TODO(jlfwong): This pattern is pretty gross, but I really don't want values
|
||||
// that can be undefined or null.
|
||||
const searchResults = flamechartData == null ? null : flamechartData.results
|
||||
const selectedNode = flamechartData == null ? null : flamechartData.selectedNode
|
||||
const setSelectedNode = flamechartData == null ? null : flamechartData.setSelectedNode
|
||||
const configSpaceViewportRect =
|
||||
flamechartData == null ? null : flamechartData.configSpaceViewportRect
|
||||
const setConfigSpaceViewportRect =
|
||||
flamechartData == null ? null : flamechartData.setConfigSpaceViewportRect
|
||||
const flamechart = flamechartData == null ? null : flamechartData.flamechart
|
||||
|
||||
const numResults = searchResults == null ? null : searchResults.count()
|
||||
const resultIndex: number | null = useMemo(() => {
|
||||
if (searchResults == null) return null
|
||||
if (selectedNode == null) return null
|
||||
return searchResults.indexOf(selectedNode)
|
||||
}, [searchResults, selectedNode])
|
||||
|
||||
const selectAndZoomToMatch = useCallback(
|
||||
(match: FlamechartSearchMatch) => {
|
||||
if (!setSelectedNode) return
|
||||
if (!flamechart) return
|
||||
if (!configSpaceViewportRect) return
|
||||
if (!setConfigSpaceViewportRect) return
|
||||
|
||||
// After the node is selected, we want to set the viewport so that the new
|
||||
// node can be seen clearly.
|
||||
//
|
||||
// TODO(jlfwong): The lack of animation here can be kind of jarring. It
|
||||
// would be nice to have some easier way for people to orient themselves
|
||||
// after the viewport shifted.
|
||||
const configSpaceResultBounds = match.configSpaceBounds
|
||||
|
||||
const viewportRect = new Rect(
|
||||
configSpaceResultBounds.origin.minus(new Vec2(0, 1)),
|
||||
configSpaceResultBounds.size.withY(configSpaceViewportRect.height()),
|
||||
)
|
||||
|
||||
setSelectedNode(match.node)
|
||||
setConfigSpaceViewportRect(
|
||||
flamechart.getClampedConfigSpaceViewportRect({configSpaceViewportRect: viewportRect}),
|
||||
)
|
||||
},
|
||||
[configSpaceViewportRect, setConfigSpaceViewportRect, setSelectedNode, flamechart],
|
||||
)
|
||||
|
||||
const {selectPrev, selectNext} = useMemo(() => {
|
||||
if (numResults == null || numResults === 0 || searchResults == null) {
|
||||
return {selectPrev: () => {}, selectNext: () => {}}
|
||||
}
|
||||
|
||||
return {
|
||||
selectPrev: () => {
|
||||
if (!searchResults?.at) return
|
||||
if (numResults == null || numResults === 0) return
|
||||
|
||||
let index = resultIndex == null ? numResults - 1 : resultIndex - 1
|
||||
if (index < 0) index = numResults - 1
|
||||
const result = searchResults.at(index)
|
||||
selectAndZoomToMatch(result)
|
||||
},
|
||||
|
||||
selectNext: () => {
|
||||
if (!searchResults?.at) return
|
||||
if (numResults == null || numResults === 0) return
|
||||
|
||||
let index = resultIndex == null ? 0 : resultIndex + 1
|
||||
if (index >= numResults) index = 0
|
||||
const result = searchResults.at(index)
|
||||
selectAndZoomToMatch(result)
|
||||
},
|
||||
}
|
||||
}, [numResults, resultIndex, searchResults, searchResults?.at, selectAndZoomToMatch])
|
||||
|
||||
return (
|
||||
<SearchView
|
||||
resultIndex={resultIndex}
|
||||
numResults={numResults}
|
||||
selectPrev={selectPrev}
|
||||
selectNext={selectNext}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -1,11 +1,11 @@
|
||||
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 {Dispatch, createContainer, ActionCreator} from '../lib/typed-redux'
|
||||
import {useActionCreator} from '../lib/preact-redux'
|
||||
import {Frame, Profile, CallTreeNode} from '../lib/profile'
|
||||
import {memoizeByShallowEquality} from '../lib/utils'
|
||||
import {ApplicationState} from '../store'
|
||||
import {FlamechartView} from './flamechart-view'
|
||||
import {
|
||||
getRowAtlas,
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
createGetCSSColorForFrame,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {ActiveProfileState} from './application'
|
||||
import {Vec2, Rect} from '../lib/math'
|
||||
import {actions} from '../store/actions'
|
||||
import {memo} from 'preact/compat'
|
||||
import {ActiveProfileState} from '../store'
|
||||
import {FlamechartSearchContextProvider} from './flamechart-search-view'
|
||||
|
||||
interface FlamechartSetters {
|
||||
setLogicalSpaceViewportSize: (logicalSpaceViewportSize: Vec2) => void
|
||||
@@ -25,46 +27,35 @@ interface FlamechartSetters {
|
||||
setSelectedNode: (node: CallTreeNode | null) => void
|
||||
}
|
||||
|
||||
interface WithFlamechartContext<T> {
|
||||
profileIndex: number
|
||||
args: {
|
||||
id: FlamechartID
|
||||
} & T
|
||||
}
|
||||
|
||||
export function createFlamechartSetters(
|
||||
dispatch: Dispatch,
|
||||
id: FlamechartID,
|
||||
profileIndex: number,
|
||||
): FlamechartSetters {
|
||||
function wrapActionCreator<T, U>(
|
||||
actionCreator: ActionCreator<WithFlamechartContext<U>>,
|
||||
map: (t: T) => U,
|
||||
): (t: T) => void {
|
||||
return (t: T) => {
|
||||
const args = Object.assign({}, map(t), {id})
|
||||
dispatch(actionCreator({profileIndex, args}))
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
setHoveredNode,
|
||||
setLogicalSpaceViewportSize,
|
||||
setConfigSpaceViewportRect,
|
||||
setSelectedNode,
|
||||
} = actions.flamechart
|
||||
const {
|
||||
setHoveredNode,
|
||||
setLogicalSpaceViewportSize,
|
||||
setConfigSpaceViewportRect,
|
||||
setSelectedNode,
|
||||
} = actions.flamechart
|
||||
|
||||
export function useFlamechartSetters(id: FlamechartID, profileIndex: number): FlamechartSetters {
|
||||
return {
|
||||
setNodeHover: wrapActionCreator(setHoveredNode, hover => ({hover})),
|
||||
setLogicalSpaceViewportSize: wrapActionCreator(
|
||||
setLogicalSpaceViewportSize,
|
||||
logicalSpaceViewportSize => ({logicalSpaceViewportSize}),
|
||||
setNodeHover: useActionCreator(
|
||||
(hover: {node: CallTreeNode; event: MouseEvent} | null) =>
|
||||
setHoveredNode({profileIndex, args: {id, hover}}),
|
||||
[profileIndex, id],
|
||||
),
|
||||
setConfigSpaceViewportRect: wrapActionCreator(
|
||||
setConfigSpaceViewportRect,
|
||||
configSpaceViewportRect => ({configSpaceViewportRect}),
|
||||
setLogicalSpaceViewportSize: useActionCreator(
|
||||
(logicalSpaceViewportSize: Vec2) =>
|
||||
setLogicalSpaceViewportSize({profileIndex, args: {id, logicalSpaceViewportSize}}),
|
||||
[profileIndex, id],
|
||||
),
|
||||
setConfigSpaceViewportRect: useActionCreator(
|
||||
(configSpaceViewportRect: Rect) =>
|
||||
setConfigSpaceViewportRect({profileIndex, args: {id, configSpaceViewportRect}}),
|
||||
[profileIndex, id],
|
||||
),
|
||||
setSelectedNode: useActionCreator(
|
||||
(selectedNode: CallTreeNode | null) =>
|
||||
setSelectedNode({profileIndex, args: {id, selectedNode}}),
|
||||
[profileIndex, id],
|
||||
),
|
||||
setSelectedNode: wrapActionCreator(setSelectedNode, selectedNode => ({selectedNode})),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,34 +112,43 @@ export interface FlamechartViewContainerProps {
|
||||
glCanvas: HTMLCanvasElement
|
||||
}
|
||||
|
||||
export const ChronoFlamechartView = createContainer(
|
||||
FlamechartView,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
const {index, profile, chronoViewState} = activeProfileState
|
||||
export const ChronoFlamechartView = memo((props: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = props
|
||||
const {index, profile, chronoViewState} = activeProfileState
|
||||
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame})
|
||||
const flamechartRenderer = getChronoViewFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame})
|
||||
const flamechartRenderer = getChronoViewFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
|
||||
return {
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.CHRONO, index),
|
||||
...chronoViewState,
|
||||
}
|
||||
},
|
||||
)
|
||||
const setters = useFlamechartSetters(FlamechartID.CHRONO, index)
|
||||
|
||||
return (
|
||||
<FlamechartSearchContextProvider
|
||||
flamechart={flamechart}
|
||||
selectedNode={chronoViewState.selectedNode}
|
||||
setSelectedNode={setters.setSelectedNode}
|
||||
configSpaceViewportRect={chronoViewState.configSpaceViewportRect}
|
||||
setConfigSpaceViewportRect={setters.setConfigSpaceViewportRect}
|
||||
>
|
||||
<FlamechartView
|
||||
renderInverted={false}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
canvasContext={canvasContext}
|
||||
getCSSColorForFrame={getCSSColorForFrame}
|
||||
{...chronoViewState}
|
||||
{...setters}
|
||||
/>
|
||||
</FlamechartSearchContextProvider>
|
||||
)
|
||||
})
|
||||
|
||||
export const getLeftHeavyFlamechart = memoizeByShallowEquality(
|
||||
({
|
||||
@@ -169,35 +169,44 @@ export const getLeftHeavyFlamechart = memoizeByShallowEquality(
|
||||
|
||||
const getLeftHeavyFlamechartRenderer = createMemoizedFlamechartRenderer()
|
||||
|
||||
export const LeftHeavyFlamechartView = createContainer(
|
||||
FlamechartView,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
|
||||
const {index, profile, leftHeavyViewState} = activeProfileState
|
||||
const {index, profile, leftHeavyViewState} = activeProfileState
|
||||
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
const flamechart = getLeftHeavyFlamechart({
|
||||
profile,
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getLeftHeavyFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
const flamechart = getLeftHeavyFlamechart({
|
||||
profile,
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getLeftHeavyFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
|
||||
return {
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.LEFT_HEAVY, index),
|
||||
...leftHeavyViewState,
|
||||
}
|
||||
},
|
||||
)
|
||||
const setters = useFlamechartSetters(FlamechartID.LEFT_HEAVY, index)
|
||||
|
||||
return (
|
||||
<FlamechartSearchContextProvider
|
||||
flamechart={flamechart}
|
||||
selectedNode={leftHeavyViewState.selectedNode}
|
||||
setSelectedNode={setters.setSelectedNode}
|
||||
configSpaceViewportRect={leftHeavyViewState.configSpaceViewportRect}
|
||||
setConfigSpaceViewportRect={setters.setConfigSpaceViewportRect}
|
||||
>
|
||||
<FlamechartView
|
||||
renderInverted={false}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
canvasContext={canvasContext}
|
||||
getCSSColorForFrame={getCSSColorForFrame}
|
||||
{...leftHeavyViewState}
|
||||
{...setters}
|
||||
/>
|
||||
</FlamechartSearchContextProvider>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {h} from 'preact'
|
||||
import {h, Fragment} from 'preact'
|
||||
import {css} from 'aphrodite'
|
||||
|
||||
import {CallTreeNode} from '../lib/profile'
|
||||
@@ -14,6 +14,8 @@ 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'
|
||||
|
||||
export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
private configSpaceSize() {
|
||||
@@ -62,7 +64,7 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
|
||||
formatValue(weight: number) {
|
||||
const totalWeight = this.props.flamechart.getTotalWeight()
|
||||
const percent = 100 * weight / totalWeight
|
||||
const percent = (100 * weight) / totalWeight
|
||||
const formattedPercent = formatPercent(percent)
|
||||
return `${this.props.flamechart.formatValue(weight)} (${formattedPercent})`
|
||||
}
|
||||
@@ -86,7 +88,7 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
}
|
||||
|
||||
container: HTMLDivElement | null = null
|
||||
containerRef = (container?: Element) => {
|
||||
containerRef = (container: Element | null) => {
|
||||
this.container = (container as HTMLDivElement) || null
|
||||
}
|
||||
|
||||
@@ -101,20 +103,28 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
canvasContext={this.props.canvasContext}
|
||||
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
|
||||
/>
|
||||
<FlamechartPanZoomView
|
||||
canvasContext={this.props.canvasContext}
|
||||
flamechart={this.props.flamechart}
|
||||
flamechartRenderer={this.props.flamechartRenderer}
|
||||
renderInverted={false}
|
||||
onNodeHover={this.onNodeHover}
|
||||
onNodeSelect={this.onNodeClick}
|
||||
selectedNode={this.props.selectedNode}
|
||||
transformViewport={this.transformViewport}
|
||||
configSpaceViewportRect={this.props.configSpaceViewportRect}
|
||||
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
|
||||
logicalSpaceViewportSize={this.props.logicalSpaceViewportSize}
|
||||
setLogicalSpaceViewportBounds={this.setLogicalSpaceViewportSize}
|
||||
/>
|
||||
<ProfileSearchContext.Consumer>
|
||||
{searchResults => (
|
||||
<Fragment>
|
||||
<FlamechartPanZoomView
|
||||
canvasContext={this.props.canvasContext}
|
||||
flamechart={this.props.flamechart}
|
||||
flamechartRenderer={this.props.flamechartRenderer}
|
||||
renderInverted={false}
|
||||
onNodeHover={this.onNodeHover}
|
||||
onNodeSelect={this.onNodeClick}
|
||||
selectedNode={this.props.selectedNode}
|
||||
transformViewport={this.transformViewport}
|
||||
configSpaceViewportRect={this.props.configSpaceViewportRect}
|
||||
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
|
||||
logicalSpaceViewportSize={this.props.logicalSpaceViewportSize}
|
||||
setLogicalSpaceViewportSize={this.setLogicalSpaceViewportSize}
|
||||
searchResults={searchResults}
|
||||
/>
|
||||
<FlamechartSearchView />
|
||||
</Fragment>
|
||||
)}
|
||||
</ProfileSearchContext.Consumer>
|
||||
{this.renderTooltip()}
|
||||
{this.props.selectedNode && (
|
||||
<FlamechartDetailView
|
||||
|
||||
@@ -12,15 +12,10 @@ import {StatelessComponent} from '../lib/typed-redux'
|
||||
export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
private clampViewportToFlamegraph(viewportRect: Rect) {
|
||||
const {flamechart, renderInverted} = this.props
|
||||
const configSpaceSize = new Vec2(flamechart.getTotalWeight(), flamechart.getLayers().length)
|
||||
const width = this.props.flamechart.getClampedViewportWidth(viewportRect.size.x)
|
||||
const size = viewportRect.size.withX(width)
|
||||
const origin = Vec2.clamp(
|
||||
viewportRect.origin,
|
||||
new Vec2(0, renderInverted ? 0 : -1),
|
||||
Vec2.max(Vec2.zero, configSpaceSize.minus(size).plus(new Vec2(0, 1))),
|
||||
)
|
||||
return new Rect(origin, viewportRect.size.withX(width))
|
||||
return flamechart.getClampedConfigSpaceViewportRect({
|
||||
configSpaceViewportRect: viewportRect,
|
||||
renderInverted,
|
||||
})
|
||||
}
|
||||
private setConfigSpaceViewportRect = (configSpaceViewportRect: Rect) => {
|
||||
this.props.setConfigSpaceViewportRect(this.clampViewportToFlamegraph(configSpaceViewportRect))
|
||||
@@ -34,7 +29,7 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
}
|
||||
private formatValue(weight: number) {
|
||||
const totalWeight = this.props.flamechart.getTotalWeight()
|
||||
const percent = 100 * weight / totalWeight
|
||||
const percent = (100 * weight) / totalWeight
|
||||
const formattedPercent = formatPercent(percent)
|
||||
return `${this.props.flamechart.formatValue(weight)} (${formattedPercent})`
|
||||
}
|
||||
@@ -54,7 +49,7 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
)
|
||||
}
|
||||
container: HTMLDivElement | null = null
|
||||
containerRef = (container?: Element) => {
|
||||
containerRef = (container: Element | null) => {
|
||||
this.container = (container as HTMLDivElement) || null
|
||||
}
|
||||
private setNodeHover = (
|
||||
@@ -83,7 +78,8 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
canvasContext={this.props.canvasContext}
|
||||
renderInverted={this.props.renderInverted}
|
||||
logicalSpaceViewportSize={this.props.logicalSpaceViewportSize}
|
||||
setLogicalSpaceViewportBounds={this.setLogicalSpaceViewportSize}
|
||||
setLogicalSpaceViewportSize={this.setLogicalSpaceViewportSize}
|
||||
searchResults={null}
|
||||
/>
|
||||
{this.renderTooltip()}
|
||||
</div>
|
||||
|
||||
@@ -14,12 +14,7 @@ export class Hovertip extends Component<HovertipProps, {}> {
|
||||
const width = containerSize.x
|
||||
const height = containerSize.y
|
||||
|
||||
const positionStyle: {
|
||||
left?: number
|
||||
right?: number
|
||||
top?: number
|
||||
bottom?: number
|
||||
} = {}
|
||||
const positionStyle: {[key: string]: number} = {}
|
||||
|
||||
const OFFSET_FROM_MOUSE = 7
|
||||
if (offset.x + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_WIDTH_MAX < width) {
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import {memoizeByShallowEquality} from '../lib/utils'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {
|
||||
createMemoizedFlamechartRenderer,
|
||||
FlamechartViewContainerProps,
|
||||
createFlamechartSetters,
|
||||
} from './flamechart-view-container'
|
||||
import {createContainer, Dispatch} from '../lib/typed-redux'
|
||||
import {ApplicationState} from '../store'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getProfileWithRecursionFlattened,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
|
||||
const getInvertedCallerProfile = memoizeByShallowEquality(
|
||||
({
|
||||
profile,
|
||||
frame,
|
||||
flattenRecursion,
|
||||
}: {
|
||||
profile: Profile
|
||||
frame: Frame
|
||||
flattenRecursion: boolean
|
||||
}): Profile => {
|
||||
let p = profile.getInvertedProfileForCallersOf(frame)
|
||||
return flattenRecursion ? p.getProfileWithRecursionFlattened() : p
|
||||
},
|
||||
)
|
||||
|
||||
const getInvertedCallerFlamegraph = memoizeByShallowEquality(
|
||||
({
|
||||
invertedCallerProfile,
|
||||
getColorBucketForFrame,
|
||||
}: {
|
||||
invertedCallerProfile: Profile
|
||||
getColorBucketForFrame: (frame: Frame) => number
|
||||
}): Flamechart => {
|
||||
return new Flamechart({
|
||||
getTotalWeight: invertedCallerProfile.getTotalNonIdleWeight.bind(invertedCallerProfile),
|
||||
forEachCall: invertedCallerProfile.forEachCallGrouped.bind(invertedCallerProfile),
|
||||
formatValue: invertedCallerProfile.formatValue.bind(invertedCallerProfile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const getInvertedCallerFlamegraphRenderer = createMemoizedFlamechartRenderer({inverted: true})
|
||||
|
||||
export const InvertedCallerFlamegraphView = createContainer(
|
||||
FlamechartWrapper,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
let {profile, sandwichViewState, index} = activeProfileState
|
||||
let {flattenRecursion, glCanvas} = state
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichViewState
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
profile = flattenRecursion ? getProfileWithRecursionFlattened(profile) : profile
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
|
||||
const flamechart = getInvertedCallerFlamegraph({
|
||||
invertedCallerProfile: getInvertedCallerProfile({
|
||||
profile,
|
||||
frame: selectedFrame,
|
||||
flattenRecursion,
|
||||
}),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getInvertedCallerFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return {
|
||||
renderInverted: true,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.SANDWICH_INVERTED_CALLERS, index),
|
||||
// This overrides the setSelectedNode specified in createFlamechartSettesr
|
||||
setSelectedNode: () => {},
|
||||
...callerCallee.invertedCallerFlamegraph,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
import {memoizeByShallowEquality, noop} from '../lib/utils'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {
|
||||
createMemoizedFlamechartRenderer,
|
||||
FlamechartViewContainerProps,
|
||||
useFlamechartSetters,
|
||||
} from './flamechart-view-container'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {useAppSelector} from '../store'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
import {h} from 'preact'
|
||||
import {memo} from 'preact/compat'
|
||||
|
||||
const getInvertedCallerProfile = memoizeByShallowEquality(
|
||||
({
|
||||
profile,
|
||||
frame,
|
||||
flattenRecursion,
|
||||
}: {
|
||||
profile: Profile
|
||||
frame: Frame
|
||||
flattenRecursion: boolean
|
||||
}): Profile => {
|
||||
let p = profile.getInvertedProfileForCallersOf(frame)
|
||||
return flattenRecursion ? p.getProfileWithRecursionFlattened() : p
|
||||
},
|
||||
)
|
||||
|
||||
const getInvertedCallerFlamegraph = memoizeByShallowEquality(
|
||||
({
|
||||
invertedCallerProfile,
|
||||
getColorBucketForFrame,
|
||||
}: {
|
||||
invertedCallerProfile: Profile
|
||||
getColorBucketForFrame: (frame: Frame) => number
|
||||
}): Flamechart => {
|
||||
return new Flamechart({
|
||||
getTotalWeight: invertedCallerProfile.getTotalNonIdleWeight.bind(invertedCallerProfile),
|
||||
forEachCall: invertedCallerProfile.forEachCallGrouped.bind(invertedCallerProfile),
|
||||
formatValue: invertedCallerProfile.formatValue.bind(invertedCallerProfile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const getInvertedCallerFlamegraphRenderer = createMemoizedFlamechartRenderer({inverted: true})
|
||||
|
||||
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, [])
|
||||
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichViewState
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
|
||||
const flamechart = getInvertedCallerFlamegraph({
|
||||
invertedCallerProfile: getInvertedCallerProfile({
|
||||
profile,
|
||||
frame: selectedFrame,
|
||||
flattenRecursion,
|
||||
}),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getInvertedCallerFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return (
|
||||
<FlamechartWrapper
|
||||
renderInverted={true}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
canvasContext={canvasContext}
|
||||
getCSSColorForFrame={getCSSColorForFrame}
|
||||
{...useFlamechartSetters(FlamechartID.SANDWICH_INVERTED_CALLERS, index)}
|
||||
{...callerCallee.invertedCallerFlamegraph}
|
||||
// This overrides the setSelectedNode specified in useFlamechartSettesr
|
||||
setSelectedNode={noop}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,410 @@
|
||||
import {Profile} from '../lib/profile'
|
||||
import {h, JSX, ComponentChild, Ref} from 'preact'
|
||||
import {useCallback, useState, useMemo, useEffect, useRef} from 'preact/hooks'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {Colors, ZIndex, Sizes} from './style'
|
||||
import {fuzzyMatchStrings} from '../lib/fuzzy-find'
|
||||
import {sortBy} from '../lib/utils'
|
||||
|
||||
interface ProfileSelectRowProps {
|
||||
setProfileIndexToView: (profileIndex: number) => void
|
||||
setHoveredProfileIndex: (profileIndex: number) => void
|
||||
profile: Profile
|
||||
matchedRanges: [number, number][]
|
||||
hovered: boolean
|
||||
selected: boolean
|
||||
indexInProfileGroup: number
|
||||
indexInFilteredListView: number
|
||||
profileCount: number
|
||||
nodeRef?: Ref<HTMLDivElement>
|
||||
closeProfileSelect: () => void
|
||||
}
|
||||
|
||||
function highlightRanges(text: string, ranges: [number, number][]): JSX.Element {
|
||||
const spans: ComponentChild[] = []
|
||||
let last = 0
|
||||
for (let range of ranges) {
|
||||
spans.push(text.slice(last, range[0]))
|
||||
spans.push(<span className={css(style.highlighted)}>{text.slice(range[0], range[1])}</span>)
|
||||
last = range[1]
|
||||
}
|
||||
spans.push(text.slice(last))
|
||||
|
||||
return <span>{spans}</span>
|
||||
}
|
||||
|
||||
export function ProfileSelectRow({
|
||||
setProfileIndexToView,
|
||||
setHoveredProfileIndex,
|
||||
profile,
|
||||
selected,
|
||||
hovered,
|
||||
profileCount,
|
||||
nodeRef,
|
||||
closeProfileSelect,
|
||||
indexInProfileGroup,
|
||||
matchedRanges,
|
||||
indexInFilteredListView,
|
||||
}: ProfileSelectRowProps) {
|
||||
const onMouseUp = useCallback(() => {
|
||||
closeProfileSelect()
|
||||
setProfileIndexToView(indexInProfileGroup)
|
||||
}, [closeProfileSelect, setProfileIndexToView, indexInProfileGroup])
|
||||
|
||||
const onMouseEnter = useCallback(
|
||||
(ev: Event) => {
|
||||
setHoveredProfileIndex(indexInProfileGroup)
|
||||
},
|
||||
[setHoveredProfileIndex, indexInProfileGroup],
|
||||
)
|
||||
|
||||
const name = profile.getName()
|
||||
|
||||
const maxDigits = 1 + Math.floor(Math.log10(profileCount))
|
||||
|
||||
const highlighted = useMemo(() => {
|
||||
const result = highlightRanges(name, matchedRanges)
|
||||
return result
|
||||
}, [name, matchedRanges])
|
||||
|
||||
// TODO(jlfwong): There's a really gnarly edge-case here where the highlighted
|
||||
// ranges are part of the text truncated by ellipsis. I'm just going to punt
|
||||
// on solving for that.
|
||||
return (
|
||||
<div
|
||||
ref={nodeRef}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseEnter={onMouseEnter}
|
||||
title={name}
|
||||
className={css(
|
||||
style.profileRow,
|
||||
indexInFilteredListView % 2 === 0 && style.profileRowEven,
|
||||
selected && style.profileRowSelected,
|
||||
hovered && style.profileRowHovered,
|
||||
)}
|
||||
>
|
||||
<span className={css(style.profileIndex)} style={{width: maxDigits + 'em'}}>
|
||||
{indexInProfileGroup + 1}:
|
||||
</span>{' '}
|
||||
{highlighted}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ProfileSelectProps {
|
||||
setProfileIndexToView: (profileIndex: number) => void
|
||||
indexToView: number
|
||||
profiles: Profile[]
|
||||
closeProfileSelect: () => void
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
function stopPropagation(ev: Event) {
|
||||
ev.stopPropagation()
|
||||
}
|
||||
|
||||
interface FilteredProfile {
|
||||
indexInProfileGroup: number
|
||||
profile: Profile
|
||||
matchedRanges: [number, number][]
|
||||
score: number
|
||||
}
|
||||
|
||||
function getSortedFilteredProfiles(profiles: Profile[], filterText: string): FilteredProfile[] {
|
||||
const filtered: FilteredProfile[] = []
|
||||
for (let i = 0; i < profiles.length; i++) {
|
||||
const profile = profiles[i]
|
||||
const match = fuzzyMatchStrings(profile.getName(), filterText)
|
||||
if (!match) continue
|
||||
filtered.push({
|
||||
indexInProfileGroup: i,
|
||||
profile,
|
||||
...match,
|
||||
})
|
||||
}
|
||||
sortBy(filtered, p => -p.score)
|
||||
return filtered
|
||||
}
|
||||
|
||||
export function ProfileSelect({
|
||||
profiles,
|
||||
closeProfileSelect,
|
||||
indexToView,
|
||||
visible,
|
||||
setProfileIndexToView,
|
||||
}: ProfileSelectProps) {
|
||||
const [filterText, setFilterText] = useState('')
|
||||
|
||||
const onFilterTextChange = useCallback(
|
||||
(ev: Event) => {
|
||||
const value = (ev.target as HTMLInputElement).value
|
||||
setFilterText(value)
|
||||
},
|
||||
[setFilterText],
|
||||
)
|
||||
|
||||
const focusFilterInput = useCallback(
|
||||
(node: HTMLInputElement | null) => {
|
||||
if (node) {
|
||||
if (visible) {
|
||||
node.select()
|
||||
} else {
|
||||
node.blur()
|
||||
}
|
||||
}
|
||||
},
|
||||
[visible],
|
||||
)
|
||||
|
||||
const filteredProfiles = useMemo(() => {
|
||||
return getSortedFilteredProfiles(profiles, filterText)
|
||||
}, [profiles, filterText])
|
||||
|
||||
const [hoveredProfileIndex, setHoveredProfileIndex] = useState<number | null>(0)
|
||||
|
||||
const selectedNodeRef = useRef<HTMLDivElement | null>(null)
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
// Whenever the profile select becomes visible...
|
||||
|
||||
// Clear any hovered element
|
||||
setHoveredProfileIndex(null)
|
||||
|
||||
// And scroll the selected profile into view, if possible
|
||||
if (selectedNodeRef.current !== null) {
|
||||
selectedNodeRef.current.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
// TODO(jlfwong): Hi-jacking the behavior of enter and the arrow keys won't
|
||||
// work well for some composition methods (e.g. a Chinese character
|
||||
// composition keyboard input method).
|
||||
const onFilterKeyUp = useCallback(
|
||||
(ev: KeyboardEvent) => {
|
||||
// Prevent the key-press from propagating to other keyboard shortcut
|
||||
// handlers in other components.
|
||||
ev.stopPropagation()
|
||||
|
||||
let newHoveredIndexInFilteredList: number | null = null
|
||||
|
||||
switch (ev.key) {
|
||||
case 'Enter': {
|
||||
if (hoveredProfileIndex != null) {
|
||||
closeProfileSelect()
|
||||
setProfileIndexToView(hoveredProfileIndex)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Escape': {
|
||||
closeProfileSelect()
|
||||
break
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
ev.preventDefault()
|
||||
newHoveredIndexInFilteredList = 0
|
||||
if (hoveredProfileIndex != null) {
|
||||
const indexInFilteredList = filteredProfiles.findIndex(
|
||||
p => p.indexInProfileGroup === hoveredProfileIndex,
|
||||
)
|
||||
if (indexInFilteredList !== -1) {
|
||||
newHoveredIndexInFilteredList = indexInFilteredList + 1
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
ev.preventDefault()
|
||||
newHoveredIndexInFilteredList = filteredProfiles.length - 1
|
||||
if (hoveredProfileIndex != null) {
|
||||
const indexInFilteredList = filteredProfiles.findIndex(
|
||||
p => p.indexInProfileGroup === hoveredProfileIndex,
|
||||
)
|
||||
if (indexInFilteredList !== -1) {
|
||||
newHoveredIndexInFilteredList = indexInFilteredList - 1
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
newHoveredIndexInFilteredList != null &&
|
||||
newHoveredIndexInFilteredList >= 0 &&
|
||||
newHoveredIndexInFilteredList < filteredProfiles.length
|
||||
) {
|
||||
const indexInProfileGroup =
|
||||
filteredProfiles[newHoveredIndexInFilteredList].indexInProfileGroup
|
||||
setHoveredProfileIndex(indexInProfileGroup)
|
||||
setPendingForcedScroll(true)
|
||||
}
|
||||
},
|
||||
[closeProfileSelect, setProfileIndexToView, hoveredProfileIndex, filteredProfiles],
|
||||
)
|
||||
|
||||
const [pendingForcedScroll, setPendingForcedScroll] = useState(false)
|
||||
useEffect(() => {
|
||||
// Whenever the list of filtered profiles changes, set the first element hovered.
|
||||
if (filteredProfiles.length > 0) {
|
||||
setHoveredProfileIndex(filteredProfiles[0].indexInProfileGroup)
|
||||
setPendingForcedScroll(true)
|
||||
}
|
||||
}, [setHoveredProfileIndex, filteredProfiles])
|
||||
|
||||
const hoveredNodeRef = useCallback(
|
||||
(hoveredNode: HTMLDivElement | null) => {
|
||||
if (pendingForcedScroll && hoveredNode) {
|
||||
hoveredNode.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
})
|
||||
setPendingForcedScroll(false)
|
||||
}
|
||||
},
|
||||
[pendingForcedScroll, setPendingForcedScroll],
|
||||
)
|
||||
|
||||
const selectedHoveredRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
selectedNodeRef.current = node
|
||||
hoveredNodeRef(node)
|
||||
},
|
||||
[selectedNodeRef, hoveredNodeRef],
|
||||
)
|
||||
|
||||
// We allow ProfileSelect to be aware of its own visibility in order to retain
|
||||
// its scroll offset state between times when it's hidden & shown, and also to
|
||||
// scroll the selected node into view once it becomes shown again after the
|
||||
// selected profile has changed.
|
||||
return (
|
||||
<div className={css(style.profileSelectOuter)}>
|
||||
<div className={css(style.caret)} />
|
||||
<div className={css(style.profileSelectBox)}>
|
||||
{/* We stop event propagation for key events on the input to prevent
|
||||
this from triggering keyboard shortcuts. */}
|
||||
<div className={css(style.filterInputContainer)}>
|
||||
<input
|
||||
type="text"
|
||||
ref={focusFilterInput}
|
||||
placeholder={'Filter...'}
|
||||
value={filterText}
|
||||
onInput={onFilterTextChange}
|
||||
onKeyDown={onFilterKeyUp}
|
||||
onKeyUp={stopPropagation}
|
||||
onKeyPress={stopPropagation}
|
||||
/>
|
||||
</div>
|
||||
<div className={css(style.profileSelectScrolling)}>
|
||||
{filteredProfiles.map(({profile, matchedRanges, indexInProfileGroup}, indexInList) => {
|
||||
let ref: Ref<HTMLDivElement> | undefined = undefined
|
||||
const selected = indexInProfileGroup === indexToView
|
||||
const hovered = indexInProfileGroup === hoveredProfileIndex
|
||||
if (selected && hovered) {
|
||||
ref = selectedHoveredRef
|
||||
} else if (selected) {
|
||||
ref = selectedNodeRef
|
||||
} else if (hovered) {
|
||||
ref = hoveredNodeRef
|
||||
}
|
||||
return (
|
||||
<ProfileSelectRow
|
||||
setHoveredProfileIndex={setHoveredProfileIndex}
|
||||
indexInProfileGroup={indexInProfileGroup}
|
||||
indexInFilteredListView={indexInList}
|
||||
hovered={indexInProfileGroup == hoveredProfileIndex}
|
||||
selected={indexInProfileGroup === indexToView}
|
||||
profile={profile}
|
||||
profileCount={profiles.length}
|
||||
nodeRef={ref}
|
||||
matchedRanges={matchedRanges}
|
||||
setProfileIndexToView={setProfileIndexToView}
|
||||
closeProfileSelect={closeProfileSelect}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{filteredProfiles.length === 0 ? (
|
||||
<div className={css(style.profileRow)}>No results match filter "{filterText}"</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const paddingHeight = 10
|
||||
|
||||
const style = StyleSheet.create({
|
||||
filterInputContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: 10,
|
||||
alignItems: 'stretch',
|
||||
},
|
||||
caret: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: '5px solid transparent',
|
||||
borderRight: '5px solid transparent',
|
||||
borderBottom: '5px solid black',
|
||||
},
|
||||
highlighted: {
|
||||
background: Colors.PALE_DARK_BLUE,
|
||||
},
|
||||
padding: {
|
||||
height: paddingHeight,
|
||||
background: Colors.BLACK,
|
||||
},
|
||||
profileRow: {
|
||||
height: Sizes.FRAME_HEIGHT - 2,
|
||||
border: '1px solid transparent',
|
||||
textAlign: 'left',
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
background: Colors.BLACK,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
profileRowHovered: {
|
||||
border: `1px solid ${Colors.DARK_BLUE}`,
|
||||
},
|
||||
profileRowSelected: {
|
||||
background: Colors.DARK_BLUE,
|
||||
},
|
||||
profileRowEven: {
|
||||
background: Colors.DARK_GRAY,
|
||||
},
|
||||
profileSelectScrolling: {
|
||||
maxHeight: `min(calc(100vh - ${Sizes.TOOLBAR_HEIGHT - 2 * paddingHeight}px), ${
|
||||
20 * Sizes.FRAME_HEIGHT
|
||||
}px)`,
|
||||
overflow: 'auto',
|
||||
},
|
||||
profileSelectBox: {
|
||||
width: '100%',
|
||||
paddingBottom: 10,
|
||||
background: Colors.BLACK,
|
||||
color: Colors.WHITE,
|
||||
},
|
||||
profileSelectOuter: {
|
||||
width: '100%',
|
||||
maxWidth: 480,
|
||||
margin: '0 auto',
|
||||
position: 'relative',
|
||||
zIndex: ZIndex.PROFILE_SELECT,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
profileIndex: {
|
||||
textAlign: 'right',
|
||||
display: 'inline-block',
|
||||
color: Colors.LIGHT_GRAY,
|
||||
},
|
||||
})
|
||||
+252
-178
@@ -1,15 +1,17 @@
|
||||
import {h, Component} from 'preact'
|
||||
import {h, Component, JSX, ComponentChild} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {sortBy, formatPercent} from '../lib/utils'
|
||||
import {formatPercent} from '../lib/utils'
|
||||
import {FontSize, Colors, Sizes, commonStyle} from './style'
|
||||
import {ColorChit} from './color-chit'
|
||||
import {ScrollableListView, ListItem} from './scrollable-list-view'
|
||||
import {ListItem, ScrollableListView} from './scrollable-list-view'
|
||||
import {actions} from '../store/actions'
|
||||
import {Dispatch, createContainer} from '../lib/typed-redux'
|
||||
import {ApplicationState} from '../store'
|
||||
import {createGetCSSColorForFrame, getFrameToColorBucket} from '../store/getters'
|
||||
import {ActiveProfileState} from './application'
|
||||
import {useActionCreator} from '../lib/preact-redux'
|
||||
import {useAppSelector, ActiveProfileState} from '../store'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useCallback, useMemo, useContext} from 'preact/hooks'
|
||||
import {SandwichViewContext} from './sandwich-view'
|
||||
|
||||
export enum SortField {
|
||||
SYMBOL_NAME,
|
||||
@@ -31,14 +33,12 @@ interface HBarProps {
|
||||
perc: number
|
||||
}
|
||||
|
||||
class HBarDisplay extends Component<HBarProps, {}> {
|
||||
render() {
|
||||
return (
|
||||
<div className={css(style.hBarDisplay)}>
|
||||
<div className={css(style.hBarDisplayFilled)} style={{width: `${this.props.perc}%`}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
function HBarDisplay(props: HBarProps) {
|
||||
return (
|
||||
<div className={css(style.hBarDisplay)}>
|
||||
<div className={css(style.hBarDisplayFilled)} style={{width: `${props.perc}%`}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SortIconProps {
|
||||
@@ -67,156 +67,219 @@ class SortIcon extends Component<SortIconProps, {}> {
|
||||
}
|
||||
}
|
||||
|
||||
interface ProfileTableRowViewProps {
|
||||
frame: Frame
|
||||
matchedRanges: [number, number][] | null
|
||||
index: number
|
||||
profile: Profile
|
||||
selectedFrame: Frame | null
|
||||
setSelectedFrame: (f: Frame) => void
|
||||
getCSSColorForFrame: (frame: Frame) => string
|
||||
}
|
||||
|
||||
function highlightRanges(
|
||||
text: string,
|
||||
ranges: [number, number][],
|
||||
highlightedClassName: string,
|
||||
): JSX.Element {
|
||||
const spans: ComponentChild[] = []
|
||||
let last = 0
|
||||
for (let range of ranges) {
|
||||
spans.push(text.slice(last, range[0]))
|
||||
spans.push(<span className={highlightedClassName}>{text.slice(range[0], range[1])}</span>)
|
||||
last = range[1]
|
||||
}
|
||||
spans.push(text.slice(last))
|
||||
|
||||
return <span>{spans}</span>
|
||||
}
|
||||
|
||||
const ProfileTableRowView = ({
|
||||
frame,
|
||||
matchedRanges,
|
||||
profile,
|
||||
index,
|
||||
selectedFrame,
|
||||
setSelectedFrame,
|
||||
getCSSColorForFrame,
|
||||
}: ProfileTableRowViewProps) => {
|
||||
const totalWeight = frame.getTotalWeight()
|
||||
const selfWeight = frame.getSelfWeight()
|
||||
const totalPerc = (100.0 * totalWeight) / profile.getTotalNonIdleWeight()
|
||||
const selfPerc = (100.0 * selfWeight) / profile.getTotalNonIdleWeight()
|
||||
|
||||
const selected = frame === selectedFrame
|
||||
|
||||
// We intentionally use index rather than frame.key here as the tr key
|
||||
// in order to re-use rows when sorting rather than creating all new elements.
|
||||
return (
|
||||
<tr
|
||||
key={`${index}`}
|
||||
onClick={setSelectedFrame.bind(null, frame)}
|
||||
className={css(
|
||||
style.tableRow,
|
||||
index % 2 == 0 && style.tableRowEven,
|
||||
selected && style.tableRowSelected,
|
||||
)}
|
||||
>
|
||||
<td className={css(style.numericCell)}>
|
||||
{profile.formatValue(totalWeight)} ({formatPercent(totalPerc)})
|
||||
<HBarDisplay perc={totalPerc} />
|
||||
</td>
|
||||
<td className={css(style.numericCell)}>
|
||||
{profile.formatValue(selfWeight)} ({formatPercent(selfPerc)})
|
||||
<HBarDisplay perc={selfPerc} />
|
||||
</td>
|
||||
<td title={frame.file} className={css(style.textCell)}>
|
||||
<ColorChit color={getCSSColorForFrame(frame)} />
|
||||
{matchedRanges
|
||||
? highlightRanges(
|
||||
frame.name,
|
||||
matchedRanges,
|
||||
css(style.matched, selected && style.matchedSelected),
|
||||
)
|
||||
: frame.name}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
interface ProfileTableViewProps {
|
||||
profile: Profile
|
||||
profileIndex: number
|
||||
selectedFrame: Frame | null
|
||||
getCSSColorForFrame: (frame: Frame) => string
|
||||
sortMethod: SortMethod
|
||||
setSelectedFrame: (frame: Frame | null) => void
|
||||
setSortMethod: (sortMethod: SortMethod) => void
|
||||
searchQuery: string
|
||||
searchIsActive: boolean
|
||||
}
|
||||
|
||||
export class ProfileTableView extends Component<ProfileTableViewProps, void> {
|
||||
renderRow(frame: Frame, index: number) {
|
||||
const {profile, selectedFrame} = this.props
|
||||
export const ProfileTableView = memo(
|
||||
({
|
||||
profile,
|
||||
sortMethod,
|
||||
setSortMethod,
|
||||
selectedFrame,
|
||||
setSelectedFrame,
|
||||
getCSSColorForFrame,
|
||||
searchQuery,
|
||||
searchIsActive,
|
||||
}: ProfileTableViewProps) => {
|
||||
const onSortClick = useCallback(
|
||||
(field: SortField, ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
const totalWeight = frame.getTotalWeight()
|
||||
const selfWeight = frame.getSelfWeight()
|
||||
const totalPerc = 100.0 * totalWeight / profile.getTotalNonIdleWeight()
|
||||
const selfPerc = 100.0 * selfWeight / profile.getTotalNonIdleWeight()
|
||||
|
||||
const selected = frame === selectedFrame
|
||||
|
||||
// We intentionally use index rather than frame.key here as the tr key
|
||||
// in order to re-use rows when sorting rather than creating all new elements.
|
||||
return (
|
||||
<tr
|
||||
key={`${index}`}
|
||||
onClick={this.props.setSelectedFrame.bind(null, frame)}
|
||||
className={css(
|
||||
style.tableRow,
|
||||
index % 2 == 0 && style.tableRowEven,
|
||||
selected && style.tableRowSelected,
|
||||
)}
|
||||
>
|
||||
<td className={css(style.numericCell)}>
|
||||
{profile.formatValue(totalWeight)} ({formatPercent(totalPerc)})
|
||||
<HBarDisplay perc={totalPerc} />
|
||||
</td>
|
||||
<td className={css(style.numericCell)}>
|
||||
{profile.formatValue(selfWeight)} ({formatPercent(selfPerc)})
|
||||
<HBarDisplay perc={selfPerc} />
|
||||
</td>
|
||||
<td title={frame.file} className={css(style.textCell)}>
|
||||
<ColorChit color={this.props.getCSSColorForFrame(frame)} />
|
||||
{frame.name}
|
||||
</td>
|
||||
</tr>
|
||||
if (sortMethod.field == field) {
|
||||
// Toggle
|
||||
setSortMethod({
|
||||
field,
|
||||
direction:
|
||||
sortMethod.direction === SortDirection.ASCENDING
|
||||
? SortDirection.DESCENDING
|
||||
: SortDirection.ASCENDING,
|
||||
})
|
||||
} else {
|
||||
// Set a sane default
|
||||
switch (field) {
|
||||
case SortField.SYMBOL_NAME: {
|
||||
setSortMethod({field, direction: SortDirection.ASCENDING})
|
||||
break
|
||||
}
|
||||
case SortField.SELF: {
|
||||
setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
}
|
||||
case SortField.TOTAL: {
|
||||
setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[sortMethod, setSortMethod],
|
||||
)
|
||||
}
|
||||
|
||||
onSortClick = (field: SortField, ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
const sandwichContext = useContext(SandwichViewContext)
|
||||
|
||||
const {sortMethod} = this.props
|
||||
const renderItems = useCallback(
|
||||
(firstIndex: number, lastIndex: number) => {
|
||||
if (!sandwichContext) return null
|
||||
|
||||
if (sortMethod.field == field) {
|
||||
// Toggle
|
||||
this.props.setSortMethod({
|
||||
field,
|
||||
direction:
|
||||
sortMethod.direction === SortDirection.ASCENDING
|
||||
? SortDirection.DESCENDING
|
||||
: SortDirection.ASCENDING,
|
||||
})
|
||||
} else {
|
||||
// Set a sane default
|
||||
switch (field) {
|
||||
case SortField.SYMBOL_NAME: {
|
||||
this.props.setSortMethod({field, direction: SortDirection.ASCENDING})
|
||||
break
|
||||
const rows: JSX.Element[] = []
|
||||
|
||||
for (let i = firstIndex; i <= lastIndex; i++) {
|
||||
const frame = sandwichContext.rowList[i]
|
||||
const match = sandwichContext.getSearchMatchForFrame(frame)
|
||||
rows.push(
|
||||
ProfileTableRowView({
|
||||
frame,
|
||||
matchedRanges: match == null ? null : match.matchedRanges,
|
||||
index: i,
|
||||
profile: profile,
|
||||
selectedFrame: selectedFrame,
|
||||
setSelectedFrame: setSelectedFrame,
|
||||
getCSSColorForFrame: getCSSColorForFrame,
|
||||
}),
|
||||
)
|
||||
}
|
||||
case SortField.SELF: {
|
||||
this.props.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
|
||||
if (rows.length === 0) {
|
||||
if (searchIsActive) {
|
||||
rows.push(
|
||||
<tr>
|
||||
<td className={css(style.emptyState)}>
|
||||
No symbol names match query "{searchQuery}".
|
||||
</td>
|
||||
</tr>,
|
||||
)
|
||||
} else {
|
||||
rows.push(
|
||||
<tr>
|
||||
<td className={css(style.emptyState)}>No symbols found.</td>
|
||||
</tr>,
|
||||
)
|
||||
}
|
||||
}
|
||||
case SortField.TOTAL: {
|
||||
this.props.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getFrameList = (): Frame[] => {
|
||||
const {profile, sortMethod} = this.props
|
||||
return <table className={css(style.tableView)}>{rows}</table>
|
||||
},
|
||||
[
|
||||
sandwichContext,
|
||||
profile,
|
||||
selectedFrame,
|
||||
setSelectedFrame,
|
||||
getCSSColorForFrame,
|
||||
searchIsActive,
|
||||
searchQuery,
|
||||
],
|
||||
)
|
||||
|
||||
const frameList: Frame[] = []
|
||||
const listItems: ListItem[] = useMemo(
|
||||
() =>
|
||||
sandwichContext == null
|
||||
? []
|
||||
: sandwichContext.rowList.map(f => ({size: Sizes.FRAME_HEIGHT})),
|
||||
[sandwichContext],
|
||||
)
|
||||
|
||||
profile.forEachFrame(f => frameList.push(f))
|
||||
|
||||
// TODO(jlfwong): This is pretty inefficient to do this on every render, but doesn't
|
||||
// seem to be a bottleneck, so we'll leave it alone.
|
||||
switch (sortMethod.field) {
|
||||
case SortField.SYMBOL_NAME: {
|
||||
sortBy(frameList, f => f.name.toLowerCase())
|
||||
break
|
||||
}
|
||||
case SortField.SELF: {
|
||||
sortBy(frameList, f => f.getSelfWeight())
|
||||
break
|
||||
}
|
||||
case SortField.TOTAL: {
|
||||
sortBy(frameList, f => f.getTotalWeight())
|
||||
break
|
||||
}
|
||||
}
|
||||
if (sortMethod.direction === SortDirection.DESCENDING) {
|
||||
frameList.reverse()
|
||||
}
|
||||
|
||||
return frameList
|
||||
}
|
||||
|
||||
private listView: ScrollableListView | null = null
|
||||
private listViewRef = (listView: ScrollableListView | null) => {
|
||||
if (listView === this.listView) return
|
||||
this.listView = listView
|
||||
|
||||
const {selectedFrame} = this.props
|
||||
if (!selectedFrame || !listView) return
|
||||
const index = this.getFrameList().indexOf(selectedFrame)
|
||||
if (index === -1) return
|
||||
listView.scrollIndexIntoView(index)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {sortMethod} = this.props
|
||||
|
||||
const frameList = this.getFrameList()
|
||||
|
||||
const renderItems = (firstIndex: number, lastIndex: number) => {
|
||||
const rows: JSX.Element[] = []
|
||||
|
||||
for (let i = firstIndex; i <= lastIndex; i++) {
|
||||
rows.push(this.renderRow(frameList[i], i))
|
||||
}
|
||||
|
||||
return <table className={css(style.tableView)}>{rows}</table>
|
||||
}
|
||||
|
||||
const listItems: ListItem[] = frameList.map(f => ({size: Sizes.FRAME_HEIGHT}))
|
||||
const onTotalClick = useCallback((ev: MouseEvent) => onSortClick(SortField.TOTAL, ev), [
|
||||
onSortClick,
|
||||
])
|
||||
const onSelfClick = useCallback((ev: MouseEvent) => onSortClick(SortField.SELF, ev), [
|
||||
onSortClick,
|
||||
])
|
||||
const onSymbolNameClick = useCallback(
|
||||
(ev: MouseEvent) => onSortClick(SortField.SYMBOL_NAME, ev),
|
||||
[onSortClick],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css(commonStyle.vbox, style.profileTableView)}>
|
||||
<table className={css(style.tableView)}>
|
||||
<thead className={css(style.tableHeader)}>
|
||||
<tr>
|
||||
<th
|
||||
className={css(style.numericCell)}
|
||||
onClick={ev => this.onSortClick(SortField.TOTAL, ev)}
|
||||
>
|
||||
<th className={css(style.numericCell)} onClick={onTotalClick}>
|
||||
<SortIcon
|
||||
activeDirection={
|
||||
sortMethod.field === SortField.TOTAL ? sortMethod.direction : null
|
||||
@@ -224,10 +287,7 @@ export class ProfileTableView extends Component<ProfileTableViewProps, void> {
|
||||
/>
|
||||
Total
|
||||
</th>
|
||||
<th
|
||||
className={css(style.numericCell)}
|
||||
onClick={ev => this.onSortClick(SortField.SELF, ev)}
|
||||
>
|
||||
<th className={css(style.numericCell)} onClick={onSelfClick}>
|
||||
<SortIcon
|
||||
activeDirection={
|
||||
sortMethod.field === SortField.SELF ? sortMethod.direction : null
|
||||
@@ -235,10 +295,7 @@ export class ProfileTableView extends Component<ProfileTableViewProps, void> {
|
||||
/>
|
||||
Self
|
||||
</th>
|
||||
<th
|
||||
className={css(style.textCell)}
|
||||
onClick={ev => this.onSortClick(SortField.SYMBOL_NAME, ev)}
|
||||
>
|
||||
<th className={css(style.textCell)} onClick={onSymbolNameClick}>
|
||||
<SortIcon
|
||||
activeDirection={
|
||||
sortMethod.field === SortField.SYMBOL_NAME ? sortMethod.direction : null
|
||||
@@ -250,16 +307,18 @@ export class ProfileTableView extends Component<ProfileTableViewProps, void> {
|
||||
</thead>
|
||||
</table>
|
||||
<ScrollableListView
|
||||
ref={this.listViewRef}
|
||||
axis={'y'}
|
||||
items={listItems}
|
||||
className={css(style.scrollView)}
|
||||
renderItems={renderItems}
|
||||
initialIndexInView={
|
||||
selectedFrame == null ? null : sandwichContext?.getIndexForFrame(selectedFrame)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const style = StyleSheet.create({
|
||||
profileTableView: {
|
||||
@@ -269,6 +328,7 @@ const style = StyleSheet.create({
|
||||
scrollView: {
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
flexGrow: 1,
|
||||
},
|
||||
tableView: {
|
||||
width: '100%',
|
||||
@@ -327,40 +387,54 @@ const style = StyleSheet.create({
|
||||
background: Colors.GREEN,
|
||||
right: 0,
|
||||
},
|
||||
matched: {
|
||||
borderBottom: `2px solid ${Colors.BLACK}`,
|
||||
},
|
||||
matchedSelected: {
|
||||
borderColor: Colors.WHITE,
|
||||
},
|
||||
emptyState: {
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
})
|
||||
|
||||
interface ProfileTableViewContainerProps {
|
||||
activeProfileState: ActiveProfileState
|
||||
}
|
||||
|
||||
export const ProfileTableViewContainer = createContainer(
|
||||
ProfileTableView,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: ProfileTableViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
const {profile, sandwichViewState, index} = activeProfileState
|
||||
if (!profile) throw new Error('profile missing')
|
||||
const {tableSortMethod} = state
|
||||
const {callerCallee} = sandwichViewState
|
||||
const selectedFrame = callerCallee ? callerCallee.selectedFrame : null
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const {setTableSortMethod} = actions.sandwichView
|
||||
|
||||
const setSelectedFrame = (selectedFrame: Frame | null) => {
|
||||
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: index, args: selectedFrame}))
|
||||
}
|
||||
export const ProfileTableViewContainer = memo((ownProps: ProfileTableViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
const {profile, sandwichViewState, index} = activeProfileState
|
||||
if (!profile) throw new Error('profile missing')
|
||||
const tableSortMethod = useAppSelector(state => state.tableSortMethod, [])
|
||||
const {callerCallee} = sandwichViewState
|
||||
const selectedFrame = callerCallee ? callerCallee.selectedFrame : null
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
const setSortMethod = (sortMethod: SortMethod) => {
|
||||
dispatch(actions.sandwichView.setTableSortMethod(sortMethod))
|
||||
}
|
||||
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, [])
|
||||
|
||||
return {
|
||||
profile,
|
||||
profileIndex: activeProfileState.index,
|
||||
selectedFrame,
|
||||
getCSSColorForFrame,
|
||||
sortMethod: tableSortMethod,
|
||||
setSelectedFrame,
|
||||
setSortMethod,
|
||||
}
|
||||
},
|
||||
)
|
||||
return (
|
||||
<ProfileTableView
|
||||
profile={profile}
|
||||
selectedFrame={selectedFrame}
|
||||
getCSSColorForFrame={getCSSColorForFrame}
|
||||
sortMethod={tableSortMethod}
|
||||
setSelectedFrame={setSelectedFrame}
|
||||
setSortMethod={setSortMethod}
|
||||
searchIsActive={searchIsActive}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {memo} from 'preact/compat'
|
||||
import {useContext, useMemo} from 'preact/hooks'
|
||||
import {SearchView} from './search-view'
|
||||
import {h} from 'preact'
|
||||
import {SandwichViewContext} from './sandwich-view'
|
||||
|
||||
export const SandwichSearchView = memo(() => {
|
||||
const sandwichViewContext = useContext(SandwichViewContext)
|
||||
|
||||
const rowList = sandwichViewContext != null ? sandwichViewContext.rowList : null
|
||||
const resultIndex =
|
||||
sandwichViewContext?.selectedFrame != null
|
||||
? sandwichViewContext.getIndexForFrame(sandwichViewContext.selectedFrame)
|
||||
: null
|
||||
const numResults = rowList != null ? rowList.length : null
|
||||
|
||||
const {selectPrev, selectNext} = useMemo(() => {
|
||||
if (rowList == null || numResults == null || numResults === 0 || sandwichViewContext == null) {
|
||||
return {selectPrev: () => {}, selectNext: () => {}}
|
||||
}
|
||||
|
||||
return {
|
||||
selectPrev: () => {
|
||||
let index = resultIndex == null ? numResults - 1 : resultIndex - 1
|
||||
if (index < 0) index = numResults - 1
|
||||
sandwichViewContext.setSelectedFrame(rowList[index])
|
||||
},
|
||||
selectNext: () => {
|
||||
let index = resultIndex == null ? 0 : resultIndex + 1
|
||||
if (index >= numResults) index = 0
|
||||
sandwichViewContext.setSelectedFrame(rowList[index])
|
||||
},
|
||||
}
|
||||
}, [resultIndex, rowList, numResults, sandwichViewContext])
|
||||
|
||||
return (
|
||||
<SearchView
|
||||
resultIndex={resultIndex}
|
||||
numResults={numResults}
|
||||
selectPrev={selectPrev}
|
||||
selectNext={selectNext}
|
||||
/>
|
||||
)
|
||||
})
|
||||
+106
-20
@@ -1,14 +1,20 @@
|
||||
import {Frame} from '../lib/profile'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {ProfileTableViewContainer} from './profile-table-view'
|
||||
import {h} from 'preact'
|
||||
import {ProfileTableViewContainer, SortField, SortDirection} 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, Colors, FontSize} from './style'
|
||||
import {actions} from '../store/actions'
|
||||
import {createContainer, Dispatch, StatelessComponent} from '../lib/typed-redux'
|
||||
import {ApplicationState} from '../store'
|
||||
import {StatelessComponent} from '../lib/typed-redux'
|
||||
import {InvertedCallerFlamegraphView} from './inverted-caller-flamegraph-view'
|
||||
import {CalleeFlamegraphView} from './callee-flamegraph-view'
|
||||
import {ActiveProfileState} from './application'
|
||||
import {useDispatch} from '../lib/preact-redux'
|
||||
import {SandwichSearchView} from './sandwich-search-view'
|
||||
import {useAppSelector, ActiveProfileState} from '../store'
|
||||
import {sortBy} from '../lib/utils'
|
||||
import {ProfileSearchContext} from './search-view'
|
||||
import {FuzzyMatch} from '../lib/fuzzy-find'
|
||||
|
||||
interface SandwichViewProps {
|
||||
selectedFrame: Frame | null
|
||||
@@ -70,6 +76,7 @@ class SandwichView extends StatelessComponent<SandwichViewProps> {
|
||||
<div className={css(commonStyle.hbox, commonStyle.fillY)}>
|
||||
<div className={css(style.tableView)}>
|
||||
<ProfileTableViewContainer activeProfileState={this.props.activeProfileState} />
|
||||
<SandwichSearchView />
|
||||
</div>
|
||||
{flamegraphViews}
|
||||
</div>
|
||||
@@ -79,6 +86,7 @@ class SandwichView extends StatelessComponent<SandwichViewProps> {
|
||||
|
||||
const style = StyleSheet.create({
|
||||
tableView: {
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
},
|
||||
panZoomViewWraper: {
|
||||
@@ -122,28 +130,106 @@ interface SandwichViewContainerProps {
|
||||
glCanvas: HTMLCanvasElement
|
||||
}
|
||||
|
||||
export const SandwichViewContainer = createContainer(
|
||||
SandwichView,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: SandwichViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
const {sandwichViewState, index} = activeProfileState
|
||||
const {callerCallee} = sandwichViewState
|
||||
interface SandwichViewContextData {
|
||||
rowList: Frame[]
|
||||
selectedFrame: Frame | null
|
||||
setSelectedFrame: (frame: Frame | null) => void
|
||||
getIndexForFrame: (frame: Frame) => number | null
|
||||
getSearchMatchForFrame: (frame: Frame) => FuzzyMatch | null
|
||||
}
|
||||
|
||||
const setSelectedFrame = (selectedFrame: Frame | null) => {
|
||||
export const SandwichViewContext = createContext<SandwichViewContextData | null>(null)
|
||||
|
||||
export const SandwichViewContainer = memo((ownProps: SandwichViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
const {sandwichViewState, index} = activeProfileState
|
||||
const {callerCallee} = sandwichViewState
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const setSelectedFrame = useCallback(
|
||||
(selectedFrame: Frame | null) => {
|
||||
dispatch(
|
||||
actions.sandwichView.setSelectedFrame({
|
||||
profileIndex: index,
|
||||
args: selectedFrame,
|
||||
}),
|
||||
)
|
||||
},
|
||||
[dispatch, index],
|
||||
)
|
||||
|
||||
const profile = activeProfileState.profile
|
||||
const tableSortMethod = useAppSelector(state => state.tableSortMethod, [])
|
||||
const profileSearchResults = useContext(ProfileSearchContext)
|
||||
|
||||
const selectedFrame = callerCallee ? callerCallee.selectedFrame : null
|
||||
|
||||
const rowList: Frame[] = useMemo(() => {
|
||||
const rowList: Frame[] = []
|
||||
|
||||
profile.forEachFrame(frame => {
|
||||
if (profileSearchResults && !profileSearchResults.getMatchForFrame(frame)) {
|
||||
return
|
||||
}
|
||||
rowList.push(frame)
|
||||
})
|
||||
|
||||
switch (tableSortMethod.field) {
|
||||
case SortField.SYMBOL_NAME: {
|
||||
sortBy(rowList, f => f.name.toLowerCase())
|
||||
break
|
||||
}
|
||||
case SortField.SELF: {
|
||||
sortBy(rowList, f => f.getSelfWeight())
|
||||
break
|
||||
}
|
||||
case SortField.TOTAL: {
|
||||
sortBy(rowList, f => f.getTotalWeight())
|
||||
break
|
||||
}
|
||||
}
|
||||
if (tableSortMethod.direction === SortDirection.DESCENDING) {
|
||||
rowList.reverse()
|
||||
}
|
||||
|
||||
return {
|
||||
activeProfileState: activeProfileState,
|
||||
glCanvas,
|
||||
setSelectedFrame,
|
||||
selectedFrame: callerCallee ? callerCallee.selectedFrame : null,
|
||||
profileIndex: index,
|
||||
return rowList
|
||||
}, [profile, profileSearchResults, tableSortMethod])
|
||||
|
||||
const getIndexForFrame: (frame: Frame) => number | null = useMemo(() => {
|
||||
const indexByFrame = new Map<Frame, number>()
|
||||
for (let i = 0; i < rowList.length; i++) {
|
||||
indexByFrame.set(rowList[i], i)
|
||||
}
|
||||
},
|
||||
)
|
||||
return (frame: Frame) => {
|
||||
const index = indexByFrame.get(frame)
|
||||
return index == null ? null : index
|
||||
}
|
||||
}, [rowList])
|
||||
|
||||
const getSearchMatchForFrame: (frame: Frame) => FuzzyMatch | null = useMemo(() => {
|
||||
return (frame: Frame) => {
|
||||
if (profileSearchResults == null) return null
|
||||
return profileSearchResults.getMatchForFrame(frame)
|
||||
}
|
||||
}, [profileSearchResults])
|
||||
|
||||
const contextData: SandwichViewContextData = {
|
||||
rowList,
|
||||
selectedFrame,
|
||||
setSelectedFrame,
|
||||
getIndexForFrame,
|
||||
getSearchMatchForFrame,
|
||||
}
|
||||
|
||||
return (
|
||||
<SandwichViewContext.Provider value={contextData}>
|
||||
<SandwichView
|
||||
activeProfileState={activeProfileState}
|
||||
glCanvas={glCanvas}
|
||||
setSelectedFrame={setSelectedFrame}
|
||||
selectedFrame={selectedFrame}
|
||||
profileIndex={index}
|
||||
/>
|
||||
</SandwichViewContext.Provider>
|
||||
)
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user