Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60f1812e51 | ||
|
|
1717fecafb | ||
|
|
c296f530c7 | ||
|
|
8e0fa58d65 | ||
|
|
b21480494e | ||
|
|
dfd3a0dfb3 | ||
|
|
ac4a015559 | ||
|
|
de17f128d0 | ||
|
|
304ccf33ab |
@@ -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,14 @@
|
||||
## [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)
|
||||
|
||||
+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.16.0",
|
||||
"version": "1.17.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,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 [
|
||||
@@ -1224,6 +1361,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,11 @@ 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 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')
|
||||
})
|
||||
|
||||
+328
-136
@@ -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
|
||||
@@ -65,16 +71,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 +146,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 frameInfoForEvent(bFront).key === frameInfoForEvent(eFront).key ? 'B' : 'E'
|
||||
}
|
||||
|
||||
function convertToEventQueues(events: ImportableTraceEvent[]): [BTraceEvent[], ETraceEvent[]] {
|
||||
@@ -244,110 +287,147 @@ function frameInfoForEvent(event: TraceEvent): FrameInfo {
|
||||
}
|
||||
}
|
||||
|
||||
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): 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), b.ts)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
const bFrameInfo = frameInfoForEvent(b)
|
||||
|
||||
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)
|
||||
|
||||
let swapped: boolean = false
|
||||
|
||||
for (let i = 1; i < eEventQueue.length; i++) {
|
||||
const eEvent = eEventQueue[i]
|
||||
if (eEvent.ts > eEventQueue[0].ts) {
|
||||
// Only consider 'E' events with the same ts as the front of the queue.
|
||||
break
|
||||
}
|
||||
|
||||
const eFrameInfo = frameInfoForEvent(eEvent)
|
||||
if (bFrameInfo.key === eFrameInfo.key) {
|
||||
// We have a match! Process this one first.
|
||||
const temp = eEventQueue[0]
|
||||
eEventQueue[0] = eEventQueue[i]
|
||||
eEventQueue[i] = temp
|
||||
swapped = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!swapped) {
|
||||
// There was no key match, let's see if we can find a name match
|
||||
for (let i = 1; i < eEventQueue.length; i++) {
|
||||
const eEvent = eEventQueue[i]
|
||||
if (eEvent.ts > eEventQueue[0].ts) {
|
||||
@@ -355,8 +435,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 +444,156 @@ 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])
|
||||
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[]): 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)])
|
||||
})
|
||||
|
||||
// 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,26 +641,33 @@ function isTraceEventList(maybeEventList: any): maybeEventList is TraceEvent[] {
|
||||
return true
|
||||
}
|
||||
|
||||
function isTraceEventObject(
|
||||
maybeTraceEventObject: any,
|
||||
): maybeTraceEventObject is {traceEvents: TraceEvent[]} {
|
||||
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 (isTraceEventList(rawProfile)) {
|
||||
return eventListToProfileGroup(rawProfile)
|
||||
|
||||
+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