Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bea0ef6913 | ||
|
|
4feb1e564d | ||
|
|
88f4fe6105 | ||
|
|
60f1812e51 | ||
|
|
1717fecafb | ||
|
|
c296f530c7 | ||
|
|
8e0fa58d65 | ||
|
|
b21480494e | ||
|
|
dfd3a0dfb3 | ||
|
|
ac4a015559 | ||
|
|
de17f128d0 | ||
|
|
304ccf33ab | ||
|
|
650e505b55 |
@@ -9,6 +9,7 @@ module.exports = {
|
||||
plugins: ['prettier', '@typescript-eslint', 'react-hooks'],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'prettier/prettier': 'error',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': [
|
||||
'error',
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
node-version: [18.x, 20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
## [1.18.0] - 2023-12-26
|
||||
|
||||
- Add hermes-specific support for the trace event format [[#458](https://github.com/jlfwong/speedscope/pull/458)] (by @zacharyfmarion)
|
||||
- Update README-ADMINS.md with npm login instructions [[#457](https://github.com/jlfwong/speedscope/pull/457)] (by @jlfwong)
|
||||
|
||||
## [1.17.0] - 2023-12-25
|
||||
|
||||
- Upgrade prettier, update prettier & react-hooks eslint plugins [[#456](https://github.com/jlfwong/speedscope/pull/456)] (by @jlfwong)
|
||||
- Upgrade typescript & eslint to latest, fix resulting errors [[#455](https://github.com/jlfwong/speedscope/pull/455)] (by @jlfwong)
|
||||
- Re-enable eslint prettier rule after being accidentally disabled for 3 years [[#454](https://github.com/jlfwong/speedscope/pull/454)] (by @jlfwong)
|
||||
- Support the chrome JSON trace format (allows viewing of hermes traces) [[#453](https://github.com/jlfwong/speedscope/pull/453)] (by @zacharyfmarion)
|
||||
- Fix bug in selectQueueToTakeFromNext for trace profiles [[#450](https://github.com/jlfwong/speedscope/pull/450)] (by @zacharyfmarion)
|
||||
- Add bounds checking for sampleTypeIndex [[#449](https://github.com/jlfwong/speedscope/pull/449)] (by @jlfwong)
|
||||
- Update README-zh_CN.md [[#442](https://github.com/jlfwong/speedscope/pull/442)] (by @byronhe)
|
||||
- Update publish-and-deploy to remove automated release creation [[#440](https://github.com/jlfwong/speedscope/pull/440)] (by @jlfwong)
|
||||
|
||||
## [1.16.0] - 2023-07-16
|
||||
|
||||
- Automate more of the release process [[#439](https://github.com/jlfwong/speedscope/pull/439)] (by @jlfwong)
|
||||
- Improve profile builder performance [[#437](https://github.com/jlfwong/speedscope/pull/437)] (by @Goose97)
|
||||
- Fix crash when importing big linux perf tool files [[#435](https://github.com/jlfwong/speedscope/pull/435)] (by @Goose97)
|
||||
- Improve splitLines: return iterator instead [[#434](https://github.com/jlfwong/speedscope/pull/434)] (by @Goose97)
|
||||
- Fix trimTextMid [[#431](https://github.com/jlfwong/speedscope/pull/431)] (by @jlfwong)
|
||||
- Added support for Papyrus profiles [[#428](https://github.com/jlfwong/speedscope/pull/428)] (by @xieve)
|
||||
|
||||
## [1.15.2] - 2023-06-21
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -34,6 +34,8 @@ Ensure you have the Github CLI tools installed and you're authenticated. Try run
|
||||
gh auth status
|
||||
npm whoami
|
||||
|
||||
In your default browser, ensure that you're logged into your npm account, otherwise you'll see a 404 page when you open the authenticate link during the npm publish.
|
||||
|
||||
Once ready to publish, run:
|
||||
|
||||
scripts/publish-and-deploy.sh
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@
|
||||
|
||||
# 使用
|
||||
|
||||
访问https://www.speedscope.app,上传文件或者拖拽到页面上。配置文件不会上传到任何地方——应用程序完全在浏览器中。
|
||||
访问 <https://www.speedscope.app> ,上传文件或者拖拽到页面上。配置文件不会上传到任何地方——应用程序完全在浏览器中。
|
||||
|
||||
## 命令行中使用
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
## 独立使用
|
||||
|
||||
如果你不想使用npm或者node下载,你也可以在这里下载独立的版本https://github.com/jlfwong/speedscope/releases.
|
||||
如果你不想使用npm或者node下载,你也可以在这里下载独立的版本 <https://github.com/jlfwong/speedscope/releases>.
|
||||
下载完一个版本的压缩文件之后,解压并在谷歌或者火狐浏览器中打开`index.html`文件即可。
|
||||
|
||||
## 支持的文件格式
|
||||
|
||||
Generated
+3114
-1735
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "speedscope",
|
||||
"version": "1.15.2",
|
||||
"version": "1.18.0",
|
||||
"description": "",
|
||||
"repository": "jlfwong/speedscope",
|
||||
"main": "index.js",
|
||||
@@ -34,24 +34,24 @@
|
||||
"@types/jszip": "3.1.4",
|
||||
"@types/node": "14.0.1",
|
||||
"@types/pako": "1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.19.0",
|
||||
"@typescript-eslint/parser": "4.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.16.0",
|
||||
"@typescript-eslint/parser": "6.16.0",
|
||||
"acorn": "7.2.0",
|
||||
"aphrodite": "2.1.0",
|
||||
"eslint": "6.0.0",
|
||||
"eslint-plugin-prettier": "2.6.0",
|
||||
"eslint-plugin-react-hooks": "4.0.2",
|
||||
"eslint": "8.0.0",
|
||||
"eslint-plugin-prettier": "5.1.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"jest": "24.3.0",
|
||||
"jsverify": "0.8.3",
|
||||
"jszip": "3.1.5",
|
||||
"pako": "1.0.6",
|
||||
"parcel-bundler": "1.12.4",
|
||||
"preact": "10.4.1",
|
||||
"prettier": "2.0.4",
|
||||
"prettier": "3.1.1",
|
||||
"protobufjs": "6.8.8",
|
||||
"source-map": "0.6.1",
|
||||
"ts-jest": "24.3.0",
|
||||
"typescript": "4.2.3",
|
||||
"typescript": "5.3.3",
|
||||
"typescript-json-schema": "0.42.0",
|
||||
"uglify-es": "3.2.2",
|
||||
"uint8array-json-parser": "0.0.2"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"version":"0.0.1","$schema":"https://www.speedscope.app/file-format-schema.json","shared":{"frames":[{"name":"a"},{"name":"b"},{"name":"c"},{"name":"d"}]},"profiles":[{"type":"evented","name":"simple.txt","unit":"none","startValue":0,"endValue":14,"events":[{"type":"O","frame":0,"at":0},{"type":"O","frame":1,"at":0},{"type":"O","frame":2,"at":0},{"type":"C","frame":2,"at":2},{"type":"O","frame":3,"at":2},{"type":"C","frame":3,"at":6},{"type":"O","frame":2,"at":6},{"type":"C","frame":2,"at":9},{"type":"C","frame":1,"at":14},{"type":"C","frame":0,"at":14}]}]}
|
||||
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"traceEvents": [
|
||||
{
|
||||
"name": "process_name",
|
||||
"ph": "M",
|
||||
"cat": "__metadata",
|
||||
"pid": 7512,
|
||||
"ts": "11550183666",
|
||||
"tid": "-1",
|
||||
"args": {
|
||||
"name": "hermes"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "thread_name",
|
||||
"ph": "M",
|
||||
"cat": "__metadata",
|
||||
"pid": 7512,
|
||||
"ts": "11550183666",
|
||||
"tid": "1",
|
||||
"args": {
|
||||
"name": "mqt_js"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mqt_js",
|
||||
"cat": "mqt_js",
|
||||
"ph": "X",
|
||||
"dur": 0,
|
||||
"pid": 7512,
|
||||
"ts": "11550183666",
|
||||
"tid": "1",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"name": "mqt_js",
|
||||
"cat": "mqt_js",
|
||||
"ph": "X",
|
||||
"dur": 0,
|
||||
"pid": 7512,
|
||||
"ts": "11550183666",
|
||||
"tid": "2",
|
||||
"args": {}
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552125241",
|
||||
"pid": 7512,
|
||||
"tid": "1",
|
||||
"weight": "1",
|
||||
"sf": 1
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552125241",
|
||||
"pid": 7512,
|
||||
"tid": "2",
|
||||
"weight": "1",
|
||||
"sf": 4
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552146011",
|
||||
"pid": 7512,
|
||||
"tid": "1",
|
||||
"weight": "1",
|
||||
"sf": 2
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552146011",
|
||||
"pid": 7512,
|
||||
"tid": "2",
|
||||
"weight": "1",
|
||||
"sf": 5
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552159337",
|
||||
"pid": 7512,
|
||||
"tid": "1",
|
||||
"weight": "1",
|
||||
"sf": 3
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552159337",
|
||||
"pid": 7512,
|
||||
"tid": "2",
|
||||
"weight": "1",
|
||||
"sf": 6
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552169337",
|
||||
"pid": 7512,
|
||||
"tid": "2",
|
||||
"weight": "1",
|
||||
"sf": 4
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552179337",
|
||||
"pid": 7512,
|
||||
"tid": "2",
|
||||
"weight": "1",
|
||||
"sf": 4
|
||||
}
|
||||
],
|
||||
"stackFrames": {
|
||||
"1": {
|
||||
"name": "[root]",
|
||||
"category": "root"
|
||||
},
|
||||
"2": {
|
||||
"name": "function1",
|
||||
"category": "JavaScript",
|
||||
"parent": 1
|
||||
},
|
||||
"3": {
|
||||
"name": "[GC Young Gen]",
|
||||
"category": "Metadata",
|
||||
"parent": 1
|
||||
},
|
||||
"4": {
|
||||
"name": "[root thread 2]",
|
||||
"category": "root"
|
||||
},
|
||||
"5": {
|
||||
"name": "function3",
|
||||
"category": "JavaScript",
|
||||
"parent": 4
|
||||
},
|
||||
"6": {
|
||||
"name": "function4",
|
||||
"category": "Metadata",
|
||||
"parent": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
[
|
||||
{
|
||||
"pid": 0,
|
||||
"tid": 0,
|
||||
"ph": "B",
|
||||
"name": "[root]",
|
||||
"ts": 0,
|
||||
"args": {
|
||||
"name": "[root]",
|
||||
"category": "root",
|
||||
"url": null,
|
||||
"line": null,
|
||||
"column": null,
|
||||
"params": null,
|
||||
"allocatedCategory": "root",
|
||||
"allocatedName": "[root]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pid": 0,
|
||||
"tid": 0,
|
||||
"ph": "B",
|
||||
"name": "beta",
|
||||
"ts": 1,
|
||||
"args": {
|
||||
"line": 54,
|
||||
"column": 12,
|
||||
"funcLine": "1",
|
||||
"funcColumn": "1",
|
||||
"name": "beta",
|
||||
"category": "JavaScript",
|
||||
"parent": 1,
|
||||
"url": "/Users/example/test_project/node_modules/metro-runtime/src/polyfills/require.js",
|
||||
"params": null,
|
||||
"allocatedCategory": "JavaScript",
|
||||
"allocatedName": "beta"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pid": 0,
|
||||
"tid": 0,
|
||||
"ph": "E",
|
||||
"name": "beta",
|
||||
"ts": 13,
|
||||
"args": {
|
||||
"line": 54,
|
||||
"column": 12,
|
||||
"funcLine": "1",
|
||||
"funcColumn": "1",
|
||||
"name": "beta",
|
||||
"category": "JavaScript",
|
||||
"parent": 1,
|
||||
"url": "/Users/example/test_project/node_modules/metro-runtime/src/polyfills/require.js",
|
||||
"params": null,
|
||||
"allocatedCategory": "JavaScript",
|
||||
"allocatedName": "beta"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pid": 0,
|
||||
"tid": 0,
|
||||
"ph": "E",
|
||||
"name": "[root]",
|
||||
"ts": 14,
|
||||
"args": {
|
||||
"name": "[root]",
|
||||
"category": "root",
|
||||
"url": null,
|
||||
"line": null,
|
||||
"column": null,
|
||||
"params": null,
|
||||
"allocatedCategory": "root",
|
||||
"allocatedName": "[root]"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"traceEvents": [
|
||||
{
|
||||
"name": "process_name",
|
||||
"ph": "M",
|
||||
"cat": "__metadata",
|
||||
"pid": 7512,
|
||||
"ts": "11550183666",
|
||||
"tid": "-1",
|
||||
"args": {
|
||||
"name": "hermes"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "thread_name",
|
||||
"ph": "M",
|
||||
"cat": "__metadata",
|
||||
"pid": 7512,
|
||||
"ts": "11550183666",
|
||||
"tid": "7695",
|
||||
"args": {
|
||||
"name": "mqt_js"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mqt_js",
|
||||
"cat": "mqt_js",
|
||||
"ph": "X",
|
||||
"dur": 0,
|
||||
"pid": 7512,
|
||||
"ts": "11550183666",
|
||||
"tid": "7695",
|
||||
"args": {}
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552125241",
|
||||
"pid": 7512,
|
||||
"tid": "7695",
|
||||
"weight": "1",
|
||||
"sf": 1
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552129681",
|
||||
"pid": 7512,
|
||||
"tid": "7695",
|
||||
"weight": "1",
|
||||
"sf": 1
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552146011",
|
||||
"pid": 7512,
|
||||
"tid": "7695",
|
||||
"weight": "1",
|
||||
"sf": 2
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552159337",
|
||||
"pid": 7512,
|
||||
"tid": "7695",
|
||||
"weight": "1",
|
||||
"sf": 4
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552169942",
|
||||
"pid": 7512,
|
||||
"tid": "7695",
|
||||
"weight": "1",
|
||||
"sf": 1
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552179951",
|
||||
"pid": 7512,
|
||||
"tid": "7695",
|
||||
"weight": "1",
|
||||
"sf": 5
|
||||
},
|
||||
{
|
||||
"cpu": "-1",
|
||||
"name": "",
|
||||
"ts": "11552189951",
|
||||
"pid": 7512,
|
||||
"tid": "7695",
|
||||
"weight": "1",
|
||||
"sf": 5
|
||||
}
|
||||
],
|
||||
"stackFrames": {
|
||||
"1": {
|
||||
"name": "[root]",
|
||||
"category": "root"
|
||||
},
|
||||
"2": {
|
||||
"name": "function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1)",
|
||||
"category": "JavaScript",
|
||||
"parent": 1
|
||||
},
|
||||
"3": {
|
||||
"name": "function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4)",
|
||||
"category": "JavaScript",
|
||||
"parent": 1
|
||||
},
|
||||
"4": {
|
||||
"name": "function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3)",
|
||||
"category": "JavaScript",
|
||||
"parent": 3
|
||||
},
|
||||
"5": {
|
||||
"name": "[GC Young Gen]",
|
||||
"category": "Metadata",
|
||||
"parent": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "anonymous", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "anonymous", "ts": 1, "args": { "parent": 1 }},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "function1", "ts": 1, "args": { "parent": 2 }},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "anonymous", "ts": 1, "args": { "parent": 3 }},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "anonymous", "ts": 3, "args": { "parent": 3 }},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "function1", "ts": 3, "args": { "parent": 2 }},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "anonymous", "ts": 3, "args": { "parent": 1 }},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "anonymous", "ts": 3, "args": { "parent": 1 }},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "anonymous", "ts": 3, "args": { "parent": 8 }},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "anonymous", "ts": 5, "args": { "parent": 8 }},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "anonymous", "ts": 5, "args": { "parent": 1 }},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "anonymous", "ts": 5}
|
||||
]
|
||||
@@ -43,9 +43,32 @@ git push --tags
|
||||
# Publish to npm
|
||||
npm publish
|
||||
|
||||
# Deploy the website
|
||||
npm run deploy
|
||||
|
||||
# Create a new release on Github
|
||||
"$script_dir/prepare-zip-file.sh"
|
||||
gh release create "$tagname" --title "$tagname" --notes "$changelog_update" --attach "dist/release/speedscope-$version.zip"
|
||||
|
||||
# Deploy the website
|
||||
npm run deploy
|
||||
# Don't double echo the below commands
|
||||
set +x
|
||||
|
||||
echo
|
||||
echo
|
||||
echo "Visit https://github.com/jlfwong/speedscope/releases/new to create a new release."
|
||||
echo
|
||||
echo "tag: $tagname"
|
||||
echo "title: $tagname"
|
||||
echo "attachment: dist/release/speedscope-$version.zip"
|
||||
echo "$changelog_update"
|
||||
|
||||
# NOTE: This part is almost-but-not-quite-automatable using a command like this:
|
||||
#
|
||||
# gh release create "$tagname" --title "$tagname" --notes "$changelog_update"
|
||||
# --attach "dist/release/speedscope-$version.zip"
|
||||
#
|
||||
# There are two problems.
|
||||
#
|
||||
# 1. The "--attach" flag doesn't exit
|
||||
# 2. I don't want the changelog notes to include the version and date like they do in CHANGELOG.md
|
||||
#
|
||||
# If 1. was solveable, then 2. would be easy to work-around.
|
||||
@@ -102,7 +102,10 @@ export class FlamechartColorPassRenderer {
|
||||
private material: Graphics.Material
|
||||
private buffer: Graphics.VertexBuffer
|
||||
|
||||
constructor(private gl: Graphics.Context, theme: Theme) {
|
||||
constructor(
|
||||
private gl: Graphics.Context,
|
||||
theme: Theme,
|
||||
) {
|
||||
const vertices = [
|
||||
{pos: [-1, 1], uv: [0, 1]},
|
||||
{pos: [1, 1], uv: [1, 1]},
|
||||
|
||||
@@ -70,7 +70,10 @@ export class ViewportRectangleRenderer {
|
||||
private material: Graphics.Material
|
||||
private buffer: Graphics.VertexBuffer
|
||||
|
||||
constructor(private gl: Graphics.Context, theme: Theme) {
|
||||
constructor(
|
||||
private gl: Graphics.Context,
|
||||
theme: Theme,
|
||||
) {
|
||||
const vertices = [
|
||||
[-1, 1],
|
||||
[1, 1],
|
||||
|
||||
@@ -172,6 +172,68 @@ exports[`importTraceEvents bad E events: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents bad E events: profileGroup.name 1`] = `"too-many-end-events.json"`;
|
||||
|
||||
exports[`importTraceEvents different number of start and end calls to same function at same timestamp 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "anonymous",
|
||||
"line": undefined,
|
||||
"name": "anonymous",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 5,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "anonymous {\\"parent\\":1}",
|
||||
"line": undefined,
|
||||
"name": "anonymous {\\"parent\\":1}",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 4,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "function1 {\\"parent\\":2}",
|
||||
"line": undefined,
|
||||
"name": "function1 {\\"parent\\":2}",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "anonymous {\\"parent\\":3}",
|
||||
"line": undefined,
|
||||
"name": "anonymous {\\"parent\\":3}",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "anonymous {\\"parent\\":8}",
|
||||
"line": undefined,
|
||||
"name": "anonymous {\\"parent\\":8}",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"anonymous 1.00µs",
|
||||
"anonymous;anonymous {\\"parent\\":1};function1 {\\"parent\\":2};anonymous {\\"parent\\":3} 2.00µs",
|
||||
"anonymous;anonymous {\\"parent\\":1};anonymous {\\"parent\\":8} 2.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents different number of start and end calls to same function at same timestamp: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents different number of start and end calls to same function at same timestamp: profileGroup.name 1`] = `"simultaneous-anonymous-calls.json"`;
|
||||
|
||||
exports[`importTraceEvents end event with empty stack 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
@@ -468,6 +530,81 @@ exports[`importTraceEvents mismatched name: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents mismatched name: profileGroup.name 1`] = `"mismatched-name.json"`;
|
||||
|
||||
exports[`importTraceEvents multi-thread profile with samples 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "[root]:root",
|
||||
"line": undefined,
|
||||
"name": "[root]",
|
||||
"selfWeight": 20770,
|
||||
"totalWeight": 34096,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "function1:JavaScript",
|
||||
"line": undefined,
|
||||
"name": "function1",
|
||||
"selfWeight": 13326,
|
||||
"totalWeight": 13326,
|
||||
},
|
||||
],
|
||||
"name": "hermes (pid 7512), mqt_js (tid 1)",
|
||||
"stacks": Array [
|
||||
"[root] 20.77ms",
|
||||
"[root];function1 13.33ms",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents multi-thread profile with samples 2`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "[root thread 2]:root",
|
||||
"line": undefined,
|
||||
"name": "[root thread 2]",
|
||||
"selfWeight": 30770,
|
||||
"totalWeight": 54096,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "function3:JavaScript",
|
||||
"line": undefined,
|
||||
"name": "function3",
|
||||
"selfWeight": 13326,
|
||||
"totalWeight": 13326,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "function4:Metadata",
|
||||
"line": undefined,
|
||||
"name": "function4",
|
||||
"selfWeight": 10000,
|
||||
"totalWeight": 10000,
|
||||
},
|
||||
],
|
||||
"name": "hermes (pid 7512, tid 2)",
|
||||
"stacks": Array [
|
||||
"[root thread 2] 20.77ms",
|
||||
"[root thread 2];function3 13.33ms",
|
||||
"[root thread 2];function4 10.00ms",
|
||||
"[root thread 2] 10.00ms",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents multi-thread profile with samples: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents multi-thread profile with samples: profileGroup.name 1`] = `"multi-thread-with-samples.json"`;
|
||||
|
||||
exports[`importTraceEvents multiprocess 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
@@ -1178,6 +1315,41 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents simple hermes profile 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": null,
|
||||
"file": null,
|
||||
"key": "[root] {\\"name\\":\\"[root]\\",\\"category\\":\\"root\\",\\"url\\":null,\\"line\\":null,\\"column\\":null,\\"params\\":null,\\"allocatedCategory\\":\\"root\\",\\"allocatedName\\":\\"[root]\\"}",
|
||||
"line": null,
|
||||
"name": "[root]",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 14,
|
||||
},
|
||||
Frame {
|
||||
"col": 12,
|
||||
"file": "/Users/example/test_project/node_modules/metro-runtime/src/polyfills/require.js",
|
||||
"key": "beta {\\"line\\":54,\\"column\\":12,\\"funcLine\\":\\"1\\",\\"funcColumn\\":\\"1\\",\\"name\\":\\"beta\\",\\"category\\":\\"JavaScript\\",\\"parent\\":1,\\"url\\":\\"/Users/example/test_project/node_modules/metro-runtime/src/polyfills/require.js\\",\\"params\\":null,\\"allocatedCategory\\":\\"JavaScript\\",\\"allocatedName\\":\\"beta\\"}",
|
||||
"line": 54,
|
||||
"name": "beta",
|
||||
"selfWeight": 12,
|
||||
"totalWeight": 12,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"[root] 1.00µs",
|
||||
"[root];beta 12.00µs",
|
||||
"[root] 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents simple hermes profile: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents simple hermes profile: profileGroup.name 1`] = `"simple-hermes.json"`;
|
||||
|
||||
exports[`importTraceEvents simple object 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
@@ -1224,6 +1396,70 @@ exports[`importTraceEvents simple object: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents simple object: profileGroup.name 1`] = `"simple-object.json"`;
|
||||
|
||||
exports[`importTraceEvents simple profile with samples 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "[root]:root",
|
||||
"line": undefined,
|
||||
"name": "[root]",
|
||||
"selfWeight": 30779,
|
||||
"totalWeight": 64710,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1):JavaScript",
|
||||
"line": undefined,
|
||||
"name": "function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1)",
|
||||
"selfWeight": 13326,
|
||||
"totalWeight": 13326,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4):JavaScript",
|
||||
"line": undefined,
|
||||
"name": "function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4)",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 10605,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3):JavaScript",
|
||||
"line": undefined,
|
||||
"name": "function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3)",
|
||||
"selfWeight": 10605,
|
||||
"totalWeight": 10605,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "[GC Young Gen]:Metadata",
|
||||
"line": undefined,
|
||||
"name": "[GC Young Gen]",
|
||||
"selfWeight": 10000,
|
||||
"totalWeight": 10000,
|
||||
},
|
||||
],
|
||||
"name": "hermes (pid 7512), mqt_js (tid 7695)",
|
||||
"stacks": Array [
|
||||
"[root] 20.77ms",
|
||||
"[root];function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1) 13.33ms",
|
||||
"[root];function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4);function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3) 10.61ms",
|
||||
"[root] 10.01ms",
|
||||
"[root];[GC Young Gen] 10.00ms",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents simple profile with samples: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents simple profile with samples: profileGroup.name 1`] = `"simple-with-samples.json"`;
|
||||
|
||||
exports[`importTraceEvents simple: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents simple: profileGroup.name 1`] = `"simple.json"`;
|
||||
|
||||
@@ -95,7 +95,10 @@ class CallGraph {
|
||||
private totalWeights = new Map<Frame, number>()
|
||||
private childrenTotalWeights = new Map<Frame, Map<Frame, number>>()
|
||||
|
||||
constructor(private fileName: string, private fieldName: string) {}
|
||||
constructor(
|
||||
private fileName: string,
|
||||
private fieldName: string,
|
||||
) {}
|
||||
|
||||
private getOrInsertFrame(info: FrameInfo): Frame {
|
||||
return Frame.getOrInsert(this.frameSet, info)
|
||||
@@ -345,7 +348,10 @@ class CallgrindParser {
|
||||
private savedFileNames: {[id: string]: string} = {}
|
||||
private savedFunctionNames: {[id: string]: string} = {}
|
||||
|
||||
constructor(contents: TextFileContent, private importedFileName: string) {
|
||||
constructor(
|
||||
contents: TextFileContent,
|
||||
private importedFileName: string,
|
||||
) {
|
||||
this.lines = [...contents.splitLines()]
|
||||
this.lineNum = 0
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// The bits of this API that we care about. This is implemented by WebKitEntry
|
||||
// https://wicg.github.io/entries-api/#api-entry
|
||||
export interface FileSystemDirectoryReader {
|
||||
readEntries(cb: (entries: FileSystemEntry[]) => void, error: (err: Error) => void): void
|
||||
}
|
||||
export interface FileSystemEntry {
|
||||
readonly isFile: boolean
|
||||
readonly isDirectory: boolean
|
||||
readonly name: string
|
||||
readonly fullPath: string
|
||||
}
|
||||
export interface FileSystemDirectoryEntry extends FileSystemEntry {
|
||||
createReader(): FileSystemDirectoryReader
|
||||
}
|
||||
export interface FileSystemFileEntry extends FileSystemEntry {
|
||||
file(cb: (file: File) => void, errCb: (err: Error) => void): void
|
||||
}
|
||||
+3
-4
@@ -1,5 +1,4 @@
|
||||
import {Profile, ProfileGroup} from '../lib/profile'
|
||||
import {FileSystemDirectoryEntry} from './file-system-entry'
|
||||
|
||||
import {
|
||||
importFromChromeCPUProfile,
|
||||
@@ -23,7 +22,7 @@ import {decodeBase64} from '../lib/utils'
|
||||
import {importFromChromeHeapProfile} from './v8heapalloc'
|
||||
import {isTraceEventFormatted, importTraceEvents} from './trace-event'
|
||||
import {importFromCallgrind} from './callgrind'
|
||||
import {importFromPapyrus} from "./papyrus";
|
||||
import {importFromPapyrus} from './papyrus'
|
||||
|
||||
export async function importProfileGroupFromText(
|
||||
fileName: string,
|
||||
@@ -189,8 +188,8 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
|
||||
return toGroup(importFromInstrumentsDeepCopy(contents))
|
||||
}
|
||||
|
||||
if (/^(Stack_|Script_|Obj_)\S+ log opened \(PC\)\n/.exec(contents.firstChunk())){
|
||||
console.log("Importing as Papyrus profile")
|
||||
if (/^(Stack_|Script_|Obj_)\S+ log opened \(PC\)\n/.exec(contents.firstChunk())) {
|
||||
console.log('Importing as Papyrus profile')
|
||||
return toGroup(importFromPapyrus(contents))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as path from 'path'
|
||||
import {dumpProfile, checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
import * as JSZip from 'jszip'
|
||||
import {FileSystemEntry} from './file-system-entry'
|
||||
import {importFromFileSystemDirectoryEntry} from '.'
|
||||
|
||||
describe('importFromInstrumentsDeepCopy', () => {
|
||||
@@ -24,7 +23,10 @@ describe('importFromInstrumentsDeepCopy', () => {
|
||||
})
|
||||
})
|
||||
|
||||
class ZipBackedFileSystemEntry implements FileSystemEntry {
|
||||
// This is a bit of a weird type signature. I'm making this almost a
|
||||
// FileSystemEntry, but ignoring the parts of the API I don't use anywhere and
|
||||
// are a pain to implement.
|
||||
class ZipBackedFileSystemEntry implements Omit<FileSystemEntry, 'filesystem' | 'getParent'> {
|
||||
readonly isFile: boolean
|
||||
readonly isDirectory: boolean
|
||||
readonly name: string
|
||||
@@ -33,7 +35,10 @@ class ZipBackedFileSystemEntry implements FileSystemEntry {
|
||||
private zipDir: any | null
|
||||
private zipFile: JSZip.JSZipObject | null
|
||||
|
||||
constructor(private zip: JSZip, fullPath: string) {
|
||||
constructor(
|
||||
private zip: JSZip,
|
||||
fullPath: string,
|
||||
) {
|
||||
this.fullPath = fullPath
|
||||
|
||||
this.zipFile = zip.file(fullPath)
|
||||
@@ -73,7 +78,7 @@ class ZipBackedFileSystemEntry implements FileSystemEntry {
|
||||
const ret: FileSystemEntry[] = []
|
||||
this.zipDir.forEach((relativePath: string, file: {name: string}) => {
|
||||
if (relativePath.split('/').length === (relativePath.endsWith('/') ? 2 : 1)) {
|
||||
ret.push(new ZipBackedFileSystemEntry(this.zip, file.name))
|
||||
ret.push(new ZipBackedFileSystemEntry(this.zip, file.name) as any as FileSystemEntry)
|
||||
}
|
||||
})
|
||||
cb(ret)
|
||||
@@ -90,7 +95,10 @@ describe('importFromInstrumentsTrace', () => {
|
||||
JSZip.loadAsync(data).then(resolve)
|
||||
})
|
||||
})
|
||||
const root = new ZipBackedFileSystemEntry(zip, 'simple-time-profile.trace')
|
||||
const root: FileSystemDirectoryEntry = new ZipBackedFileSystemEntry(
|
||||
zip,
|
||||
'simple-time-profile.trace',
|
||||
) as any
|
||||
const profileGroup = await importFromFileSystemDirectoryEntry(root)
|
||||
const profile = profileGroup.profiles[profileGroup.indexToView]
|
||||
expect(dumpProfile(profile)).toMatchSnapshot()
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from '../lib/profile'
|
||||
import {sortBy, getOrThrow, getOrInsert, lastOf, getOrElse, zeroPad} from '../lib/utils'
|
||||
import {ByteFormatter, TimeFormatter} from '../lib/value-formatters'
|
||||
import {FileSystemDirectoryEntry, FileSystemEntry, FileSystemFileEntry} from './file-system-entry'
|
||||
import {MaybeCompressedDataReader, TextFileContent} from './utils'
|
||||
|
||||
function parseTSV<T>(contents: TextFileContent): T[] {
|
||||
|
||||
+11
-1
@@ -129,6 +129,11 @@ export function importAsPprofProfile(rawProfile: ArrayBuffer): Profile | null {
|
||||
}))
|
||||
|
||||
const sampleTypeIndex = getSampleTypeIndex(protoProfile)
|
||||
|
||||
if (sampleTypeIndex < 0 || sampleTypeIndex >= sampleTypes.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const sampleType = sampleTypes[sampleTypeIndex]
|
||||
|
||||
const profileBuilder = new StackListProfileBuilder()
|
||||
@@ -149,7 +154,12 @@ export function importAsPprofProfile(rawProfile: ArrayBuffer): Profile | null {
|
||||
for (let s of protoProfile.sample) {
|
||||
const stack = s.locationId ? s.locationId.map(l => frameByLocationID.get(i32(l))) : []
|
||||
stack.reverse()
|
||||
const value = s.value![sampleTypeIndex]
|
||||
|
||||
if (s.value == null || s.value.length <= sampleTypeIndex) {
|
||||
return null
|
||||
}
|
||||
|
||||
const value = s.value[sampleTypeIndex]
|
||||
profileBuilder.appendSampleWithWeight(stack.filter(f => f != null) as FrameInfo[], +value)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { importProfileGroupFromText } from '.'
|
||||
import {readFileSync} from 'fs'
|
||||
import {importProfileGroupFromText} from '.'
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
test('importFromStackprof', async () => {
|
||||
@@ -20,9 +20,11 @@ describe('importCpuProfileWithProperWeights', () => {
|
||||
})
|
||||
|
||||
test('uses samples count for weight when importing cpu profile', async () => {
|
||||
|
||||
const profileFile = readFileSync('./sample/profiles/stackprof/simple-cpu-stackprof.json')
|
||||
const profileGroup = await importProfileGroupFromText('simple-cpu-stackprof.json', profileFile.toString())
|
||||
const profileGroup = await importProfileGroupFromText(
|
||||
'simple-cpu-stackprof.json',
|
||||
profileFile.toString(),
|
||||
)
|
||||
expect(profileGroup).not.toBeNull()
|
||||
|
||||
if (profileGroup) {
|
||||
|
||||
@@ -33,9 +33,9 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile
|
||||
let stack: FrameInfo[] = []
|
||||
for (let j = 0; j < stackHeight; j++) {
|
||||
const id = raw[i++]
|
||||
let frameName = frames[id].name;
|
||||
let frameName = frames[id].name
|
||||
if (frameName == null) {
|
||||
frameName = '(unknown)';
|
||||
frameName = '(unknown)'
|
||||
}
|
||||
const frame = {
|
||||
key: id,
|
||||
|
||||
@@ -47,6 +47,10 @@ test('importTraceEvents bad E events', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/too-many-end-events.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents different number of start and end calls to same function at same timestamp', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/simultaneous-anonymous-calls.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents event re-ordering', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/must-retain-original-order.json')
|
||||
})
|
||||
@@ -118,3 +122,15 @@ test('importTraceEvents invalid x nesting', async () => {
|
||||
test('importTraceEvents event reordering name match', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/event-reordering-name-match.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents simple hermes profile', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/simple-hermes.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents simple profile with samples', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/simple-with-samples.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents multi-thread profile with samples', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/multi-thread-with-samples.json')
|
||||
})
|
||||
|
||||
+419
-144
@@ -1,5 +1,11 @@
|
||||
import {sortBy, zeroPad, getOrInsert, lastOf} from '../lib/utils'
|
||||
import {ProfileGroup, CallTreeProfileBuilder, FrameInfo, Profile} from '../lib/profile'
|
||||
import {
|
||||
ProfileGroup,
|
||||
CallTreeProfileBuilder,
|
||||
FrameInfo,
|
||||
Profile,
|
||||
StackListProfileBuilder,
|
||||
} from '../lib/profile'
|
||||
import {TimeFormatter} from '../lib/value-formatters'
|
||||
|
||||
// This file concerns import from the "Trace Event Format", authored by Google
|
||||
@@ -42,12 +48,46 @@ interface TraceEvent {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
enum ExporterSource {
|
||||
HERMES = 'HERMES',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
}
|
||||
|
||||
interface HermesTraceEventArgs {
|
||||
line: number | null
|
||||
column: number | null
|
||||
funcLine?: string | null
|
||||
funcColumn?: string | null
|
||||
name: string
|
||||
category: string
|
||||
parent?: number
|
||||
url: string | null
|
||||
params: string | null
|
||||
allocatedCategory: string
|
||||
allocatedName: string
|
||||
}
|
||||
|
||||
const requiredHermesArguments: Array<keyof HermesTraceEventArgs> = [
|
||||
'line',
|
||||
'column',
|
||||
'name',
|
||||
'category',
|
||||
'url',
|
||||
'params',
|
||||
'allocatedCategory',
|
||||
'allocatedName',
|
||||
]
|
||||
|
||||
type HermesTraceEvent = TraceEvent & {
|
||||
args: HermesTraceEventArgs
|
||||
}
|
||||
|
||||
interface BTraceEvent extends TraceEvent {
|
||||
ph: 'B'
|
||||
}
|
||||
@@ -65,16 +105,53 @@ interface XTraceEvent extends TraceEvent {
|
||||
// The trace format supports a number of event types that we ignore.
|
||||
type ImportableTraceEvent = BTraceEvent | ETraceEvent | XTraceEvent
|
||||
|
||||
interface StackFrame {
|
||||
line: string
|
||||
column: string
|
||||
funcLine: string
|
||||
funcColumn: string
|
||||
name: string
|
||||
category: string
|
||||
// A parent function may or may not exist
|
||||
parent?: number
|
||||
}
|
||||
|
||||
interface Sample {
|
||||
cpu: string
|
||||
name: string
|
||||
ts: string
|
||||
pid: number
|
||||
tid: string
|
||||
weight: string
|
||||
// Will refer to an element in the stackFrames object
|
||||
sf: number
|
||||
stackFrameData?: StackFrame
|
||||
}
|
||||
|
||||
interface TraceWithSamples {
|
||||
traceEvents: TraceEvent[]
|
||||
samples: Sample[]
|
||||
stackFrames: {[key: string]: StackFrame}
|
||||
}
|
||||
|
||||
interface TraceEventObject {
|
||||
traceEvents: TraceEvent[]
|
||||
}
|
||||
|
||||
type Trace = TraceEvent[] | TraceEventObject | TraceWithSamples
|
||||
|
||||
function pidTidKey(pid: number, tid: number): string {
|
||||
// We zero-pad the PID and TID to make sorting them by pid/tid pair later easier.
|
||||
return `${zeroPad('' + pid, 10)}:${zeroPad('' + tid, 10)}`
|
||||
}
|
||||
|
||||
function partitionByPidTid(events: ImportableTraceEvent[]): Map<string, ImportableTraceEvent[]> {
|
||||
const map = new Map<string, ImportableTraceEvent[]>()
|
||||
function partitionByPidTid<T extends {tid: number | string; pid: number | string}>(
|
||||
events: T[],
|
||||
): Map<string, T[]> {
|
||||
const map = new Map<string, T[]>()
|
||||
|
||||
for (let ev of events) {
|
||||
const list = getOrInsert(map, pidTidKey(ev.pid, ev.tid), () => [])
|
||||
const list = getOrInsert(map, pidTidKey(Number(ev.pid), Number(ev.tid)), () => [])
|
||||
list.push(ev)
|
||||
}
|
||||
|
||||
@@ -103,12 +180,12 @@ function selectQueueToTakeFromNext(
|
||||
// If we got here, the 'B' event queue and the 'E' event queue have events at
|
||||
// the front with equal timestamps.
|
||||
|
||||
// If the front of the 'E' queue matches the front of the 'B' queue by name,
|
||||
// If the front of the 'E' queue matches the front of the 'B' queue by key,
|
||||
// then it means we have a zero duration event. Process the 'B' queue first
|
||||
// to ensure it opens before we try to close it.
|
||||
//
|
||||
// Otherwise, process the 'E' queue first.
|
||||
return bFront.name === eFront.name ? 'B' : 'E'
|
||||
return keyForEvent(bFront) === keyForEvent(eFront) ? 'B' : 'E'
|
||||
}
|
||||
|
||||
function convertToEventQueues(events: ImportableTraceEvent[]): [BTraceEvent[], ETraceEvent[]] {
|
||||
@@ -228,126 +305,187 @@ function getThreadNamesByPidTid(events: TraceEvent[]): Map<string, string> {
|
||||
return threadNameByPidTid
|
||||
}
|
||||
|
||||
function keyForEvent(event: TraceEvent): string {
|
||||
let name = `${event.name || '(unnamed)'}`
|
||||
if (event.args) {
|
||||
name += ` ${JSON.stringify(event.args)}`
|
||||
}
|
||||
return name
|
||||
function getEventName(event: TraceEvent): string {
|
||||
return `${event.name || '(unnamed)'}`
|
||||
}
|
||||
|
||||
function frameInfoForEvent(event: TraceEvent): FrameInfo {
|
||||
function keyForEvent(event: TraceEvent): string {
|
||||
let key = getEventName(event)
|
||||
if (event.args) {
|
||||
key += ` ${JSON.stringify(event.args)}`
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
function frameInfoForEvent(
|
||||
event: TraceEvent,
|
||||
exporterSource: ExporterSource = ExporterSource.UNKNOWN,
|
||||
): FrameInfo {
|
||||
const key = keyForEvent(event)
|
||||
|
||||
// In Hermes profiles we have additional guaranteed metadata we can use to
|
||||
// more accurately populate profiles with info such as line + col number
|
||||
if (exporterSource === ExporterSource.HERMES) {
|
||||
return {
|
||||
name: getEventName(event),
|
||||
key: key,
|
||||
file: event.args.url,
|
||||
line: event.args.line,
|
||||
col: event.args.column,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: key,
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
||||
function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
|
||||
const importableEvents = filterIgnoredEventTypes(events)
|
||||
const partitioned = partitionByPidTid(importableEvents)
|
||||
|
||||
/**
|
||||
* Constructs an array mapping pid-tid keys to profile builders. Both the traceEvent[]
|
||||
* format and the sample + stack frame based object format specify the process and thread
|
||||
* names based on metadata so we share this logic.
|
||||
*
|
||||
* See https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.xqopa5m0e28f
|
||||
*/
|
||||
function getProfileNameByPidTid(
|
||||
events: TraceEvent[],
|
||||
partitionedTraceEvents: Map<string, TraceEvent[]>,
|
||||
): Map<string, string> {
|
||||
const processNamesByPid = getProcessNamesByPid(events)
|
||||
const threadNamesByPidTid = getThreadNamesByPidTid(events)
|
||||
|
||||
const profilePairs: [string, Profile][] = []
|
||||
const profileNamesByPidTid = new Map<string, string>()
|
||||
|
||||
partitioned.forEach(eventsForThread => {
|
||||
if (eventsForThread.length === 0) return
|
||||
partitionedTraceEvents.forEach(importableEvents => {
|
||||
if (importableEvents.length === 0) return
|
||||
|
||||
const {pid, tid} = eventsForThread[0]
|
||||
|
||||
const profile = new CallTreeProfileBuilder()
|
||||
profile.setValueFormatter(new TimeFormatter('microseconds'))
|
||||
const {pid, tid} = importableEvents[0]
|
||||
|
||||
const profileKey = pidTidKey(pid, tid)
|
||||
const processName = processNamesByPid.get(pid)
|
||||
const threadName = threadNamesByPidTid.get(pidTidKey(pid, tid))
|
||||
const threadName = threadNamesByPidTid.get(profileKey)
|
||||
|
||||
if (processName != null && threadName != null) {
|
||||
profile.setName(`${processName} (pid ${pid}), ${threadName} (tid ${tid})`)
|
||||
profileNamesByPidTid.set(
|
||||
profileKey,
|
||||
`${processName} (pid ${pid}), ${threadName} (tid ${tid})`,
|
||||
)
|
||||
} else if (processName != null) {
|
||||
profile.setName(`${processName} (pid ${pid}, tid ${tid})`)
|
||||
profileNamesByPidTid.set(profileKey, `${processName} (pid ${pid}, tid ${tid})`)
|
||||
} else if (threadName != null) {
|
||||
profile.setName(`${threadName} (pid ${pid}, tid ${tid})`)
|
||||
profileNamesByPidTid.set(profileKey, `${threadName} (pid ${pid}, tid ${tid})`)
|
||||
} else {
|
||||
profile.setName(`pid ${pid}, tid ${tid}`)
|
||||
profileNamesByPidTid.set(profileKey, `pid ${pid}, tid ${tid}`)
|
||||
}
|
||||
})
|
||||
|
||||
return profileNamesByPidTid
|
||||
}
|
||||
|
||||
function eventListToProfile(
|
||||
importableEvents: ImportableTraceEvent[],
|
||||
name: string,
|
||||
exporterSource: ExporterSource = ExporterSource.UNKNOWN,
|
||||
): Profile {
|
||||
// The trace event format is hard to deal with because it specifically
|
||||
// allows events to be recorded out of order, *but* event ordering is still
|
||||
// important for events with the same timestamp. Because of this, rather
|
||||
// than thinking about the entire event stream as a single queue of events,
|
||||
// we're going to first construct two time-ordered lists of events:
|
||||
//
|
||||
// 1. ts ordered queue of 'B' events
|
||||
// 2. ts ordered queue of 'E' events
|
||||
//
|
||||
// We deal with 'X' events by converting them to one entry in the 'B' event
|
||||
// queue and one entry in the 'E' event queue.
|
||||
//
|
||||
// The high level goal is to deal with 'B' events in 'ts' order, breaking
|
||||
// ties by the order the events occurred in the file, and deal with 'E'
|
||||
// events in 'ts' order, breaking ties in whatever order causes the 'E'
|
||||
// events to match whatever is on the top of the stack.
|
||||
const [bEventQueue, eEventQueue] = convertToEventQueues(importableEvents)
|
||||
|
||||
const profileBuilder = new CallTreeProfileBuilder()
|
||||
profileBuilder.setValueFormatter(new TimeFormatter('microseconds'))
|
||||
profileBuilder.setName(name)
|
||||
|
||||
const frameStack: BTraceEvent[] = []
|
||||
const enterFrame = (b: BTraceEvent) => {
|
||||
frameStack.push(b)
|
||||
profileBuilder.enterFrame(frameInfoForEvent(b, exporterSource), b.ts)
|
||||
}
|
||||
|
||||
const tryToLeaveFrame = (e: ETraceEvent) => {
|
||||
const b = lastOf(frameStack)
|
||||
|
||||
if (b == null) {
|
||||
console.warn(
|
||||
`Tried to end frame "${
|
||||
frameInfoForEvent(e, exporterSource).key
|
||||
}", but the stack was empty. Doing nothing instead.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// The trace event format is hard to deal with because it specifically
|
||||
// allows events to be recorded out of order, *but* event ordering is still
|
||||
// important for events with the same timestamp. Because of this, rather
|
||||
// than thinking about the entire event stream as a single queue of events,
|
||||
// we're going to first construct two time-ordered lists of events:
|
||||
//
|
||||
// 1. ts ordered queue of 'B' events
|
||||
// 2. ts ordered queue of 'E' events
|
||||
//
|
||||
// We deal with 'X' events by converting them to one entry in the 'B' event
|
||||
// queue and one entry in the 'E' event queue.
|
||||
//
|
||||
// The high level goal is to deal with 'B' events in 'ts' order, breaking
|
||||
// ties by the order the events occurred in the file, and deal with 'E'
|
||||
// events in 'ts' order, breaking ties in whatever order causes the 'E'
|
||||
// events to match whatever is on the top of the stack.
|
||||
const [bEventQueue, eEventQueue] = convertToEventQueues(eventsForThread)
|
||||
const eFrameInfo = frameInfoForEvent(e, exporterSource)
|
||||
const bFrameInfo = frameInfoForEvent(b, exporterSource)
|
||||
|
||||
const frameStack: BTraceEvent[] = []
|
||||
const enterFrame = (b: BTraceEvent) => {
|
||||
frameStack.push(b)
|
||||
profile.enterFrame(frameInfoForEvent(b), b.ts)
|
||||
if (e.name !== b.name) {
|
||||
console.warn(
|
||||
`ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Doing nothing instead.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const tryToLeaveFrame = (e: ETraceEvent) => {
|
||||
const b = lastOf(frameStack)
|
||||
|
||||
if (b == null) {
|
||||
console.warn(
|
||||
`Tried to end frame "${
|
||||
frameInfoForEvent(e).key
|
||||
}", but the stack was empty. Doing nothing instead.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const eFrameInfo = frameInfoForEvent(e)
|
||||
const bFrameInfo = frameInfoForEvent(b)
|
||||
|
||||
if (e.name !== b.name) {
|
||||
console.warn(
|
||||
`ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Doing nothing instead.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (eFrameInfo.key !== bFrameInfo.key) {
|
||||
console.warn(
|
||||
`ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Ending ${bFrameInfo.key} instead.`,
|
||||
)
|
||||
}
|
||||
|
||||
frameStack.pop()
|
||||
profile.leaveFrame(bFrameInfo, e.ts)
|
||||
if (eFrameInfo.key !== bFrameInfo.key) {
|
||||
console.warn(
|
||||
`ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Ending ${bFrameInfo.key} instead.`,
|
||||
)
|
||||
}
|
||||
|
||||
while (bEventQueue.length > 0 || eEventQueue.length > 0) {
|
||||
const queueName = selectQueueToTakeFromNext(bEventQueue, eEventQueue)
|
||||
switch (queueName) {
|
||||
case 'B': {
|
||||
enterFrame(bEventQueue.shift()!)
|
||||
break
|
||||
}
|
||||
case 'E': {
|
||||
// Before we take the first event in the 'E' queue, let's first see if
|
||||
// there are any e events that exactly match the top of the stack.
|
||||
// We'll prioritize first by key, then by name if we can't find a key
|
||||
// match.
|
||||
const stackTop = lastOf(frameStack)
|
||||
if (stackTop != null) {
|
||||
const bFrameInfo = frameInfoForEvent(stackTop)
|
||||
frameStack.pop()
|
||||
profileBuilder.leaveFrame(bFrameInfo, e.ts)
|
||||
}
|
||||
|
||||
let swapped: boolean = false
|
||||
while (bEventQueue.length > 0 || eEventQueue.length > 0) {
|
||||
const queueName = selectQueueToTakeFromNext(bEventQueue, eEventQueue)
|
||||
switch (queueName) {
|
||||
case 'B': {
|
||||
enterFrame(bEventQueue.shift()!)
|
||||
break
|
||||
}
|
||||
case 'E': {
|
||||
// Before we take the first event in the 'E' queue, let's first see if
|
||||
// there are any e events that exactly match the top of the stack.
|
||||
// We'll prioritize first by key, then by name if we can't find a key
|
||||
// match.
|
||||
const stackTop = lastOf(frameStack)
|
||||
if (stackTop != null) {
|
||||
const bFrameInfo = frameInfoForEvent(stackTop, exporterSource)
|
||||
|
||||
let swapped: boolean = false
|
||||
|
||||
for (let i = 1; i < eEventQueue.length; i++) {
|
||||
const eEvent = eEventQueue[i]
|
||||
if (eEvent.ts > eEventQueue[0].ts) {
|
||||
// Only consider 'E' events with the same ts as the front of the queue.
|
||||
break
|
||||
}
|
||||
|
||||
const eFrameInfo = frameInfoForEvent(eEvent, exporterSource)
|
||||
if (bFrameInfo.key === eFrameInfo.key) {
|
||||
// We have a match! Process this one first.
|
||||
const temp = eEventQueue[0]
|
||||
eEventQueue[0] = eEventQueue[i]
|
||||
eEventQueue[i] = temp
|
||||
swapped = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!swapped) {
|
||||
// There was no key match, let's see if we can find a name match
|
||||
for (let i = 1; i < eEventQueue.length; i++) {
|
||||
const eEvent = eEventQueue[i]
|
||||
if (eEvent.ts > eEventQueue[0].ts) {
|
||||
@@ -355,8 +493,7 @@ function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
|
||||
break
|
||||
}
|
||||
|
||||
const eFrameInfo = frameInfoForEvent(eEvent)
|
||||
if (bFrameInfo.key === eFrameInfo.key) {
|
||||
if (eEvent.name === stackTop.name) {
|
||||
// We have a match! Process this one first.
|
||||
const temp = eEventQueue[0]
|
||||
eEventQueue[0] = eEventQueue[i]
|
||||
@@ -365,50 +502,162 @@ function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!swapped) {
|
||||
// There was no key match, let's see if we can find a name match
|
||||
for (let i = 1; i < eEventQueue.length; i++) {
|
||||
const eEvent = eEventQueue[i]
|
||||
if (eEvent.ts > eEventQueue[0].ts) {
|
||||
// Only consider 'E' events with the same ts as the front of the queue.
|
||||
break
|
||||
}
|
||||
|
||||
if (eEvent.name === stackTop.name) {
|
||||
// We have a match! Process this one first.
|
||||
const temp = eEventQueue[0]
|
||||
eEventQueue[0] = eEventQueue[i]
|
||||
eEventQueue[i] = temp
|
||||
swapped = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If swapped is still false at this point, it means we're about to
|
||||
// pop a stack frame that doesn't even match by name. Bummer.
|
||||
}
|
||||
|
||||
const e = eEventQueue.shift()!
|
||||
|
||||
tryToLeaveFrame(e)
|
||||
break
|
||||
// If swapped is still false at this point, it means we're about to
|
||||
// pop a stack frame that doesn't even match by name. Bummer.
|
||||
}
|
||||
|
||||
default:
|
||||
const _exhaustiveCheck: never = queueName
|
||||
return _exhaustiveCheck
|
||||
const e = eEventQueue.shift()!
|
||||
|
||||
tryToLeaveFrame(e)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
const _exhaustiveCheck: never = queueName
|
||||
return _exhaustiveCheck
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = frameStack.length - 1; i >= 0; i--) {
|
||||
const frame = frameInfoForEvent(frameStack[i], exporterSource)
|
||||
console.warn(`Frame "${frame.key}" was still open at end of profile. Closing automatically.`)
|
||||
profileBuilder.leaveFrame(frame, profileBuilder.getTotalWeight())
|
||||
}
|
||||
|
||||
return profileBuilder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the time difference in microseconds between the current
|
||||
* sample and the next sample
|
||||
*/
|
||||
function getTimeDeltasForSamples(samples: Sample[]): number[] {
|
||||
const timeDeltas: number[] = []
|
||||
let lastTimeStamp = Number(samples[0].ts)
|
||||
|
||||
samples.forEach((sample: Sample, idx: number) => {
|
||||
if (idx === 0) return
|
||||
|
||||
const timeDiff = Number(sample.ts) - lastTimeStamp
|
||||
lastTimeStamp = Number(sample.ts)
|
||||
timeDeltas.push(timeDiff)
|
||||
})
|
||||
|
||||
timeDeltas.push(0)
|
||||
|
||||
return timeDeltas
|
||||
}
|
||||
|
||||
/**
|
||||
* The chrome json trace event spec only specifies name and category
|
||||
* as required stack frame properties
|
||||
*
|
||||
* https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.b4y98p32171
|
||||
*/
|
||||
function frameInfoForSampleFrame({name, category}: StackFrame): FrameInfo {
|
||||
return {
|
||||
key: `${name}:${category}`,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
function getActiveFramesForSample(
|
||||
stackFrames: {[key: string]: StackFrame},
|
||||
frameId: number,
|
||||
): FrameInfo[] {
|
||||
const frames = []
|
||||
let parent: number | undefined = frameId
|
||||
|
||||
while (parent) {
|
||||
const frame: StackFrame = stackFrames[parent]
|
||||
|
||||
if (!frame) {
|
||||
throw new Error(`Could not find frame for id ${parent}`)
|
||||
}
|
||||
|
||||
for (let i = frameStack.length - 1; i >= 0; i--) {
|
||||
const frame = frameInfoForEvent(frameStack[i])
|
||||
console.warn(`Frame "${frame.key}" was still open at end of profile. Closing automatically.`)
|
||||
profile.leaveFrame(frame, profile.getTotalWeight())
|
||||
frames.push(frameInfoForSampleFrame(frame))
|
||||
parent = frame.parent
|
||||
}
|
||||
|
||||
return frames.reverse()
|
||||
}
|
||||
|
||||
function sampleListToProfile(contents: TraceWithSamples, samples: Sample[], name: string): Profile {
|
||||
const profileBuilder = new StackListProfileBuilder()
|
||||
|
||||
profileBuilder.setValueFormatter(new TimeFormatter('microseconds'))
|
||||
profileBuilder.setName(name)
|
||||
|
||||
const timeDeltas = getTimeDeltasForSamples(samples)
|
||||
|
||||
samples.forEach((sample, index) => {
|
||||
const timeDelta = timeDeltas[index]
|
||||
const activeFrames = getActiveFramesForSample(contents.stackFrames, sample.sf)
|
||||
|
||||
profileBuilder.appendSampleWithWeight(activeFrames, timeDelta)
|
||||
})
|
||||
|
||||
return profileBuilder.build()
|
||||
}
|
||||
|
||||
function eventListToProfileGroup(
|
||||
events: TraceEvent[],
|
||||
exporterSource: ExporterSource = ExporterSource.UNKNOWN,
|
||||
): ProfileGroup {
|
||||
const importableEvents = filterIgnoredEventTypes(events)
|
||||
const partitionedTraceEvents = partitionByPidTid(importableEvents)
|
||||
const profileNamesByPidTid = getProfileNameByPidTid(events, partitionedTraceEvents)
|
||||
|
||||
const profilePairs: [string, Profile][] = []
|
||||
|
||||
profileNamesByPidTid.forEach((name, profileKey) => {
|
||||
const importableEventsForPidTid = partitionedTraceEvents.get(profileKey)
|
||||
|
||||
if (!importableEventsForPidTid) {
|
||||
throw new Error(`Could not find events for key: ${importableEventsForPidTid}`)
|
||||
}
|
||||
|
||||
profilePairs.push([pidTidKey(pid, tid), profile.build()])
|
||||
profilePairs.push([
|
||||
profileKey,
|
||||
eventListToProfile(importableEventsForPidTid, name, exporterSource),
|
||||
])
|
||||
})
|
||||
|
||||
// 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 :)
|
||||
sortBy(profilePairs, p => p[0])
|
||||
|
||||
return {
|
||||
name: '',
|
||||
indexToView: 0,
|
||||
profiles: profilePairs.map(p => p[1]),
|
||||
}
|
||||
}
|
||||
|
||||
function sampleListToProfileGroup(contents: TraceWithSamples): ProfileGroup {
|
||||
const importableEvents = filterIgnoredEventTypes(contents.traceEvents)
|
||||
const partitionedTraceEvents = partitionByPidTid(importableEvents)
|
||||
const partitionedSamples = partitionByPidTid(contents.samples)
|
||||
const profileNamesByPidTid = getProfileNameByPidTid(contents.traceEvents, partitionedTraceEvents)
|
||||
|
||||
const profilePairs: [string, Profile][] = []
|
||||
|
||||
profileNamesByPidTid.forEach((name, profileKey) => {
|
||||
const samplesForPidTid = partitionedSamples.get(profileKey)
|
||||
|
||||
if (!samplesForPidTid) {
|
||||
throw new Error(`Could not find samples for key: ${samplesForPidTid}`)
|
||||
}
|
||||
|
||||
if (samplesForPidTid.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
profilePairs.push([profileKey, sampleListToProfile(contents, samplesForPidTid, name)])
|
||||
})
|
||||
|
||||
// For now, we just sort processes by pid & tid.
|
||||
@@ -456,27 +705,53 @@ function isTraceEventList(maybeEventList: any): maybeEventList is TraceEvent[] {
|
||||
return true
|
||||
}
|
||||
|
||||
function isTraceEventObject(
|
||||
maybeTraceEventObject: any,
|
||||
): maybeTraceEventObject is {traceEvents: TraceEvent[]} {
|
||||
function isHermesTraceEvent(traceEventArgs: any): traceEventArgs is HermesTraceEventArgs {
|
||||
if (!traceEventArgs) {
|
||||
return false
|
||||
}
|
||||
|
||||
return requiredHermesArguments.every(prop => prop in traceEventArgs)
|
||||
}
|
||||
|
||||
function isHermesTraceEventList(maybeEventList: any): maybeEventList is HermesTraceEvent[] {
|
||||
if (!isTraceEventList(maybeEventList)) return false
|
||||
|
||||
// We just check the first element to avoid iterating over all trace events,
|
||||
// and asumme that if the first one is formatted like a hermes profile then
|
||||
// all events will be
|
||||
return isHermesTraceEvent(maybeEventList[0].args)
|
||||
}
|
||||
|
||||
function isTraceEventObject(maybeTraceEventObject: any): maybeTraceEventObject is TraceEventObject {
|
||||
if (!('traceEvents' in maybeTraceEventObject)) return false
|
||||
return isTraceEventList(maybeTraceEventObject['traceEvents'])
|
||||
}
|
||||
|
||||
export function isTraceEventFormatted(
|
||||
rawProfile: any,
|
||||
): rawProfile is {traceEvents: TraceEvent[]} | TraceEvent[] {
|
||||
function isTraceEventWithSamples(
|
||||
maybeTraceEventObject: any,
|
||||
): maybeTraceEventObject is TraceWithSamples {
|
||||
return (
|
||||
'traceEvents' in maybeTraceEventObject &&
|
||||
'stackFrames' in maybeTraceEventObject &&
|
||||
'samples' in maybeTraceEventObject &&
|
||||
isTraceEventList(maybeTraceEventObject['traceEvents'])
|
||||
)
|
||||
}
|
||||
|
||||
export function isTraceEventFormatted(rawProfile: any): rawProfile is Trace {
|
||||
// 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)) {
|
||||
export function importTraceEvents(rawProfile: Trace): ProfileGroup {
|
||||
if (isTraceEventWithSamples(rawProfile)) {
|
||||
return sampleListToProfileGroup(rawProfile)
|
||||
} else if (isTraceEventObject(rawProfile)) {
|
||||
return eventListToProfileGroup(rawProfile.traceEvents)
|
||||
} else if (isHermesTraceEventList(rawProfile)) {
|
||||
return eventListToProfileGroup(rawProfile, ExporterSource.HERMES)
|
||||
} else if (isTraceEventList(rawProfile)) {
|
||||
return eventListToProfileGroup(rawProfile)
|
||||
} else {
|
||||
|
||||
+4
-1
@@ -204,7 +204,10 @@ export class StringBackedTextFileContent implements TextFileContent {
|
||||
}
|
||||
|
||||
export class TextProfileDataSource implements ProfileDataSource {
|
||||
constructor(private fileName: string, private contents: string) {}
|
||||
constructor(
|
||||
private fileName: string,
|
||||
private contents: string,
|
||||
) {}
|
||||
async name() {
|
||||
return this.fileName
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ function codeToFrameInfo(code: Code, v8log: V8LogProfile): FrameInfo {
|
||||
matches[1].length > 0
|
||||
? matches[1]
|
||||
: file
|
||||
? `(anonymous ${file.split('/').pop()}:${line})`
|
||||
: '(anonymous)'
|
||||
? `(anonymous ${file.split('/').pop()}:${line})`
|
||||
: '(anonymous)'
|
||||
return {
|
||||
key: name,
|
||||
name: functionName,
|
||||
|
||||
@@ -1,5 +1,59 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.0.1 evented profile (no pretty print) 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 0,
|
||||
"line": undefined,
|
||||
"name": "a",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 14,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 1,
|
||||
"line": undefined,
|
||||
"name": "b",
|
||||
"selfWeight": 5,
|
||||
"totalWeight": 14,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 2,
|
||||
"line": undefined,
|
||||
"name": "c",
|
||||
"selfWeight": 5,
|
||||
"totalWeight": 5,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 3,
|
||||
"line": undefined,
|
||||
"name": "d",
|
||||
"selfWeight": 4,
|
||||
"totalWeight": 4,
|
||||
},
|
||||
],
|
||||
"name": "simple.txt",
|
||||
"stacks": Array [
|
||||
"a;b;c 2",
|
||||
"a;b;d 4",
|
||||
"a;b;c 3",
|
||||
"a;b 5",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.0.1 evented profile (no pretty print): indexToView 1`] = `0`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.0.1 evented profile (no pretty print): profileGroup.name 1`] = `"simple.txt"`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.0.1 evented profile 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
|
||||
+4
-1
@@ -82,7 +82,10 @@ if (process.env.NODE_ENV === 'development') {
|
||||
|
||||
export class Atom<T> {
|
||||
private observers: AtomListener[] = []
|
||||
constructor(protected state: T, debugKey: string) {
|
||||
constructor(
|
||||
protected state: T,
|
||||
debugKey: string,
|
||||
) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (hotReloadStash?.has(debugKey)) {
|
||||
// If we have a stored value from a previous hot reload, use that
|
||||
|
||||
+8
-8
@@ -20,14 +20,14 @@ export class Color {
|
||||
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)
|
||||
|
||||
|
||||
@@ -5,6 +5,12 @@ describe('importSpeedscopeProfiles', () => {
|
||||
await checkProfileSnapshot('./sample/profiles/speedscope/0.0.1/simple.speedscope.json')
|
||||
})
|
||||
|
||||
test('0.0.1 evented profile (no pretty print)', async () => {
|
||||
await checkProfileSnapshot(
|
||||
'./sample/profiles/speedscope/0.0.1/simple-no-pretty-print.speedscope.json',
|
||||
)
|
||||
})
|
||||
|
||||
test('0.1.2 sampled profile', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/speedscope/0.1.2/simple-sampled.speedscope.json')
|
||||
})
|
||||
|
||||
+8
-2
@@ -5,7 +5,10 @@ export function clamp(x: number, minVal: number, maxVal: number) {
|
||||
}
|
||||
|
||||
export class Vec2 {
|
||||
constructor(readonly x: number, readonly y: number) {}
|
||||
constructor(
|
||||
readonly x: number,
|
||||
readonly y: number,
|
||||
) {}
|
||||
withX(x: number) {
|
||||
return new Vec2(x, this.y)
|
||||
}
|
||||
@@ -251,7 +254,10 @@ export class AffineTransform {
|
||||
}
|
||||
|
||||
export class Rect {
|
||||
constructor(readonly origin: Vec2, readonly size: Vec2) {}
|
||||
constructor(
|
||||
readonly origin: Vec2,
|
||||
readonly size: Vec2,
|
||||
) {}
|
||||
|
||||
isEmpty() {
|
||||
return this.width() == 0 || this.height() == 0
|
||||
|
||||
@@ -39,7 +39,10 @@ export function exactMatchStrings(text: string, pattern: string): [number, numbe
|
||||
// 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) {}
|
||||
constructor(
|
||||
readonly profile: Profile,
|
||||
readonly searchQuery: string,
|
||||
) {}
|
||||
|
||||
private matches: Map<Frame, [number, number][] | null> | null = null
|
||||
getMatchForFrame(frame: Frame): [number, number][] | null {
|
||||
@@ -65,7 +68,10 @@ interface CachedFlamechartResult {
|
||||
}
|
||||
|
||||
export class FlamechartSearchResults {
|
||||
constructor(readonly flamechart: Flamechart, readonly profileResults: ProfileSearchResults) {}
|
||||
constructor(
|
||||
readonly flamechart: Flamechart,
|
||||
readonly profileResults: ProfileSearchResults,
|
||||
) {}
|
||||
|
||||
private matches: CachedFlamechartResult | null = null
|
||||
private getResults(): CachedFlamechartResult {
|
||||
|
||||
+4
-1
@@ -99,7 +99,10 @@ export class CallTreeNode extends HasWeights {
|
||||
this.frozen = true
|
||||
}
|
||||
|
||||
constructor(readonly frame: Frame, readonly parent: CallTreeNode | null) {
|
||||
constructor(
|
||||
readonly frame: Frame,
|
||||
readonly parent: CallTreeNode | null,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -76,7 +76,11 @@ class Panel {
|
||||
private GRAPH_WIDTH = 74 * PR
|
||||
private GRAPH_HEIGHT = 30 * PR
|
||||
|
||||
constructor(private name: string, private fg: string, private bg: string) {
|
||||
constructor(
|
||||
private name: string,
|
||||
private fg: string,
|
||||
private bg: string,
|
||||
) {
|
||||
this.canvas.width = this.WIDTH
|
||||
this.canvas.height = this.HEIGHT
|
||||
this.canvas.style.cssText = 'width:80px;height:48px'
|
||||
|
||||
@@ -105,6 +105,6 @@ export async function expectImportFailure(filepath: string) {
|
||||
await importProfilesFromArrayBuffer(path.basename(filepath), arrayBuffer)
|
||||
fail('Expected import to fail but it succeeded')
|
||||
} catch (error) {
|
||||
expect(error.message).toMatchSnapshot()
|
||||
expect((error as Error).message).toMatchSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@ test('getOrInsert', () => {
|
||||
})
|
||||
|
||||
class ValueType {
|
||||
private constructor(readonly a: string, readonly num: number) {}
|
||||
private constructor(
|
||||
readonly a: string,
|
||||
readonly num: number,
|
||||
) {}
|
||||
get key() {
|
||||
return `${this.a}_${this.num}`
|
||||
}
|
||||
|
||||
+9
-11
@@ -213,17 +213,15 @@ 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.
|
||||
|
||||
@@ -20,10 +20,10 @@ import {Application} from './application'
|
||||
export const ApplicationContainer = memo(() => {
|
||||
const canvas = useAtom(glCanvasAtom)
|
||||
const theme = useTheme()
|
||||
const canvasContext = useMemo(() => (canvas ? getCanvasContext({theme, canvas}) : null), [
|
||||
theme,
|
||||
canvas,
|
||||
])
|
||||
const canvasContext = useMemo(
|
||||
() => (canvas ? getCanvasContext({theme, canvas}) : null),
|
||||
[theme, canvas],
|
||||
)
|
||||
|
||||
return (
|
||||
<ProfileSearchContextProvider>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {h} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {FileSystemDirectoryEntry} from '../import/file-system-entry'
|
||||
|
||||
import {ProfileGroup, SymbolRemapper} from '../lib/profile'
|
||||
import {FontFamily, FontSize, Duration} from './style'
|
||||
@@ -60,6 +59,10 @@ async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntr
|
||||
declare function require(x: string): any
|
||||
const exampleProfileURL = require('../../sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt')
|
||||
|
||||
function isFileSystemDirectoryEntry(entry: FileSystemEntry): entry is FileSystemDirectoryEntry {
|
||||
return entry != null && entry.isDirectory
|
||||
}
|
||||
|
||||
interface GLCanvasProps {
|
||||
canvasContext: CanvasContext | null
|
||||
theme: Theme
|
||||
@@ -306,13 +309,18 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
|
||||
const firstItem = ev.dataTransfer.items[0]
|
||||
if ('webkitGetAsEntry' in firstItem) {
|
||||
const webkitEntry: FileSystemDirectoryEntry = firstItem.webkitGetAsEntry()
|
||||
const webkitEntry: FileSystemEntry | null = firstItem.webkitGetAsEntry()
|
||||
|
||||
// Instrument.app file format is actually a directory.
|
||||
if (webkitEntry.isDirectory && webkitEntry.name.endsWith('.trace')) {
|
||||
if (
|
||||
webkitEntry &&
|
||||
isFileSystemDirectoryEntry(webkitEntry) &&
|
||||
webkitEntry.name.endsWith('.trace')
|
||||
) {
|
||||
console.log('Importing as Instruments.app .trace file')
|
||||
const webkitDirectoryEntry: FileSystemDirectoryEntry = webkitEntry
|
||||
this.loadProfile(async () => {
|
||||
return await importFromFileSystemDirectoryEntry(webkitEntry)
|
||||
return await importFromFileSystemDirectoryEntry(webkitDirectoryEntry)
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -416,7 +424,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
}
|
||||
|
||||
async maybeLoadHashParamProfile() {
|
||||
const {profileURL} = this.props.hashParams;
|
||||
const {profileURL} = this.props.hashParams
|
||||
if (profileURL) {
|
||||
if (!canUseXHR) {
|
||||
alert(
|
||||
|
||||
@@ -308,9 +308,8 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
const logicalSpaceMouse = this.windowToLogicalViewSpace().transformPosition(
|
||||
new Vec2(ev.clientX, ev.clientY),
|
||||
)
|
||||
const physicalSpaceMouse = this.logicalToPhysicalViewSpace().transformPosition(
|
||||
logicalSpaceMouse,
|
||||
)
|
||||
const physicalSpaceMouse =
|
||||
this.logicalToPhysicalViewSpace().transformPosition(logicalSpaceMouse)
|
||||
return this.configSpaceToPhysicalViewSpace().inverseTransformPosition(physicalSpaceMouse)
|
||||
}
|
||||
|
||||
|
||||
@@ -239,10 +239,7 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
)
|
||||
|
||||
if (match) {
|
||||
const rangesToHighlightInTrimmedText = remapRangesToTrimmedText(
|
||||
trimmedText,
|
||||
match
|
||||
)
|
||||
const rangesToHighlightInTrimmedText = remapRangesToTrimmedText(trimmedText, match)
|
||||
|
||||
// Once we have the character ranges to highlight, we need to
|
||||
// actually do the highlighting.
|
||||
@@ -546,12 +543,10 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
private zoom(logicalViewSpaceCenter: Vec2, multiplier: number) {
|
||||
this.interactionLock = 'zoom'
|
||||
|
||||
const physicalCenter = this.logicalToPhysicalViewSpace().transformPosition(
|
||||
logicalViewSpaceCenter,
|
||||
)
|
||||
const configSpaceCenter = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(
|
||||
physicalCenter,
|
||||
)
|
||||
const physicalCenter =
|
||||
this.logicalToPhysicalViewSpace().transformPosition(logicalViewSpaceCenter)
|
||||
const configSpaceCenter =
|
||||
this.configSpaceToPhysicalViewSpace().inverseTransformPosition(physicalCenter)
|
||||
if (!configSpaceCenter) return
|
||||
|
||||
const zoomTransform = AffineTransform.withTranslation(configSpaceCenter.times(-1))
|
||||
@@ -634,14 +629,12 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
this.onMouseDrag(ev)
|
||||
return
|
||||
}
|
||||
this.hoveredLabel = null
|
||||
|
||||
const logicalViewSpaceMouse = new Vec2(ev.offsetX, ev.offsetY)
|
||||
const physicalViewSpaceMouse = this.logicalToPhysicalViewSpace().transformPosition(
|
||||
logicalViewSpaceMouse,
|
||||
)
|
||||
const configSpaceMouse = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(
|
||||
physicalViewSpaceMouse,
|
||||
)
|
||||
const physicalViewSpaceMouse =
|
||||
this.logicalToPhysicalViewSpace().transformPosition(logicalViewSpaceMouse)
|
||||
const configSpaceMouse =
|
||||
this.configSpaceToPhysicalViewSpace().inverseTransformPosition(physicalViewSpaceMouse)
|
||||
|
||||
if (!configSpaceMouse) return
|
||||
|
||||
@@ -664,6 +657,21 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
}
|
||||
}
|
||||
|
||||
// This is a dumb hack to get around what appears to be a bug in
|
||||
// TypeScript's reachability analysis. If I do the this.hoveredLabel = null
|
||||
// in the outer function body, the code below accessing
|
||||
// this.hoveredLabel!.node inside of the `if (this.hoveredLabel) {`
|
||||
// complains that "no property node on never", indicating that it thinks
|
||||
// that codepath is unreachable.
|
||||
//
|
||||
// Because this.hoveredLabel is accessed in the bound function
|
||||
// setHoveredLabel, the codepath is obviously reachable, but the type
|
||||
// checker is confused about this for some reason.
|
||||
const clearHoveredLabel = () => {
|
||||
this.hoveredLabel = null
|
||||
}
|
||||
clearHoveredLabel()
|
||||
|
||||
for (let frame of this.props.flamechart.getLayers()[0] || []) {
|
||||
setHoveredLabel(frame)
|
||||
}
|
||||
|
||||
+36
-30
@@ -3,7 +3,7 @@ import {Sizes, FontSize, FontFamily, ZIndex} from './style'
|
||||
import {css, StyleSheet} from 'aphrodite'
|
||||
import {ComponentChildren, h} from 'preact'
|
||||
import {useTheme, withTheme} from './themes/theme'
|
||||
import { useCallback } from 'preact/hooks'
|
||||
import {useCallback} from 'preact/hooks'
|
||||
|
||||
interface HovertipProps {
|
||||
containerSize: Vec2
|
||||
@@ -20,42 +20,48 @@ export function Hovertip(props: HovertipProps) {
|
||||
|
||||
const OFFSET_FROM_MOUSE = 7
|
||||
|
||||
const updateLocation = useCallback((el: HTMLDivElement | null) => {
|
||||
if (!el) return
|
||||
const updateLocation = useCallback(
|
||||
(el: HTMLDivElement | null) => {
|
||||
if (!el) return
|
||||
|
||||
const clientRect = el.getBoundingClientRect()
|
||||
const clientRect = el.getBoundingClientRect()
|
||||
|
||||
// Place the hovertip to the right of the cursor.
|
||||
let leftEdgeX = offset.x + OFFSET_FROM_MOUSE
|
||||
// Place the hovertip to the right of the cursor.
|
||||
let leftEdgeX = offset.x + OFFSET_FROM_MOUSE
|
||||
|
||||
// If this would cause it to overflow the container, align the right
|
||||
// edge of the hovertip with the right edge of the container.
|
||||
if (leftEdgeX + clientRect.width > containerWidth - 1) {
|
||||
leftEdgeX = containerWidth - clientRect.width - 1
|
||||
// If this would cause it to overflow the container, align the right
|
||||
// edge of the hovertip with the right edge of the container.
|
||||
if (leftEdgeX + clientRect.width > containerWidth - 1) {
|
||||
leftEdgeX = containerWidth - clientRect.width - 1
|
||||
|
||||
// If aligning the right edge overflows the container, align the left edge
|
||||
// of the hovertip with the left edge of the container.
|
||||
if (leftEdgeX < 1) { leftEdgeX = 1 }
|
||||
}
|
||||
el.style.left = `${leftEdgeX}px`
|
||||
// If aligning the right edge overflows the container, align the left edge
|
||||
// of the hovertip with the left edge of the container.
|
||||
if (leftEdgeX < 1) {
|
||||
leftEdgeX = 1
|
||||
}
|
||||
}
|
||||
el.style.left = `${leftEdgeX}px`
|
||||
|
||||
// Place the tooltip below the cursor
|
||||
let topEdgeY = offset.y + OFFSET_FROM_MOUSE
|
||||
// Place the tooltip below the cursor
|
||||
let topEdgeY = offset.y + OFFSET_FROM_MOUSE
|
||||
|
||||
// If this would cause it to overflow the container, place the hovertip
|
||||
// above the cursor instead. This intentionally differs from the horizontal
|
||||
// axis logic to avoid the cursor being in the middle of a hovertip when
|
||||
// possible.
|
||||
if (topEdgeY + clientRect.height > containerHeight - 1) {
|
||||
topEdgeY = offset.y - clientRect.height - 1
|
||||
// If this would cause it to overflow the container, place the hovertip
|
||||
// above the cursor instead. This intentionally differs from the horizontal
|
||||
// axis logic to avoid the cursor being in the middle of a hovertip when
|
||||
// possible.
|
||||
if (topEdgeY + clientRect.height > containerHeight - 1) {
|
||||
topEdgeY = offset.y - clientRect.height - 1
|
||||
|
||||
// If placing the hovertip above the cursor overflows the container, align
|
||||
// the top edge of the hovertip with the top edge of the container.
|
||||
if (topEdgeY < 1) { topEdgeY = 1 }
|
||||
}
|
||||
el.style.top = `${topEdgeY}px`
|
||||
|
||||
}, [containerWidth, containerHeight, offset.x, offset.y])
|
||||
// If placing the hovertip above the cursor overflows the container, align
|
||||
// the top edge of the hovertip with the top edge of the container.
|
||||
if (topEdgeY < 1) {
|
||||
topEdgeY = 1
|
||||
}
|
||||
}
|
||||
el.style.top = `${topEdgeY}px`
|
||||
},
|
||||
[containerWidth, containerHeight, offset.x, offset.y],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css(style.hoverTip)} ref={updateLocation}>
|
||||
|
||||
@@ -268,12 +268,14 @@ export const ProfileTableView = memo(
|
||||
[sandwichContext],
|
||||
)
|
||||
|
||||
const onTotalClick = useCallback((ev: MouseEvent) => onSortClick(SortField.TOTAL, ev), [
|
||||
onSortClick,
|
||||
])
|
||||
const onSelfClick = useCallback((ev: MouseEvent) => onSortClick(SortField.SELF, ev), [
|
||||
onSortClick,
|
||||
])
|
||||
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],
|
||||
|
||||
Reference in New Issue
Block a user