Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a979bb568 | ||
|
|
36911599cb | ||
|
|
b32ff08ca3 | ||
|
|
361bdc9cd2 | ||
|
|
a1384b03be | ||
|
|
5f32640060 | ||
|
|
a10c834f99 | ||
|
|
de3ab89eb5 | ||
|
|
aee2dfdf89 | ||
|
|
d9b3950274 | ||
|
|
177696e359 | ||
|
|
16e32dc08e |
+32
-1
@@ -1,4 +1,35 @@
|
||||
## Unreleased
|
||||
## [1.12.1] - 2020-11-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed for retina displays [[#327](https://github.com/jlfwong/speedscope/issues/327)]
|
||||
|
||||
## [1.12.0] - 2020-11-12
|
||||
|
||||
### Added
|
||||
|
||||
- Dark mode! [[#323](https://github.com/jlfwong/speedscope/pull/323)]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed incorrect highlighting when search result overlaps "…" [[#326](https://github.com/jlfwong/speedscope/pull/326)]
|
||||
|
||||
## [1.11.1] - 2020-10-25
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix trace-event import for many cases where there are 'ts' collisions [[#322](https://github.com/jlfwong/speedscope/pull/322)]
|
||||
- Fix import of trace event files where B/E events' args don't match [[#321](https://github.com/jlfwong/speedscope/pull/321)]
|
||||
|
||||
## [1.11.0] - 2020-10-13
|
||||
|
||||
### Added
|
||||
|
||||
- Support remapping profiles using source maps [[#317](https://github.com/jlfwong/speedscope/pull/317)]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix line & column numbers in imports from Chrome & Firefox [[#318](https://github.com/jlfwong/speedscope/pull/318)]
|
||||
|
||||
## [1.10.0] - 2020-09-29
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ speedscope is designed to ingest profiles from a variety of different profilers
|
||||
- Python
|
||||
- [Importing from py-spy](https://github.com/jlfwong/speedscope/wiki/Importing-from-py-spy-(python))
|
||||
- [pyspeedscope](https://github.com/windelbouwman/pyspeedscope)
|
||||
- [Importing from Austin](https://github.com/p403n1x87/austin#speedscope)
|
||||
- [Importing from Austin](https://github.com/P403n1x87/austin-python#format-conversion)
|
||||
- Go
|
||||
- [Importing from pprof](https://github.com/jlfwong/speedscope/wiki/Importing-from-pprof-(go))
|
||||
- Rust
|
||||
|
||||
Generated
+154
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "speedscope",
|
||||
"version": "1.9.0",
|
||||
"version": "1.11.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -1432,6 +1432,12 @@
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1457,6 +1463,12 @@
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
|
||||
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1518,6 +1530,12 @@
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3632,6 +3650,14 @@
|
||||
"requires": {
|
||||
"mdn-data": "2.0.4",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"css-what": {
|
||||
@@ -3740,6 +3766,14 @@
|
||||
"requires": {
|
||||
"mdn-data": "2.0.6",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"mdn-data": {
|
||||
@@ -4173,6 +4207,13 @@
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
|
||||
"integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5852,6 +5893,14 @@
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6494,6 +6543,12 @@
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6852,6 +6907,14 @@
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
@@ -6996,6 +7059,12 @@
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8297,6 +8366,12 @@
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8555,6 +8630,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
|
||||
@@ -8782,6 +8863,14 @@
|
||||
"chalk": "^2.4.1",
|
||||
"source-map": "^0.6.1",
|
||||
"supports-color": "^5.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
@@ -8834,6 +8923,14 @@
|
||||
"chalk": "^2.4.1",
|
||||
"source-map": "^0.6.1",
|
||||
"supports-color": "^5.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
@@ -8886,6 +8983,14 @@
|
||||
"chalk": "^2.4.1",
|
||||
"source-map": "^0.6.1",
|
||||
"supports-color": "^5.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
@@ -8938,6 +9043,14 @@
|
||||
"chalk": "^2.4.1",
|
||||
"source-map": "^0.6.1",
|
||||
"supports-color": "^5.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
@@ -10249,6 +10362,14 @@
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"source-map-url": {
|
||||
@@ -10352,6 +10473,15 @@
|
||||
"esutils": "^2.0.2",
|
||||
"optionator": "^0.8.1",
|
||||
"source-map": "~0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11117,6 +11247,14 @@
|
||||
"commander": "^2.19.0",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"test-exclude": {
|
||||
@@ -11550,6 +11688,12 @@
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz",
|
||||
"integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -11605,6 +11749,15 @@
|
||||
"esutils": "^2.0.2",
|
||||
"optionator": "^0.8.1",
|
||||
"source-map": "~0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "speedscope",
|
||||
"version": "1.10.0",
|
||||
"version": "1.12.1",
|
||||
"description": "",
|
||||
"repository": "jlfwong/speedscope",
|
||||
"main": "index.js",
|
||||
@@ -50,6 +50,7 @@
|
||||
"prettier": "2.0.4",
|
||||
"protobufjs": "6.8.8",
|
||||
"redux": "^4.0.5",
|
||||
"source-map": "0.6.1",
|
||||
"ts-jest": "24.3.0",
|
||||
"typescript": "3.9.2",
|
||||
"typescript-eslint-parser": "17.0.1",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
This directory contains profiles & source-maps to test if source-map
|
||||
remapping of profiles is working correctly. See the corresponding
|
||||
"sourcemaps" directory in programs/javascript to see how these were
|
||||
generated.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["gamma.ts", "beta.ts", "delta.ts", "alpha.ts", "kludge.ts", "typescript-source-map-test.ts"],
|
||||
"sourcesContent": ["export const gamma = () => {\n let prod = 1\n for (let i = 1; i < 1000; i++) {\n prod *= i\n }\n return prod\n}\n", "import {gamma} from './gamma'\n\nexport function beta() {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n", "import {gamma} from './gamma'\n\nexport const delta = function () {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n", "import {beta} from './beta'\nimport {delta} from './delta'\n\nexport function alpha() {\n ;(function () {\n for (let i = 0; i < 1000; i++) {\n beta()\n delta()\n }\n })()\n}\n", "import {alpha} from './alpha'\n\nexport class Kludge {\n constructor() {\n alpha()\n console.log(this.floop)\n }\n\n zap() {\n alpha()\n }\n\n get floop(): number {\n alpha()\n return 1\n }\n}\n", "import {Kludge} from './kludge'\n\nconst k = new Kludge()\nk.zap()\n"],
|
||||
"mappings": "MAAO,KAAM,GAAQ,KACnB,GAAI,GAAO,EACX,OAAS,GAAI,EAAG,EAAI,IAAM,IACxB,GAAQ,EAEV,MAAO,ICHF,aACL,OAAS,GAAI,EAAG,EAAI,GAAI,IACtB,ICFG,KAAM,GAAQ,WACnB,OAAS,GAAI,EAAG,EAAI,GAAI,IACtB,KCDG,aACJ,AAAC,YACA,OAAS,GAAI,EAAG,EAAI,IAAM,IACxB,IACA,QCPN,QAGE,cACE,IACA,QAAQ,IAAI,KAAK,OAGnB,MACE,OAGE,SACF,WACO,GCZX,KAAM,GAAI,GAAI,GACd,EAAE",
|
||||
"names": []
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["gamma.ts","beta.ts","delta.ts","alpha.ts","kludge.ts","typescript-source-map-test.ts"],"names":[],"mappings":";AAAO,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,WAAA,EAAA,IAAM,EAAQ,WAEd,IADD,IAAA,EAAO,EACF,EAAI,EAAG,EAAI,IAAM,IACxB,GAAQ,EAEH,OAAA,GALF,QAAA,MAAA;;ACMN,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,KAAA,EAND,IAAA,EAAA,QAAA,WAEM,SAAU,IACT,IAAA,IAAI,EAAI,EAAG,EAAI,GAAI,KACtB,EAAA,EAAA;;ACFG,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,WAAA,EAFP,IAAA,EAAA,QAAA,WAEa,EAAQ,WACd,IAAA,IAAI,EAAI,EAAG,EAAI,GAAI,KACtB,EAAA,EAAA,UAFG,QAAA,MAAA;;ACQN,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,MAAA,EAVD,IAAA,EAAA,QAAA,UACA,EAAA,QAAA,WAEM,SAAU,KACZ,WACK,IAAA,IAAI,EAAI,EAAG,EAAI,IAAM,KACxB,EAAA,EAAA,SACA,EAAA,EAAA,SAHF;;ACFJ,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,YAAA,EAFA,IAAA,EAAA,QAAA,WAEA,EAAA,WACE,SAAA,KACE,EAAA,EAAA,SACA,QAAQ,IAAI,KAAK,OAWrB,OARE,EAAA,UAAA,IAAA,YACE,EAAA,EAAA,UAGF,OAAA,eAAI,EAAA,UAAA,QAAK,CAAT,IAAA,WAES,OADP,EAAA,EAAA,SACO,GAFA,YAAA,EAVX,cAAA,IAcA,EAdA,GAAA,QAAA,OAAA;;ACCA,aAHA,IAAA,EAAA,QAAA,YAEM,EAAI,IAAI,EAAJ,OACV,EAAE","file":"typescript-source-map-test.js","sourceRoot":"..","sourcesContent":["export const gamma = () => {\n let prod = 1\n for (let i = 1; i < 1000; i++) {\n prod *= i\n }\n return prod\n}\n","import {gamma} from './gamma'\n\nexport function beta() {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n","import {gamma} from './gamma'\n\nexport const delta = function () {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n","import {beta} from './beta'\nimport {delta} from './delta'\n\nexport function alpha() {\n ;(function () {\n for (let i = 0; i < 1000; i++) {\n beta()\n delta()\n }\n })()\n}\n","import {alpha} from './alpha'\n\nexport class Kludge {\n constructor() {\n alpha()\n console.log(this.floop)\n }\n\n zap() {\n alpha()\n }\n\n get floop(): number {\n alpha()\n return 1\n }\n}\n","import {Kludge} from './kludge'\n\nconst k = new Kludge()\nk.zap()\n"]}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack://speedscope-sourcemap-test-project/./gamma.ts","webpack://speedscope-sourcemap-test-project/./beta.ts","webpack://speedscope-sourcemap-test-project/./delta.ts","webpack://speedscope-sourcemap-test-project/./alpha.ts","webpack://speedscope-sourcemap-test-project/./typescript-source-map-test.ts","webpack://speedscope-sourcemap-test-project/./kludge.ts"],"names":["gamma","prod","i","beta","delta","alpha","console","log","this","floop","zap"],"mappings":"mBAAO,IAAMA,EAAQ,WAEnB,IADA,IAAIC,EAAO,EACFC,EAAI,EAAGA,EAAI,IAAMA,IACxBD,GAAQC,EAEV,OAAOD,GCHF,SAASE,IACd,IAAK,IAAID,EAAI,EAAGA,EAAI,GAAIA,IACtBF,ICFG,IAAMI,EAAQ,WACnB,IAAK,IAAIF,EAAI,EAAGA,EAAI,GAAIA,IACtBF,KCDG,SAASK,KACb,WACC,IAAK,IAAIH,EAAI,EAAGA,EAAI,IAAMA,IACxBC,IACAC,IAHH,ICFO,ICAV,WACE,aACEC,IACAC,QAAQC,IAAIC,KAAKC,OAWrB,OARE,YAAAC,IAAA,WACEL,KAGF,sBAAI,oBAAK,C,IAAT,WAEE,OADAA,IACO,G,gCAEX,EAdA,KDCEK,O","file":"typescript-source-map-test.js","sourcesContent":["export const gamma = () => {\n let prod = 1\n for (let i = 1; i < 1000; i++) {\n prod *= i\n }\n return prod\n}\n","import {gamma} from './gamma'\n\nexport function beta() {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n","import {gamma} from './gamma'\n\nexport const delta = function () {\n for (let i = 0; i < 10; i++) {\n gamma()\n }\n}\n","import {beta} from './beta'\nimport {delta} from './delta'\n\nexport function alpha() {\n ;(function () {\n for (let i = 0; i < 1000; i++) {\n beta()\n delta()\n }\n })()\n}\n","import {Kludge} from './kludge'\n\nconst k = new Kludge()\nk.zap()\n","import {alpha} from './alpha'\n\nexport class Kludge {\n constructor() {\n alpha()\n console.log(this.floop)\n }\n\n zap() {\n alpha()\n }\n\n get floop(): number {\n alpha()\n return 1\n }\n}\n"],"sourceRoot":""}
|
||||
@@ -0,0 +1,25 @@
|
||||
[
|
||||
{ "pid": 0, "tid": 0, "ph": "B", "ts": 0, "name": "A" },
|
||||
{ "pid": 0, "tid": 0, "ph": "E", "ts": 2, "name": "A" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 2, "name": "B" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 10, "dur": 2, "name": "C" },
|
||||
{ "pid": 0, "tid": 0, "ph": "B", "ts": 10, "name": "D" },
|
||||
{ "pid": 0, "tid": 0, "ph": "E", "ts": 12, "name": "D" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 20, "dur": 1, "name": "E" },
|
||||
{ "pid": 0, "tid": 0, "ph": "B", "ts": 20, "name": "F" },
|
||||
{ "pid": 0, "tid": 0, "ph": "E", "ts": 22, "name": "F" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "B", "ts": 30, "name": "G" },
|
||||
{ "pid": 0, "tid": 0, "ph": "E", "ts": 32, "name": "G" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 30, "dur": 1, "name": "H" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 41, "dur": 1, "name": "I" },
|
||||
{ "pid": 0, "tid": 0, "ph": "B", "ts": 40, "name": "J" },
|
||||
{ "pid": 0, "tid": 0, "ph": "E", "ts": 42, "name": "J" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "B", "ts": 50, "name": "K" },
|
||||
{ "pid": 0, "tid": 0, "ph": "E", "ts": 52, "name": "K" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 51, "dur": 1, "name": "L" }
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 2}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 1}, "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "B", "args": {"x": 2}, "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "args": {"x": 2}, "ts": 10},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "Z", "args": {"x": 1}, "ts": 10, "dur": 1}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{"tid": 1, "ph": "X", "pid": 0, "name": "alpha", "args": {"x": 0}, "ts": 0, "dur": 10},
|
||||
{"tid": 1, "ph": "B", "pid": 0, "name": "beta", "args": {"x": 0}, "ts": 1},
|
||||
{"tid": 1, "ph": "B", "pid": 0, "name": "gamma", "args": {"x": 0}, "ts": 1},
|
||||
{"tid": 1, "ph": "E", "pid": 0, "name": "beta", "args": {"x": 1}, "ts": 2},
|
||||
{"tid": 1, "ph": "E", "pid": 0, "name": "gamma", "args": {"x": 1}, "ts": 2}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
[
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 10, "name": "alpha" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 10, "name": "beta" }
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 10, "name": "alpha" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 1, "dur": 1, "name": "beta" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 1, "dur": 1, "name": "gamma" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 1, "name": "gamma" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 1, "name": "beta" }
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 1}, "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "args": {"x": 2}, "ts": 10}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 10}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 1}, "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 2}, "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "A", "args": {"x": 1}, "ts": 10, "dur": 1}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 1}, "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "B", "args": {"x": 2}, "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "C", "args": {"x": 2}, "ts": 2}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 9},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 10},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "B", "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "B", "ts": 9},
|
||||
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "ts": 10},
|
||||
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "C", "ts": 2},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "C", "ts": 8}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 1}, "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "args": {"x": 2}, "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "args": {"x": 1}, "ts": 9},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "args": {"x": 2}, "ts": 10}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "alpha", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "beta", "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "alpha", "ts": 9},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "beta", "ts": 10}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 20, "name": "alpha" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 1, "dur": 2, "name": "beta" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 2, "dur": 1, "name": "gamma" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 2, "name": "gamma" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 6, "dur": 1, "name": "beta" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 10, "dur": 1, "name": "gamma" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 9, "dur": 2, "name": "beta" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 15, "dur": 1, "name": "beta" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 14, "dur": 2, "name": "gamma" }
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 0, "dur": 20, "name": "alpha" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 1, "dur": 2, "name": "beta" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 1, "dur": 1, "name": "gamma" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 2, "name": "gamma" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 5, "dur": 1, "name": "beta" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 9, "dur": 1, "name": "beta" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 9, "dur": 2, "name": "gamma" },
|
||||
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 13, "dur": 1, "name": "gamma" },
|
||||
{ "pid": 0, "tid": 0, "ph": "X", "ts": 13, "dur": 2, "name": "beta" }
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "A", "ts": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "B", "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "B", "ts": 1},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "C", "ts": 2},
|
||||
{"pid": 0, "tid": 0, "ph": "B", "name": "C", "ts": 2},
|
||||
{"pid": 0, "tid": 0, "ph": "X", "name": "D", "ts": 3, "dur": 0},
|
||||
{"pid": 0, "tid": 0, "ph": "E", "name": "A", "ts": 10}
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
## Source Map Test Project
|
||||
|
||||
This directory contains test files used to test whether the remapping of
|
||||
performance profiles using sourcemaps work correctly.
|
||||
|
||||
Run `npm run build` to build the artifacts, then open the appropriate files in
|
||||
the `html` directory in whatever browser you're testing.
|
||||
|
||||
The idea is to sourcemaps generated by a variety of tools, and also to take
|
||||
profiles from a variety of browsers, and hopefully see that they all get
|
||||
remapped as expected.
|
||||
|
||||
This project is set up to go through three levels of source-map indirection,
|
||||
and also using multiple different build chains.
|
||||
|
||||
1. TypeScript -> JavaScript source generation
|
||||
2. JavaScript source -> JavaScript bundling
|
||||
3. Minification
|
||||
|
||||
Some bundlers will swap the order of steps 2 & 3, or potentially merge them,
|
||||
but it's complex yet realistic enough that this will hoepfully suss out
|
||||
problems.
|
||||
@@ -0,0 +1,11 @@
|
||||
import {beta} from './beta'
|
||||
import {delta} from './delta'
|
||||
|
||||
export function alpha() {
|
||||
;(function () {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
beta()
|
||||
delta()
|
||||
}
|
||||
})()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {gamma} from './gamma'
|
||||
|
||||
export function beta() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
gamma()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {gamma} from './gamma'
|
||||
|
||||
export const delta = function () {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
gamma()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const gamma = () => {
|
||||
let prod = 1
|
||||
for (let i = 1; i < 1000; i++) {
|
||||
prod *= i
|
||||
}
|
||||
return prod
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<title>ESbuild</title>
|
||||
<script src="../dist/esbuild/typescript-source-map-test.js"></script>
|
||||
<h1>ESbuild Source Map Test</h1>
|
||||
@@ -0,0 +1,3 @@
|
||||
<title>Parcel</title>
|
||||
<script src="../dist/parcel/typescript-source-map-test.js"></script>
|
||||
<h1>Parcel Source Map Test</h1>
|
||||
@@ -0,0 +1,3 @@
|
||||
<title>Webpack</title>
|
||||
<script src="../dist/webpack/typescript-source-map-test.js"></script>
|
||||
<h1>Webpack Source Map Test</h1>
|
||||
@@ -0,0 +1,17 @@
|
||||
import {alpha} from './alpha'
|
||||
|
||||
export class Kludge {
|
||||
constructor() {
|
||||
alpha()
|
||||
console.log(this.floop)
|
||||
}
|
||||
|
||||
zap() {
|
||||
alpha()
|
||||
}
|
||||
|
||||
get floop(): number {
|
||||
alpha()
|
||||
return 1
|
||||
}
|
||||
}
|
||||
+10497
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "speedscope-sourcemap-test-project",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"private": "true",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "npm run parcel && npm run webpack && npm run esbuild",
|
||||
"parcel": "parcel build -o parcel/typescript-source-map-test typescript-source-map-test.ts",
|
||||
"webpack": "webpack",
|
||||
"esbuild": "esbuild --sourcemap --minify --bundle --outdir=dist/esbuild typescript-source-map-test.ts"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.7.14",
|
||||
"parcel": "^1.12.4",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^5.0.0",
|
||||
"webpack-cli": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"module": "es6",
|
||||
"target": "es5",
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import {Kludge} from './kludge'
|
||||
|
||||
const k = new Kludge()
|
||||
k.zap()
|
||||
@@ -0,0 +1,22 @@
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
entry: './typescript-source-map-test.ts',
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
},
|
||||
output: {
|
||||
filename: 'typescript-source-map-test.js',
|
||||
path: path.resolve(__dirname, 'dist', 'webpack'),
|
||||
},
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import {TextureRenderer} from './texture-renderer'
|
||||
import {Rect, Vec2} from '../lib/math'
|
||||
import {ViewportRectangleRenderer} from './overlay-rectangle-renderer'
|
||||
import {FlamechartColorPassRenderer} from './flamechart-color-pass-renderer'
|
||||
import {Color} from '../lib/color'
|
||||
import {Theme} from '../views/themes/theme'
|
||||
|
||||
type FrameCallback = () => void
|
||||
|
||||
@@ -13,13 +15,19 @@ export class CanvasContext {
|
||||
public readonly textureRenderer: TextureRenderer
|
||||
public readonly viewportRectangleRenderer: ViewportRectangleRenderer
|
||||
public readonly flamechartColorPassRenderer: FlamechartColorPassRenderer
|
||||
public readonly theme: Theme
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
constructor(canvas: HTMLCanvasElement, theme: Theme) {
|
||||
this.gl = new WebGL.Context(canvas)
|
||||
this.rectangleBatchRenderer = new RectangleBatchRenderer(this.gl)
|
||||
this.textureRenderer = new TextureRenderer(this.gl)
|
||||
this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl)
|
||||
this.flamechartColorPassRenderer = new FlamechartColorPassRenderer(this.gl)
|
||||
this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl, theme)
|
||||
this.flamechartColorPassRenderer = new FlamechartColorPassRenderer(this.gl, theme)
|
||||
this.theme = theme
|
||||
|
||||
// Whenever the canvas is resized, draw immediately. This prevents
|
||||
// flickering during resizing.
|
||||
this.gl.addAfterResizeEventHandler(this.onBeforeFrame)
|
||||
|
||||
const webGLInfo = this.gl.getWebGLInfo()
|
||||
if (webGLInfo) {
|
||||
@@ -48,7 +56,8 @@ export class CanvasContext {
|
||||
private onBeforeFrame = () => {
|
||||
this.animationFrameRequest = null
|
||||
this.gl.setViewport(0, 0, this.gl.renderTargetWidthInPixels, this.gl.renderTargetHeightInPixels)
|
||||
this.gl.clear(new Graphics.Color(1, 1, 1, 1))
|
||||
const color = Color.fromCSSHex(this.theme.bgPrimaryColor)
|
||||
this.gl.clear(new Graphics.Color(color.r, color.g, color.b, color.a))
|
||||
|
||||
for (const handler of this.beforeFrameHandlers) {
|
||||
handler()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {Vec2, Rect, AffineTransform} from '../lib/math'
|
||||
import {Theme} from '../views/themes/theme'
|
||||
import {Graphics} from './graphics'
|
||||
import {setUniformAffineTransform} from './utils'
|
||||
|
||||
@@ -20,7 +21,7 @@ const vert = `
|
||||
}
|
||||
`
|
||||
|
||||
const frag = `
|
||||
const frag = (colorForBucket: string) => `
|
||||
precision mediump float;
|
||||
|
||||
uniform vec2 uvSpacePixelSize;
|
||||
@@ -49,13 +50,7 @@ const frag = `
|
||||
return 2.0 * abs(fract(x) - 0.5) - 1.0;
|
||||
}
|
||||
|
||||
vec3 colorForBucket(float t) {
|
||||
float x = triangle(30.0 * t);
|
||||
float H = 360.0 * (0.9 * t);
|
||||
float C = 0.25 + 0.2 * x;
|
||||
float L = 0.80 - 0.15 * x;
|
||||
return hcl2rgb(H, C, L);
|
||||
}
|
||||
${colorForBucket}
|
||||
|
||||
void main() {
|
||||
vec4 here = texture2D(colorTexture, vUv);
|
||||
@@ -107,7 +102,7 @@ export class FlamechartColorPassRenderer {
|
||||
private material: Graphics.Material
|
||||
private buffer: Graphics.VertexBuffer
|
||||
|
||||
constructor(private gl: Graphics.Context) {
|
||||
constructor(private gl: Graphics.Context, theme: Theme) {
|
||||
const vertices = [
|
||||
{pos: [-1, 1], uv: [0, 1]},
|
||||
{pos: [1, 1], uv: [1, 1]},
|
||||
@@ -124,7 +119,7 @@ export class FlamechartColorPassRenderer {
|
||||
|
||||
this.buffer = gl.createVertexBuffer(vertexFormat.stride * vertices.length)
|
||||
this.buffer.uploadFloats(floats)
|
||||
this.material = gl.createMaterial(vertexFormat, vert, frag)
|
||||
this.material = gl.createMaterial(vertexFormat, vert, frag(theme.colorForBucketGLSL))
|
||||
}
|
||||
|
||||
render(props: FlamechartColorPassRenderProps) {
|
||||
|
||||
+21
-2
@@ -83,6 +83,15 @@ export namespace Graphics {
|
||||
public alphaF: number,
|
||||
) {}
|
||||
|
||||
equals(other: Color): boolean {
|
||||
return (
|
||||
this.redF === other.redF &&
|
||||
this.greenF === other.greenF &&
|
||||
this.blueF === other.blueF &&
|
||||
this.alphaF === other.alphaF
|
||||
)
|
||||
}
|
||||
|
||||
static TRANSPARENT = new Color(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
@@ -160,6 +169,14 @@ export namespace Graphics {
|
||||
setUnpremultipliedBlendState() {
|
||||
this.setBlendState(BlendOperation.SOURCE_ALPHA, BlendOperation.INVERSE_SOURCE_ALPHA)
|
||||
}
|
||||
|
||||
protected resizeEventHandlers = new Set<() => void>()
|
||||
addAfterResizeEventHandler(callback: () => void): void {
|
||||
this.resizeEventHandlers.add(callback)
|
||||
}
|
||||
removeAfterResizeEventHandler(callback: () => void): void {
|
||||
this.resizeEventHandlers.delete(callback)
|
||||
}
|
||||
}
|
||||
|
||||
export interface Material {
|
||||
@@ -459,7 +476,7 @@ export namespace WebGL {
|
||||
const bounds = canvas.getBoundingClientRect()
|
||||
|
||||
if (
|
||||
this._width === widthInAppUnits &&
|
||||
this._width === widthInPixels &&
|
||||
this._height === heightInPixels &&
|
||||
bounds.width === widthInAppUnits &&
|
||||
bounds.height === heightInAppUnits
|
||||
@@ -476,13 +493,15 @@ export namespace WebGL {
|
||||
this.setViewport(0, 0, widthInPixels, heightInPixels)
|
||||
this._width = widthInPixels
|
||||
this._height = heightInPixels
|
||||
|
||||
this.resizeEventHandlers.forEach(cb => cb())
|
||||
}
|
||||
|
||||
clear(color: Graphics.Color) {
|
||||
this._updateRenderTargetAndViewport()
|
||||
this._updateBlendState()
|
||||
|
||||
if (color != this._currentClearColor) {
|
||||
if (!color.equals(this._currentClearColor)) {
|
||||
this._gl.clearColor(color.redF, color.greenF, color.blueF, color.alphaF)
|
||||
this._currentClearColor = color
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import {Color} from '../lib/color'
|
||||
import {AffineTransform, Rect} from '../lib/math'
|
||||
import {Theme} from '../views/themes/theme'
|
||||
import {Graphics} from './graphics'
|
||||
import {setUniformAffineTransform, setUniformVec2} from './utils'
|
||||
|
||||
@@ -18,53 +20,57 @@ const vert = `
|
||||
}
|
||||
`
|
||||
|
||||
const frag = `
|
||||
precision mediump float;
|
||||
const frag = (theme: Theme) => {
|
||||
const {r, g, b} = Color.fromCSSHex(theme.fgSecondaryColor)
|
||||
const rgb = `${r.toFixed(1)}, ${g.toFixed(1)}, ${b.toFixed(1)}`
|
||||
return `
|
||||
precision mediump float;
|
||||
|
||||
uniform mat3 configSpaceToPhysicalViewSpace;
|
||||
uniform vec2 physicalSize;
|
||||
uniform vec2 physicalOrigin;
|
||||
uniform vec2 configSpaceViewportOrigin;
|
||||
uniform vec2 configSpaceViewportSize;
|
||||
uniform float framebufferHeight;
|
||||
uniform mat3 configSpaceToPhysicalViewSpace;
|
||||
uniform vec2 physicalSize;
|
||||
uniform vec2 physicalOrigin;
|
||||
uniform vec2 configSpaceViewportOrigin;
|
||||
uniform vec2 configSpaceViewportSize;
|
||||
uniform float framebufferHeight;
|
||||
|
||||
void main() {
|
||||
vec2 origin = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportOrigin, 1.0)).xy;
|
||||
vec2 size = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportSize, 0.0)).xy;
|
||||
void main() {
|
||||
vec2 origin = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportOrigin, 1.0)).xy;
|
||||
vec2 size = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportSize, 0.0)).xy;
|
||||
|
||||
vec2 halfSize = physicalSize / 2.0;
|
||||
vec2 halfSize = physicalSize / 2.0;
|
||||
|
||||
float borderWidth = 2.0;
|
||||
float borderWidth = 2.0;
|
||||
|
||||
origin = floor(origin * halfSize) / halfSize + borderWidth * vec2(1.0, 1.0);
|
||||
size = floor(size * halfSize) / halfSize - 2.0 * borderWidth * vec2(1.0, 1.0);
|
||||
origin = floor(origin * halfSize) / halfSize + borderWidth * vec2(1.0, 1.0);
|
||||
size = floor(size * halfSize) / halfSize - 2.0 * borderWidth * vec2(1.0, 1.0);
|
||||
|
||||
vec2 coord = gl_FragCoord.xy;
|
||||
coord.x = coord.x - physicalOrigin.x;
|
||||
coord.y = framebufferHeight - coord.y - physicalOrigin.y;
|
||||
vec2 clamped = clamp(coord, origin, origin + size);
|
||||
vec2 gap = clamped - coord;
|
||||
float maxdist = max(abs(gap.x), abs(gap.y));
|
||||
vec2 coord = gl_FragCoord.xy;
|
||||
coord.x = coord.x - physicalOrigin.x;
|
||||
coord.y = framebufferHeight - coord.y - physicalOrigin.y;
|
||||
vec2 clamped = clamp(coord, origin, origin + size);
|
||||
vec2 gap = clamped - coord;
|
||||
float maxdist = max(abs(gap.x), abs(gap.y));
|
||||
|
||||
// TOOD(jlfwong): Could probably optimize this to use mix somehow.
|
||||
if (maxdist == 0.0) {
|
||||
// Inside viewport rectangle
|
||||
gl_FragColor = vec4(0, 0, 0, 0);
|
||||
} else if (maxdist < borderWidth) {
|
||||
// Inside viewport rectangle at border
|
||||
gl_FragColor = vec4(0.7, 0.7, 0.7, 0.8);
|
||||
} else {
|
||||
// Outside viewport rectangle
|
||||
gl_FragColor = vec4(0.7, 0.7, 0.7, 0.5);
|
||||
// TOOD(jlfwong): Could probably optimize this to use mix somehow.
|
||||
if (maxdist == 0.0) {
|
||||
// Inside viewport rectangle
|
||||
gl_FragColor = vec4(0, 0, 0, 0);
|
||||
} else if (maxdist < borderWidth) {
|
||||
// Inside viewport rectangle at border
|
||||
gl_FragColor = vec4(${rgb}, 0.8);
|
||||
} else {
|
||||
// Outside viewport rectangle
|
||||
gl_FragColor = vec4(${rgb}, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`
|
||||
}
|
||||
|
||||
export class ViewportRectangleRenderer {
|
||||
private material: Graphics.Material
|
||||
private buffer: Graphics.VertexBuffer
|
||||
|
||||
constructor(private gl: Graphics.Context) {
|
||||
constructor(private gl: Graphics.Context, theme: Theme) {
|
||||
const vertices = [
|
||||
[-1, 1],
|
||||
[1, 1],
|
||||
@@ -78,7 +84,7 @@ export class ViewportRectangleRenderer {
|
||||
}
|
||||
this.buffer = gl.createVertexBuffer(vertexFormat.stride * vertices.length)
|
||||
this.buffer.upload(new Uint8Array(new Float32Array(floats).buffer))
|
||||
this.material = gl.createMaterial(vertexFormat, vert, frag)
|
||||
this.material = gl.createMaterial(vertexFormat, vert, frag(theme))
|
||||
}
|
||||
|
||||
render(props: ViewportRectangleRendererProps) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -64,17 +64,17 @@ exports[`importFromFirefox ignore self-hosted 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "http://localhost:8000/simple.js:1",
|
||||
"col": 15,
|
||||
"file": "http://localhost:8000/simple.js",
|
||||
"key": "alpha (http://localhost:8000/simple.js:1:14)",
|
||||
"line": 14,
|
||||
"line": 1,
|
||||
"name": "alpha",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 26.983816999942064,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "http://localhost:8000/simple.js:14",
|
||||
"col": 15,
|
||||
"file": "http://localhost:8000/simple.js",
|
||||
"key": "delta (http://localhost:8000/simple.js:14:14)",
|
||||
"line": 14,
|
||||
"name": "delta",
|
||||
@@ -82,19 +82,19 @@ Object {
|
||||
"totalWeight": 11.946324001066387,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "http://localhost:8000/simple.js:20",
|
||||
"col": 15,
|
||||
"file": "http://localhost:8000/simple.js",
|
||||
"key": "gamma (http://localhost:8000/simple.js:20:14)",
|
||||
"line": 14,
|
||||
"line": 20,
|
||||
"name": "gamma",
|
||||
"selfWeight": 26.983816999942064,
|
||||
"totalWeight": 26.983816999942064,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "http://localhost:8000/simple.js:8",
|
||||
"col": 14,
|
||||
"file": "http://localhost:8000/simple.js",
|
||||
"key": "beta (http://localhost:8000/simple.js:8:13)",
|
||||
"line": 13,
|
||||
"line": 8,
|
||||
"name": "beta",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 15.037492998875678,
|
||||
|
||||
@@ -1,5 +1,142 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`importTraceEvents BEX interaction 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "A",
|
||||
"line": undefined,
|
||||
"name": "A",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "B",
|
||||
"line": undefined,
|
||||
"name": "B",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "D",
|
||||
"line": undefined,
|
||||
"name": "D",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "C",
|
||||
"line": undefined,
|
||||
"name": "C",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "F",
|
||||
"line": undefined,
|
||||
"name": "F",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "E",
|
||||
"line": undefined,
|
||||
"name": "E",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "G",
|
||||
"line": undefined,
|
||||
"name": "G",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "H",
|
||||
"line": undefined,
|
||||
"name": "H",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "J",
|
||||
"line": undefined,
|
||||
"name": "J",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "I",
|
||||
"line": undefined,
|
||||
"name": "I",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "K",
|
||||
"line": undefined,
|
||||
"name": "K",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "L",
|
||||
"line": undefined,
|
||||
"name": "L",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"A;B 2.00µs",
|
||||
" 8.00µs",
|
||||
"D;C 2.00µs",
|
||||
" 8.00µs",
|
||||
"F;E 1.00µs",
|
||||
"F 1.00µs",
|
||||
" 8.00µs",
|
||||
"G;H 1.00µs",
|
||||
"G 1.00µs",
|
||||
" 8.00µs",
|
||||
"J 1.00µs",
|
||||
"J;I 1.00µs",
|
||||
" 8.00µs",
|
||||
"K 1.00µs",
|
||||
"K;L 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents BEX interaction: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents BEX interaction: profileGroup.name 1`] = `"bex-interaction.json"`;
|
||||
|
||||
exports[`importTraceEvents bad E events 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
@@ -35,6 +172,74 @@ exports[`importTraceEvents bad E events: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents bad E events: profileGroup.name 1`] = `"too-many-end-events.json"`;
|
||||
|
||||
exports[`importTraceEvents end event with empty stack 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "alpha",
|
||||
"line": undefined,
|
||||
"name": "alpha",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"alpha 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents end event with empty stack: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents end event with empty stack: profileGroup.name 1`] = `"end-event-with-empty-stack.json"`;
|
||||
|
||||
exports[`importTraceEvents end-non-top-of-stack 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "A {\\"x\\":1}",
|
||||
"line": undefined,
|
||||
"name": "A {\\"x\\":1}",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 11,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "B {\\"x\\":2}",
|
||||
"line": undefined,
|
||||
"name": "B {\\"x\\":2}",
|
||||
"selfWeight": 9,
|
||||
"totalWeight": 10,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "Z {\\"x\\":1}",
|
||||
"line": undefined,
|
||||
"name": "Z {\\"x\\":1}",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"A {\\"x\\":1} 1.00µs",
|
||||
"A {\\"x\\":1};B {\\"x\\":2} 9.00µs",
|
||||
"A {\\"x\\":1};B {\\"x\\":2};Z {\\"x\\":1} 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents end-non-top-of-stack: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents end-non-top-of-stack: profileGroup.name 1`] = `"end-non-top-of-stack.json"`;
|
||||
|
||||
exports[`importTraceEvents event re-ordering 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
@@ -93,6 +298,176 @@ exports[`importTraceEvents event re-ordering: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents event re-ordering: profileGroup.name 1`] = `"must-retain-original-order.json"`;
|
||||
|
||||
exports[`importTraceEvents event reordering name match 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "alpha {\\"x\\":0}",
|
||||
"line": undefined,
|
||||
"name": "alpha {\\"x\\":0}",
|
||||
"selfWeight": 9,
|
||||
"totalWeight": 10,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "beta {\\"x\\":0}",
|
||||
"line": undefined,
|
||||
"name": "beta {\\"x\\":0}",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "gamma {\\"x\\":0}",
|
||||
"line": undefined,
|
||||
"name": "gamma {\\"x\\":0}",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 1",
|
||||
"stacks": Array [
|
||||
"alpha {\\"x\\":0} 1.00µs",
|
||||
"alpha {\\"x\\":0};beta {\\"x\\":0};gamma {\\"x\\":0} 1.00µs",
|
||||
"alpha {\\"x\\":0} 8.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents event reordering name match: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents event reordering name match: profileGroup.name 1`] = `"event-reordering-name-match.json"`;
|
||||
|
||||
exports[`importTraceEvents invalid x nesting 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "alpha",
|
||||
"line": undefined,
|
||||
"name": "alpha",
|
||||
"selfWeight": 5,
|
||||
"totalWeight": 15,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "beta",
|
||||
"line": undefined,
|
||||
"name": "beta",
|
||||
"selfWeight": 10,
|
||||
"totalWeight": 10,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"alpha 5.00µs",
|
||||
"alpha;beta 10.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents invalid x nesting: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents invalid x nesting: profileGroup.name 1`] = `"invalid-x-nesting.json"`;
|
||||
|
||||
exports[`importTraceEvents matching x 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "alpha",
|
||||
"line": undefined,
|
||||
"name": "alpha",
|
||||
"selfWeight": 8,
|
||||
"totalWeight": 10,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "beta",
|
||||
"line": undefined,
|
||||
"name": "beta",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "gamma",
|
||||
"line": undefined,
|
||||
"name": "gamma",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"alpha 1.00µs",
|
||||
"alpha;beta;gamma 1.00µs",
|
||||
"alpha 3.00µs",
|
||||
"alpha;gamma;beta 1.00µs",
|
||||
"alpha 4.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents matching x: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents matching x: profileGroup.name 1`] = `"matching-x.json"`;
|
||||
|
||||
exports[`importTraceEvents mismatched args 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "A {\\"x\\":1}",
|
||||
"line": undefined,
|
||||
"name": "A {\\"x\\":1}",
|
||||
"selfWeight": 10,
|
||||
"totalWeight": 10,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"A {\\"x\\":1} 10.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents mismatched args: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents mismatched args: profileGroup.name 1`] = `"mismatched-args.json"`;
|
||||
|
||||
exports[`importTraceEvents mismatched name 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "alpha",
|
||||
"line": undefined,
|
||||
"name": "alpha",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 0,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents mismatched name: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents mismatched name: profileGroup.name 1`] = `"mismatched-name.json"`;
|
||||
|
||||
exports[`importTraceEvents multiprocess 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
@@ -257,6 +632,164 @@ exports[`importTraceEvents multiprocess: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents multiprocess: profileGroup.name 1`] = `"multiprocess.json"`;
|
||||
|
||||
exports[`importTraceEvents not enough end events 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "A {\\"x\\":1}",
|
||||
"line": undefined,
|
||||
"name": "A {\\"x\\":1}",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 11,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "A {\\"x\\":2}",
|
||||
"line": undefined,
|
||||
"name": "A {\\"x\\":2}",
|
||||
"selfWeight": 9,
|
||||
"totalWeight": 10,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"A {\\"x\\":1} 1.00µs",
|
||||
"A {\\"x\\":1};A {\\"x\\":2} 9.00µs",
|
||||
"A {\\"x\\":1};A {\\"x\\":2};A {\\"x\\":1} 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents not enough end events: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents not enough end events: profileGroup.name 1`] = `"not-enough-end-events.json"`;
|
||||
|
||||
exports[`importTraceEvents not out-of-order 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "A",
|
||||
"line": undefined,
|
||||
"name": "A",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 10,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "B",
|
||||
"line": undefined,
|
||||
"name": "B",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 8,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "C",
|
||||
"line": undefined,
|
||||
"name": "C",
|
||||
"selfWeight": 6,
|
||||
"totalWeight": 6,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"A 1.00µs",
|
||||
"A;B 1.00µs",
|
||||
"A;B;C 6.00µs",
|
||||
"A;B 1.00µs",
|
||||
"A 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents not out-of-order unbalanced name 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "alpha",
|
||||
"line": undefined,
|
||||
"name": "alpha",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 10,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "beta",
|
||||
"line": undefined,
|
||||
"name": "beta",
|
||||
"selfWeight": 9,
|
||||
"totalWeight": 9,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"alpha 1.00µs",
|
||||
"alpha;beta 9.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents not out-of-order unbalanced name: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents not out-of-order unbalanced name: profileGroup.name 1`] = `"out-of-order-unbalanced-name.json"`;
|
||||
|
||||
exports[`importTraceEvents not out-of-order: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents not out-of-order: profileGroup.name 1`] = `"out-of-order.json"`;
|
||||
|
||||
exports[`importTraceEvents only begin events 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "A {\\"x\\":1}",
|
||||
"line": undefined,
|
||||
"name": "A {\\"x\\":1}",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 2,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "B {\\"x\\":2}",
|
||||
"line": undefined,
|
||||
"name": "B {\\"x\\":2}",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 1,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "C {\\"x\\":2}",
|
||||
"line": undefined,
|
||||
"name": "C {\\"x\\":2}",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 0,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"A {\\"x\\":1} 1.00µs",
|
||||
"A {\\"x\\":1};B {\\"x\\":2} 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents only begin events: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents only begin events: profileGroup.name 1`] = `"only-begin-events.json"`;
|
||||
|
||||
exports[`importTraceEvents partial json import 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
@@ -526,3 +1059,231 @@ exports[`importTraceEvents simple object: profileGroup.name 1`] = `"simple-objec
|
||||
exports[`importTraceEvents simple: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents simple: profileGroup.name 1`] = `"simple.json"`;
|
||||
|
||||
exports[`importTraceEvents unbalanced args 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "A {\\"x\\":1}",
|
||||
"line": undefined,
|
||||
"name": "A {\\"x\\":1}",
|
||||
"selfWeight": 2,
|
||||
"totalWeight": 10,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "A {\\"x\\":2}",
|
||||
"line": undefined,
|
||||
"name": "A {\\"x\\":2}",
|
||||
"selfWeight": 8,
|
||||
"totalWeight": 8,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"A {\\"x\\":1} 1.00µs",
|
||||
"A {\\"x\\":1};A {\\"x\\":2} 8.00µs",
|
||||
"A {\\"x\\":1} 1.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents unbalanced args: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents unbalanced args: profileGroup.name 1`] = `"unbalanced-args.json"`;
|
||||
|
||||
exports[`importTraceEvents unbalanced name 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "alpha",
|
||||
"line": undefined,
|
||||
"name": "alpha",
|
||||
"selfWeight": 1,
|
||||
"totalWeight": 10,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "beta",
|
||||
"line": undefined,
|
||||
"name": "beta",
|
||||
"selfWeight": 9,
|
||||
"totalWeight": 9,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"alpha 1.00µs",
|
||||
"alpha;beta 9.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents unbalanced name: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents unbalanced name: profileGroup.name 1`] = `"unbalanced-name.json"`;
|
||||
|
||||
exports[`importTraceEvents x events matching end 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "alpha",
|
||||
"line": undefined,
|
||||
"name": "alpha",
|
||||
"selfWeight": 12,
|
||||
"totalWeight": 20,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "beta",
|
||||
"line": undefined,
|
||||
"name": "beta",
|
||||
"selfWeight": 4,
|
||||
"totalWeight": 6,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "gamma",
|
||||
"line": undefined,
|
||||
"name": "gamma",
|
||||
"selfWeight": 4,
|
||||
"totalWeight": 6,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"alpha 1.00µs",
|
||||
"alpha;beta 1.00µs",
|
||||
"alpha;beta;gamma 1.00µs",
|
||||
"alpha 2.00µs",
|
||||
"alpha;gamma 1.00µs",
|
||||
"alpha;gamma;beta 1.00µs",
|
||||
"alpha 2.00µs",
|
||||
"alpha;beta 1.00µs",
|
||||
"alpha;beta;gamma 1.00µs",
|
||||
"alpha 3.00µs",
|
||||
"alpha;gamma 1.00µs",
|
||||
"alpha;gamma;beta 1.00µs",
|
||||
"alpha 4.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents x events matching end: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents x events matching end: profileGroup.name 1`] = `"x-events-matching-end.json"`;
|
||||
|
||||
exports[`importTraceEvents x events matching start 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "alpha",
|
||||
"line": undefined,
|
||||
"name": "alpha",
|
||||
"selfWeight": 12,
|
||||
"totalWeight": 20,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "beta",
|
||||
"line": undefined,
|
||||
"name": "beta",
|
||||
"selfWeight": 4,
|
||||
"totalWeight": 6,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "gamma",
|
||||
"line": undefined,
|
||||
"name": "gamma",
|
||||
"selfWeight": 4,
|
||||
"totalWeight": 6,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"alpha 1.00µs",
|
||||
"alpha;beta;gamma 1.00µs",
|
||||
"alpha;beta 1.00µs",
|
||||
"alpha 2.00µs",
|
||||
"alpha;gamma;beta 1.00µs",
|
||||
"alpha;gamma 1.00µs",
|
||||
"alpha 2.00µs",
|
||||
"alpha;gamma;beta 1.00µs",
|
||||
"alpha;gamma 1.00µs",
|
||||
"alpha 2.00µs",
|
||||
"alpha;beta;gamma 1.00µs",
|
||||
"alpha;beta 1.00µs",
|
||||
"alpha 5.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents x events matching start: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents x events matching start: profileGroup.name 1`] = `"x-events-matching-start.json"`;
|
||||
|
||||
exports[`importTraceEvents zero duration events 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "A",
|
||||
"line": undefined,
|
||||
"name": "A",
|
||||
"selfWeight": 10,
|
||||
"totalWeight": 10,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "B",
|
||||
"line": undefined,
|
||||
"name": "B",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 0,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "C",
|
||||
"line": undefined,
|
||||
"name": "C",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 0,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": "D",
|
||||
"line": undefined,
|
||||
"name": "D",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 0,
|
||||
},
|
||||
],
|
||||
"name": "pid 0, tid 0",
|
||||
"stacks": Array [
|
||||
"A 10.00µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importTraceEvents zero duration events: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importTraceEvents zero duration events: profileGroup.name 1`] = `"zero-duration-events.json"`;
|
||||
|
||||
+11
-2
@@ -170,8 +170,17 @@ function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
|
||||
return getOrInsert(callFrameToFrameInfo, callFrame, callFrame => {
|
||||
const name = callFrame.functionName || '(anonymous)'
|
||||
const file = callFrame.url
|
||||
const line = callFrame.lineNumber
|
||||
const col = callFrame.columnNumber
|
||||
|
||||
// In Chrome profiles, line numbers & column numbers are both 0-indexed.
|
||||
//
|
||||
// We're going to normalize these to be 1-based to avoid needing to normalize
|
||||
// these at the presentation layer.
|
||||
let line = callFrame.lineNumber
|
||||
if (line != null) line++
|
||||
|
||||
let col = callFrame.columnNumber
|
||||
if (col != null) col++
|
||||
|
||||
return {
|
||||
key: `${name}:${file}:${line}:${col}`,
|
||||
name,
|
||||
|
||||
@@ -176,7 +176,7 @@ export function importFromFirefox(firefoxProfile: FirefoxProfile): Profile {
|
||||
const frameData = thread.frameTable.data[f]
|
||||
const location = thread.stringTable[frameData[0]]
|
||||
|
||||
const match = /(.*)\s+\((.*?):?(\d+)?\)$/.exec(location)
|
||||
const match = /(.*)\s+\((.*?)(?::(\d+))?(?::(\d+))?\)$/.exec(location)
|
||||
|
||||
if (!match) return null
|
||||
|
||||
@@ -193,7 +193,11 @@ export function importFromFirefox(firefoxProfile: FirefoxProfile): Profile {
|
||||
key: location,
|
||||
name: match[1]!,
|
||||
file: match[2]!,
|
||||
|
||||
// In Firefox profiles, line numbers are 1-based, but columns are
|
||||
// 0-based. Let's normalize both to be 1-based.
|
||||
line: match[3] ? parseInt(match[3]) : undefined,
|
||||
col: match[4] ? parseInt(match[4]) + 1 : undefined,
|
||||
}))
|
||||
})
|
||||
.filter(f => f != null) as FrameInfo[]
|
||||
|
||||
@@ -31,3 +31,71 @@ test('importTraceEvents bad E events', async () => {
|
||||
test('importTraceEvents event re-ordering', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/must-retain-original-order.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents end-non-top-of-stack', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/end-non-top-of-stack.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents mismatched args', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/mismatched-args.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents mismatched name', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/mismatched-name.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents not enough end events', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/not-enough-end-events.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents not out-of-order unbalanced name', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/out-of-order-unbalanced-name.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents not out-of-order', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/out-of-order.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents unbalanced name', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/unbalanced-name.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents unbalanced args', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/unbalanced-args.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents end event with empty stack', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/end-event-with-empty-stack.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents only begin events', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/only-begin-events.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents zero duration events', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/zero-duration-events.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents matching x', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/matching-x.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents x events matching start', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/x-events-matching-start.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents x events matching end', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/x-events-matching-end.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents BEX interaction', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/bex-interaction.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents invalid x nesting', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/invalid-x-nesting.json')
|
||||
})
|
||||
|
||||
test('importTraceEvents event reordering name match', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/trace-event/event-reordering-name-match.json')
|
||||
})
|
||||
|
||||
+281
-118
@@ -1,5 +1,5 @@
|
||||
import {sortBy, zeroPad, lastOf} from '../lib/utils'
|
||||
import {ProfileGroup, CallTreeProfileBuilder, FrameInfo} from '../lib/profile'
|
||||
import {sortBy, zeroPad, getOrInsert, lastOf} from '../lib/utils'
|
||||
import {ProfileGroup, CallTreeProfileBuilder, FrameInfo, Profile} from '../lib/profile'
|
||||
import {TimeFormatter} from '../lib/value-formatters'
|
||||
|
||||
// This file concerns import from the "Trace Event Format", authored by Google
|
||||
@@ -64,7 +64,135 @@ interface XTraceEvent extends TraceEvent {
|
||||
|
||||
// The trace format supports a number of event types that we ignore.
|
||||
type ImportableTraceEvent = BTraceEvent | ETraceEvent | XTraceEvent
|
||||
type DurationEvent = BTraceEvent | ETraceEvent
|
||||
|
||||
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[]>()
|
||||
|
||||
for (let ev of events) {
|
||||
const list = getOrInsert(map, pidTidKey(ev.pid, ev.tid), () => [])
|
||||
list.push(ev)
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
function selectQueueToTakeFromNext(
|
||||
bEventQueue: BTraceEvent[],
|
||||
eEventQueue: ETraceEvent[],
|
||||
): 'B' | 'E' {
|
||||
if (bEventQueue.length === 0 && eEventQueue.length === 0) {
|
||||
throw new Error('This method should not be given both queues empty')
|
||||
}
|
||||
if (eEventQueue.length === 0) return 'B'
|
||||
if (bEventQueue.length === 0) return 'E'
|
||||
|
||||
const bFront = bEventQueue[0]
|
||||
const eFront = eEventQueue[0]
|
||||
|
||||
const bts = bFront.ts
|
||||
const ets = eFront.ts
|
||||
|
||||
if (bts < ets) return 'B'
|
||||
if (ets < bts) return 'E'
|
||||
|
||||
// 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,
|
||||
// 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'
|
||||
}
|
||||
|
||||
function convertToEventQueues(events: ImportableTraceEvent[]): [BTraceEvent[], ETraceEvent[]] {
|
||||
const beginEvents: BTraceEvent[] = []
|
||||
const endEvents: ETraceEvent[] = []
|
||||
|
||||
// Rebase all of the timestamps on the lowest timestamp
|
||||
if (events.length > 0) {
|
||||
let firstTs = Number.MAX_SAFE_INTEGER
|
||||
for (let ev of events) {
|
||||
firstTs = Math.min(firstTs, ev.ts)
|
||||
}
|
||||
for (let ev of events) {
|
||||
ev.ts -= firstTs
|
||||
}
|
||||
}
|
||||
|
||||
// Next, combine B, E, and X events into two timestamp ordered queues.
|
||||
const xEvents: XTraceEvent[] = []
|
||||
for (let ev of events) {
|
||||
switch (ev.ph) {
|
||||
case 'B': {
|
||||
beginEvents.push(ev)
|
||||
break
|
||||
}
|
||||
|
||||
case 'E': {
|
||||
endEvents.push(ev)
|
||||
break
|
||||
}
|
||||
|
||||
case 'X': {
|
||||
xEvents.push(ev)
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
const _exhaustiveCheck: never = ev
|
||||
return _exhaustiveCheck
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dur(x: XTraceEvent): number {
|
||||
return x.dur ?? x.tdur ?? 0
|
||||
}
|
||||
|
||||
xEvents.sort((a, b) => {
|
||||
if (a.ts < b.ts) return -1
|
||||
if (a.ts > b.ts) return 1
|
||||
|
||||
// Super weird special case: if we have two 'X' events with the same 'ts'
|
||||
// but different 'dur' the only valid interpretation is to put the one with
|
||||
// the longer 'dur' first, because you can't nest longer things in shorter
|
||||
// things.
|
||||
const aDur = dur(a)
|
||||
const bDur = dur(b)
|
||||
if (aDur > bDur) return -1
|
||||
if (aDur < bDur) return 1
|
||||
|
||||
// Otherwise, retain the original order by relying upon a stable sort here.
|
||||
return 0
|
||||
})
|
||||
|
||||
for (let x of xEvents) {
|
||||
const xDur = dur(x)
|
||||
beginEvents.push({...x, ph: 'B'} as BTraceEvent)
|
||||
endEvents.push({...x, ph: 'E', ts: x.ts + xDur} as ETraceEvent)
|
||||
}
|
||||
|
||||
function compareTimestamps(a: TraceEvent, b: TraceEvent) {
|
||||
if (a.ts < b.ts) return -1
|
||||
if (a.ts > b.ts) return 1
|
||||
|
||||
// Important: if the timestamps are the same, return zero. We're going to
|
||||
// rely upon a stable sort here.
|
||||
return 0
|
||||
}
|
||||
|
||||
beginEvents.sort(compareTimestamps)
|
||||
endEvents.sort(compareTimestamps)
|
||||
|
||||
return [beginEvents, endEvents]
|
||||
}
|
||||
|
||||
function filterIgnoredEventTypes(events: TraceEvent[]): ImportableTraceEvent[] {
|
||||
const ret: ImportableTraceEvent[] = []
|
||||
@@ -79,40 +207,6 @@ function filterIgnoredEventTypes(events: TraceEvent[]): ImportableTraceEvent[] {
|
||||
return ret
|
||||
}
|
||||
|
||||
function convertToDurationEvents(events: ImportableTraceEvent[]): DurationEvent[] {
|
||||
const ret: DurationEvent[] = []
|
||||
for (let ev of events) {
|
||||
switch (ev.ph) {
|
||||
case 'B':
|
||||
ret.push(ev)
|
||||
break
|
||||
|
||||
case 'E':
|
||||
ret.push(ev)
|
||||
break
|
||||
|
||||
case 'X':
|
||||
let dur: number | null = null
|
||||
if (ev.dur != null) dur = ev.dur
|
||||
else if (ev.tdur != null) dur = ev.tdur
|
||||
|
||||
if (dur == null) {
|
||||
console.warn('Found a complete event (X) with no duration. Skipping: ', ev)
|
||||
continue
|
||||
}
|
||||
|
||||
ret.push({...ev, ph: 'B'} as BTraceEvent)
|
||||
ret.push({...ev, ph: 'E', ts: ev.ts + dur} as ETraceEvent)
|
||||
break
|
||||
|
||||
default:
|
||||
const _exhaustiveCheck: never = ev
|
||||
return _exhaustiveCheck
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
function getProcessNamesByPid(events: TraceEvent[]): Map<number, string> {
|
||||
const processNamesByPid = new Map<number, string>()
|
||||
for (let ev of events) {
|
||||
@@ -128,8 +222,7 @@ function getThreadNamesByPidTid(events: TraceEvent[]): Map<string, string> {
|
||||
|
||||
for (let ev of events) {
|
||||
if (ev.ph === 'M' && ev.name === 'thread_name' && ev.args && ev.args.name) {
|
||||
const key = `${ev.pid}:${ev.tid}`
|
||||
threadNameByPidTid.set(key, ev.args.name)
|
||||
threadNameByPidTid.set(pidTidKey(ev.pid, ev.tid), ev.args.name)
|
||||
}
|
||||
}
|
||||
return threadNameByPidTid
|
||||
@@ -143,66 +236,33 @@ function keyForEvent(event: TraceEvent): string {
|
||||
return name
|
||||
}
|
||||
|
||||
type TraceEventProfileState = {profile: CallTreeProfileBuilder; eventStack: BTraceEvent[]}
|
||||
function frameInfoForEvent(event: TraceEvent): FrameInfo {
|
||||
const key = keyForEvent(event)
|
||||
return {
|
||||
name: key,
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
||||
function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
|
||||
const stateByPidTid = new Map<string, TraceEventProfileState>()
|
||||
|
||||
const importableEvents = filterIgnoredEventTypes(events)
|
||||
const durationEvents = convertToDurationEvents(importableEvents)
|
||||
const partitioned = partitionByPidTid(importableEvents)
|
||||
|
||||
const processNamesByPid = getProcessNamesByPid(events)
|
||||
const threadNamesByPidTid = getThreadNamesByPidTid(events)
|
||||
|
||||
durationEvents.sort((a, b) => {
|
||||
if (a.ts < b.ts) return -1
|
||||
if (a.ts > b.ts) return 1
|
||||
if (a.pid < b.pid) return -1
|
||||
if (a.pid > b.pid) return 1
|
||||
if (a.tid < b.tid) return -1
|
||||
if (a.tid > b.tid) return 1
|
||||
const profilePairs: [string, Profile][] = []
|
||||
|
||||
// We have to be careful with events that have the same timestamp
|
||||
// and the same pid/tid
|
||||
const aKey = keyForEvent(a)
|
||||
const bKey = keyForEvent(b)
|
||||
if (aKey === bKey) {
|
||||
// If the two elements have the same key, we need to process the begin
|
||||
// event before the end event. This will be a zero-duration event.
|
||||
if (a.ph === 'B' && b.ph === 'E') return -1
|
||||
if (a.ph === 'E' && b.ph === 'B') return 1
|
||||
} else {
|
||||
// If the two elements have *different* keys, we want to process
|
||||
// the end of an event before the beginning of the event to prevent
|
||||
// out-of-order push/pops from the call-stack.
|
||||
if (a.ph === 'B' && b.ph === 'E') return 1
|
||||
if (a.ph === 'E' && b.ph === 'B') return -1
|
||||
}
|
||||
partitioned.forEach(eventsForThread => {
|
||||
if (eventsForThread.length === 0) return
|
||||
|
||||
// In all other cases, retain the original sort order.
|
||||
return 0
|
||||
})
|
||||
const {pid, tid} = eventsForThread[0]
|
||||
|
||||
if (durationEvents.length > 0) {
|
||||
const firstTs = durationEvents[0].ts
|
||||
for (let ev of durationEvents) {
|
||||
ev.ts -= firstTs
|
||||
}
|
||||
}
|
||||
|
||||
function getOrCreateProfileState(pid: number, tid: number): TraceEventProfileState {
|
||||
// We zero-pad the PID and TID to make sorting them by pid/tid pair later easier.
|
||||
const pidTid = `${zeroPad('' + pid, 10)}:${zeroPad('' + tid, 10)}`
|
||||
|
||||
let state = stateByPidTid.get(pidTid)
|
||||
if (state != null) return state
|
||||
let profile = new CallTreeProfileBuilder()
|
||||
state = {profile, eventStack: []}
|
||||
const profile = new CallTreeProfileBuilder()
|
||||
profile.setValueFormatter(new TimeFormatter('microseconds'))
|
||||
stateByPidTid.set(pidTid, state)
|
||||
|
||||
const processName = processNamesByPid.get(pid)
|
||||
const threadName = threadNamesByPidTid.get(`${pid}:${tid}`)
|
||||
const threadName = threadNamesByPidTid.get(pidTidKey(pid, tid))
|
||||
|
||||
if (processName != null && threadName != null) {
|
||||
profile.setName(`${processName} (pid ${pid}), ${threadName} (tid ${tid})`)
|
||||
@@ -214,51 +274,154 @@ function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
|
||||
profile.setName(`pid ${pid}, tid ${tid}`)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
// 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)
|
||||
|
||||
for (let ev of durationEvents) {
|
||||
const {profile, eventStack} = getOrCreateProfileState(ev.pid, ev.tid)
|
||||
const key = keyForEvent(ev)
|
||||
const frameInfo: FrameInfo = {
|
||||
key: key,
|
||||
name: key,
|
||||
const frameStack: BTraceEvent[] = []
|
||||
const enterFrame = (b: BTraceEvent) => {
|
||||
frameStack.push(b)
|
||||
profile.enterFrame(frameInfoForEvent(b), b.ts)
|
||||
}
|
||||
switch (ev.ph) {
|
||||
case 'B':
|
||||
eventStack.push(ev)
|
||||
profile.enterFrame(frameInfo, ev.ts)
|
||||
break
|
||||
|
||||
case 'E':
|
||||
const lastEvent = lastOf(eventStack)
|
||||
if (lastEvent != null && lastEvent.name === ev.name) {
|
||||
profile.leaveFrame(frameInfo, ev.ts)
|
||||
eventStack.pop()
|
||||
} else {
|
||||
console.warn(
|
||||
'Event discarded because it did not match top-of-stack. Discarded event:',
|
||||
ev,
|
||||
'Top of stack:',
|
||||
lastEvent,
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
while (bEventQueue.length > 0 || eEventQueue.length > 0) {
|
||||
const queueName = selectQueueToTakeFromNext(bEventQueue, eEventQueue)
|
||||
switch (queueName) {
|
||||
case 'B': {
|
||||
enterFrame(bEventQueue.shift()!)
|
||||
break
|
||||
}
|
||||
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)
|
||||
|
||||
default:
|
||||
const _exhaustiveCheck: never = ev
|
||||
return _exhaustiveCheck
|
||||
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) {
|
||||
// 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
|
||||
}
|
||||
|
||||
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.`)
|
||||
profile.leaveFrame(frame, profile.getTotalWeight())
|
||||
}
|
||||
|
||||
profilePairs.push([pidTidKey(pid, tid), profile.build()])
|
||||
})
|
||||
|
||||
// For now, we just sort processes by pid & tid.
|
||||
// TODO: The standard specifies that metadata events with the name
|
||||
// "process_sort_index" and "thread_sort_index" can be used to influence the
|
||||
// order, but for simplicity we'll ignore that until someone complains :)
|
||||
const profilePairs = Array.from(stateByPidTid.entries())
|
||||
sortBy(profilePairs, p => p[0])
|
||||
|
||||
return {name: '', indexToView: 0, profiles: profilePairs.map(p => p[1].profile)}
|
||||
return {
|
||||
name: '',
|
||||
indexToView: 0,
|
||||
profiles: profilePairs.map(p => p[1]),
|
||||
}
|
||||
}
|
||||
|
||||
function isTraceEventList(maybeEventList: any): maybeEventList is TraceEvent[] {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`source-map remapping of chrome-85-esbuild 1`] = `
|
||||
Array [
|
||||
"((anonymous) @ alpha.ts:5:5) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:170)",
|
||||
"(e @ beta.ts:3:8) <- (e @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:75)",
|
||||
"(get floop @ kludge.ts:13:7) <- (get floop @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:276)",
|
||||
"(l constructor @ kludge.ts:4:3) <- (l @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:226)",
|
||||
"(m @ delta.ts:3:22) <- (m @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:119)",
|
||||
"(r @ alpha.ts:4:8) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:158)",
|
||||
"(gamma @ gamma.ts:1:14) <- (t @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:15)",
|
||||
"(zap @ kludge.ts:9:3) <- (zap @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/esbuild/typescript-source-map-test.js:1:260)",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`source-map remapping of chrome-85-parcel 1`] = `
|
||||
Array [
|
||||
"((anonymous) @ ../alpha.ts:5:5) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:8:152)",
|
||||
"(e @ ../gamma.ts:1:22) <- (e @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:2:104)",
|
||||
"(get @ ../kludge.ts:13:3) <- (get @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:10:284)",
|
||||
"(o.zap @ ../kludge.ts:9:3) <- (o.zap @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:10:211)",
|
||||
"(o constructor @ ../kludge.ts:4:3) <- (o @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:10:139)",
|
||||
"(parcelRequire.CIJJ../alpha @ ../alpha.ts:5:5) <- (parcelRequire.CIJJ../alpha @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:9:55)",
|
||||
"(parcelRequire.EJAe../gamma @ ../gamma.ts:1:8) <- (parcelRequire.EJAe../gamma @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:3:23)",
|
||||
"(parcelRequire.NDR3../beta @ ../delta.ts:3:8) <- (parcelRequire.NDR3../beta @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:7:39)",
|
||||
"(parcelRequire.xEzo../kludge @ ../kludge.ts:3:1) <- (parcelRequire.xEzo../kludge @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:11:39)",
|
||||
"(beta @ ../beta.ts:3:17) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:4:119)",
|
||||
"(r @ ../delta.ts:3:22) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:6:125)",
|
||||
"(alpha @ ../alpha.ts:4:17) <- (t @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/parcel/typescript-source-map-test.js:8:140)",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`source-map remapping of chrome-85-webpack 1`] = `
|
||||
Array [
|
||||
"((anonymous) @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
|
||||
"(get @ webpack://speedscope-sourcemap-test-project/kludge.ts:13:3) <- (get @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:368)",
|
||||
"(n.zap @ webpack://speedscope-sourcemap-test-project/kludge.ts:9:3) <- (n.zap @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:305)",
|
||||
"(n constructor @ webpack://speedscope-sourcemap-test-project/kludge.ts:4:3) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:243)",
|
||||
"(n @ webpack://speedscope-sourcemap-test-project/gamma.ts:1:22) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:34)",
|
||||
"(beta @ webpack://speedscope-sourcemap-test-project/beta.ts:3:17) <- (o @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:88)",
|
||||
"(r @ webpack://speedscope-sourcemap-test-project/delta.ts:3:22) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:130)",
|
||||
"(alpha @ webpack://speedscope-sourcemap-test-project/alpha.ts:4:17) <- (t @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:169)",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`source-map remapping of firefox-79-webpack 1`] = `
|
||||
Array [
|
||||
"(get @ webpack://speedscope-sourcemap-test-project/kludge.ts:13:3) <- (get @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:368)",
|
||||
"(n constructor @ webpack://speedscope-sourcemap-test-project/kludge.ts:4:3) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:243)",
|
||||
"(n @ webpack://speedscope-sourcemap-test-project/gamma.ts:1:22) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:34)",
|
||||
"(n.prototype.zap @ webpack://speedscope-sourcemap-test-project/kludge.ts:9:3) <- (n.prototype.zap @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:305)",
|
||||
"(beta @ webpack://speedscope-sourcemap-test-project/beta.ts:3:17) <- (o @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:88)",
|
||||
"(r @ webpack://speedscope-sourcemap-test-project/delta.ts:3:22) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:130)",
|
||||
"(alpha @ webpack://speedscope-sourcemap-test-project/alpha.ts:4:17) <- (t @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:169)",
|
||||
"(t/< @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- (t/< @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`source-map remapping of safari-13-webpack 1`] = `
|
||||
Array [
|
||||
"((anonymous) @ webpack://speedscope-sourcemap-test-project/alpha.ts:5:4) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:181)",
|
||||
"(zap @ webpack://speedscope-sourcemap-test-project/kludge.ts:9:3) <- ((anonymous) @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:305)",
|
||||
"(get @ webpack://speedscope-sourcemap-test-project/kludge.ts:13:3) <- (get @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:368)",
|
||||
"(n constructor @ webpack://speedscope-sourcemap-test-project/kludge.ts:4:3) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:243)",
|
||||
"(n @ webpack://speedscope-sourcemap-test-project/gamma.ts:1:22) <- (n @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:34)",
|
||||
"(beta @ webpack://speedscope-sourcemap-test-project/beta.ts:3:17) <- (o @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:88)",
|
||||
"(r @ webpack://speedscope-sourcemap-test-project/delta.ts:3:22) <- (r @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:130)",
|
||||
"(alpha @ webpack://speedscope-sourcemap-test-project/alpha.ts:4:17) <- (t @ file:///Users/jlfwong/code/speedscope/sample/programs/javascript/sourcemaps/dist/webpack/typescript-source-map-test.js:1:169)",
|
||||
]
|
||||
`;
|
||||
@@ -34,6 +34,23 @@ export class Color {
|
||||
return new Color(clamp(R1 + m, 0, 1), clamp(G1 + m, 0, 1), clamp(B1 + m, 0, 1), 1.0)
|
||||
}
|
||||
|
||||
static fromCSSHex(hex: string) {
|
||||
if (hex.length !== 7 || hex[0] !== '#') {
|
||||
throw new Error(`Invalid color input ${hex}`)
|
||||
}
|
||||
const r = parseInt(hex.substr(1, 2), 16) / 255
|
||||
const g = parseInt(hex.substr(3, 2), 16) / 255
|
||||
const b = parseInt(hex.substr(5, 2), 16) / 255
|
||||
if (r < 0 || r > 1 || g < 0 || g > 1 || b < 0 || b > 1) {
|
||||
throw new Error(`Invalid color input ${hex}`)
|
||||
}
|
||||
return new Color(r, g, b)
|
||||
}
|
||||
|
||||
withAlpha(a: number): Color {
|
||||
return new Color(this.r, this.g, this.b, a)
|
||||
}
|
||||
|
||||
toCSS(): string {
|
||||
return `rgba(${(255 * this.r).toFixed()}, ${(255 * this.g).toFixed()}, ${(
|
||||
255 * this.b
|
||||
|
||||
+51
-46
@@ -1,67 +1,72 @@
|
||||
import {importEmscriptenSymbolMap} from './emscripten'
|
||||
import {Frame} from './profile'
|
||||
import {KeyedSet} from './utils'
|
||||
|
||||
test('importEmscriptenSymbolMap', () => {
|
||||
function checkMap(input: string, expectedMapping: [string, string][]) {
|
||||
const mapping = importEmscriptenSymbolMap(input)
|
||||
if (mapping == null) {
|
||||
fail('Mapping failed to parse')
|
||||
return
|
||||
}
|
||||
|
||||
const frames = new KeyedSet<Frame>()
|
||||
for (let [key, value] of expectedMapping) {
|
||||
const frame = Frame.getOrInsert(frames, {key, name: key})
|
||||
expect(mapping(frame)?.name).toBe(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Valid symbol map
|
||||
expect(
|
||||
importEmscriptenSymbolMap(
|
||||
[
|
||||
/* prettier: ignore */
|
||||
'a:A',
|
||||
'b:B',
|
||||
'c:C',
|
||||
].join('\n'),
|
||||
),
|
||||
).toEqual(
|
||||
new Map([
|
||||
checkMap(
|
||||
[
|
||||
/* prettier: ignore */
|
||||
'a:A',
|
||||
'b:B',
|
||||
'c:C',
|
||||
].join('\n'),
|
||||
[
|
||||
['a', 'A'],
|
||||
['b', 'B'],
|
||||
['c', 'C'],
|
||||
]),
|
||||
],
|
||||
)
|
||||
|
||||
// Valid symbol map with trailing newline
|
||||
expect(
|
||||
importEmscriptenSymbolMap(
|
||||
[
|
||||
/* prettier: ignore */
|
||||
'a:A',
|
||||
'b:B',
|
||||
'c:C',
|
||||
'd:D-D',
|
||||
'',
|
||||
].join('\n'),
|
||||
),
|
||||
).toEqual(
|
||||
new Map([
|
||||
checkMap(
|
||||
[
|
||||
/* prettier: ignore */
|
||||
'a:A',
|
||||
'b:B',
|
||||
'c:C',
|
||||
'd:D-D',
|
||||
'',
|
||||
].join('\n'),
|
||||
[
|
||||
['a', 'A'],
|
||||
['b', 'B'],
|
||||
['c', 'C'],
|
||||
['d', 'D-D'],
|
||||
]),
|
||||
],
|
||||
)
|
||||
|
||||
// Valid symbol map with non-alpha characters
|
||||
expect(importEmscriptenSymbolMap('u6:__ZN8tinyxml210XMLCommentD0Ev\n')).toEqual(
|
||||
new Map([['u6', '__ZN8tinyxml210XMLCommentD0Ev']]),
|
||||
)
|
||||
checkMap('u6:__ZN8tinyxml210XMLCommentD0Ev\n', [['u6', '__ZN8tinyxml210XMLCommentD0Ev']])
|
||||
|
||||
// WebAssembly symbol map
|
||||
expect(
|
||||
importEmscriptenSymbolMap(
|
||||
[
|
||||
/* prettier: ignore */
|
||||
'0:A',
|
||||
'1:B',
|
||||
'2:C',
|
||||
'3:D-D',
|
||||
'4:a\\20b',
|
||||
'5:a\\2',
|
||||
'6:a\\3z',
|
||||
'7:a\\20b\\20c',
|
||||
].join('\n'),
|
||||
),
|
||||
).toEqual(
|
||||
new Map([
|
||||
checkMap(
|
||||
[
|
||||
/* prettier: ignore */
|
||||
'0:A',
|
||||
'1:B',
|
||||
'2:C',
|
||||
'3:D-D',
|
||||
'4:a\\20b',
|
||||
'5:a\\2',
|
||||
'6:a\\3z',
|
||||
'7:a\\20b\\20c',
|
||||
].join('\n'),
|
||||
[
|
||||
['wasm-function[0]', 'A'],
|
||||
['wasm-function[1]', 'B'],
|
||||
['wasm-function[2]', 'C'],
|
||||
@@ -70,7 +75,7 @@ test('importEmscriptenSymbolMap', () => {
|
||||
['wasm-function[5]', 'a\\2'],
|
||||
['wasm-function[6]', 'a\\3z'],
|
||||
['wasm-function[7]', 'a b c'],
|
||||
]),
|
||||
],
|
||||
)
|
||||
|
||||
// Invalid symbol map
|
||||
|
||||
+12
-5
@@ -1,7 +1,8 @@
|
||||
type EmscriptenSymbolMap = Map<string, string>
|
||||
|
||||
// Returns `input` with hex escapes expanded (e.g. `\20` becomes ` `.)
|
||||
//
|
||||
|
||||
import {Frame, SymbolRemapper} from './profile'
|
||||
|
||||
// NOTE: This will fail to ignore escaped backslahes (e.g. `\\20`).
|
||||
function unescapeHex(input: string): string {
|
||||
return input.replace(/\\([a-fA-F0-9]{2})/g, (_match, group) => {
|
||||
@@ -15,7 +16,7 @@ function unescapeHex(input: string): string {
|
||||
// have the associated symbol map. To do this, first drop the profile into speedscope
|
||||
// and then drop the symbol map. After the second drop, the symbols will be remapped to
|
||||
// their original names.
|
||||
export function importEmscriptenSymbolMap(contents: string): EmscriptenSymbolMap | null {
|
||||
export function importEmscriptenSymbolMap(contents: string): SymbolRemapper | null {
|
||||
const lines = contents.split('\n')
|
||||
if (!lines.length) return null
|
||||
|
||||
@@ -23,7 +24,7 @@ export function importEmscriptenSymbolMap(contents: string): EmscriptenSymbolMap
|
||||
if (lines[lines.length - 1] === '') lines.pop()
|
||||
if (!lines.length) return null
|
||||
|
||||
const map: EmscriptenSymbolMap = new Map()
|
||||
const map = new Map<string, string>()
|
||||
const intRegex = /^(\d+):(.+)$/
|
||||
const idRegex = /^([\$\w]+):([\$\w-]+)$/
|
||||
|
||||
@@ -45,5 +46,11 @@ export function importEmscriptenSymbolMap(contents: string): EmscriptenSymbolMap
|
||||
return null
|
||||
}
|
||||
|
||||
return map
|
||||
return (frame: Frame) => {
|
||||
if (!map.has(frame.name)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {name: map.get(frame.name)}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import {importProfilesFromArrayBuffer} from '../import'
|
||||
import {importJavaScriptSourceMapSymbolRemapper} from './js-source-map'
|
||||
import {Frame} from './profile'
|
||||
|
||||
async function checkSourceMapApplication(pathToProfile: string, pathToSourceMap: string) {
|
||||
const buffer = fs.readFileSync(pathToProfile)
|
||||
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
|
||||
const profileGroup = await importProfilesFromArrayBuffer(
|
||||
path.basename(pathToProfile),
|
||||
arrayBuffer,
|
||||
)
|
||||
|
||||
if (!profileGroup) {
|
||||
fail('Failed to extract profile')
|
||||
return
|
||||
}
|
||||
|
||||
const sourceMapFileName = path.basename(pathToSourceMap)
|
||||
const remapper = await importJavaScriptSourceMapSymbolRemapper(
|
||||
fs.readFileSync(pathToSourceMap, 'utf-8'),
|
||||
sourceMapFileName,
|
||||
)
|
||||
|
||||
if (!remapper) {
|
||||
fail('Failed to extract sourcemap')
|
||||
return
|
||||
}
|
||||
|
||||
const key: (f: {name?: string; file?: string; line?: number; col?: number}) => string = f => {
|
||||
return `${f.name} @ ${f.file}:${f.line}:${f.col}`
|
||||
}
|
||||
|
||||
const frames: Frame[] = []
|
||||
|
||||
profileGroup.profiles[profileGroup.indexToView].forEachFrame(f => {
|
||||
frames.push(f)
|
||||
})
|
||||
|
||||
frames.sort((a, b) => (a.key < b.key ? -1 : 1))
|
||||
|
||||
const remappedFrames: string[] = []
|
||||
frames.forEach(f => {
|
||||
const remapped = remapper(f)
|
||||
if (!remapped) return
|
||||
remappedFrames.push(`(${key({...f, ...remapped})}) <- (${key(f)})`)
|
||||
})
|
||||
|
||||
expect(remappedFrames).toMatchSnapshot()
|
||||
}
|
||||
|
||||
test('source-map remapping of chrome-85-webpack', async () => {
|
||||
await checkSourceMapApplication(
|
||||
'./sample/profiles/source-maps/chrome-85-webpack.json',
|
||||
'./sample/profiles/source-maps/webpack/typescript-source-map-test.js.map',
|
||||
)
|
||||
})
|
||||
|
||||
test('source-map remapping of firefox-79-webpack', async () => {
|
||||
await checkSourceMapApplication(
|
||||
'./sample/profiles/source-maps/firefox-79-webpack.json',
|
||||
'./sample/profiles/source-maps/webpack/typescript-source-map-test.js.map',
|
||||
)
|
||||
})
|
||||
|
||||
test('source-map remapping of safari-13-webpack', async () => {
|
||||
await checkSourceMapApplication(
|
||||
'./sample/profiles/source-maps/safari-13-webpack.json',
|
||||
'./sample/profiles/source-maps/webpack/typescript-source-map-test.js.map',
|
||||
)
|
||||
})
|
||||
|
||||
test('source-map remapping of chrome-85-esbuild', async () => {
|
||||
await checkSourceMapApplication(
|
||||
'./sample/profiles/source-maps/chrome-85-esbuild.json',
|
||||
'./sample/profiles/source-maps/esbuild/typescript-source-map-test.js.map',
|
||||
)
|
||||
})
|
||||
|
||||
test('source-map remapping of chrome-85-parcel', async () => {
|
||||
await checkSourceMapApplication(
|
||||
'./sample/profiles/source-maps/chrome-85-parcel.json',
|
||||
'./sample/profiles/source-maps/parcel/typescript-source-map-test.js.map',
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,227 @@
|
||||
// This file contains code to allow profiles to be remapped by JavaScript source maps.
|
||||
//
|
||||
// As of writing, this is using an out-of-date version of source-map, because the
|
||||
// source-map library migrated to using web-assembly. This requires loading the
|
||||
// web-assembly ball. The easiest way to do this is to load it from a third-party
|
||||
// URL, but I want speedscope to work standalone offline. This means that the remaining
|
||||
// options require some way of having a local URL that corresponds the .wasm file.
|
||||
//
|
||||
// Also as of writing, speedscope is bundled with Parcel v1. Trying to import
|
||||
// a .wasm file in Parcel v1 tries to load the wasm module itself, which is not
|
||||
// what I'm trying to do -- I want SourceMapConsumer.initialize to be the thing
|
||||
// booting the WebAssembly, not Parcel itself.
|
||||
//
|
||||
// One way of getting around this problem is to modify the build system to
|
||||
// copy the .wasm file from node_modules/source-map/lib/mappings.wasm. I could do
|
||||
// this, but it's a bit of a pain.
|
||||
//
|
||||
// Another would be to use something like
|
||||
// import("url:../node_modules/source-map/lib/mappings.wasm"), and then pass the
|
||||
// resulting URL to SourceMapConsumer.initialize. This is also kind of a pain,
|
||||
// because I can only do that if I upgrade to Parcel v2. Ultimately, I'd like to
|
||||
// use esbuild rather than parcel at all, so for now I'm just punting on this by
|
||||
// using an old-version of source-map which doesn't depend on wasm.
|
||||
|
||||
// This is rarely used, so let's load it async to avoid bloating the initial
|
||||
// bundle.
|
||||
import type {MappingItem, RawSourceMap, SourceMapConsumer} from 'source-map'
|
||||
const sourceMapModule = import('source-map')
|
||||
|
||||
import {Frame, SymbolRemapper} from './profile'
|
||||
import {findIndexBisect} from './utils'
|
||||
|
||||
const DEBUG = false
|
||||
|
||||
export async function importJavaScriptSourceMapSymbolRemapper(
|
||||
contentsString: string,
|
||||
sourceMapFileName: string,
|
||||
): Promise<SymbolRemapper | null> {
|
||||
const sourceMap = await sourceMapModule
|
||||
|
||||
let consumer: SourceMapConsumer | null = null
|
||||
let contents: RawSourceMap | null = null
|
||||
|
||||
try {
|
||||
contents = JSON.parse(contentsString)
|
||||
consumer = new sourceMap.SourceMapConsumer(contents!)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
|
||||
const mappingItems: MappingItem[] = []
|
||||
|
||||
consumer.eachMapping(
|
||||
function (m: MappingItem) {
|
||||
// The sourcemap library uses 1-based line numbers, and 0-based column
|
||||
// numbers. speedscope uses 1-based line-numbers, and 1-based column
|
||||
// numbers for its in-memory representation, so we'll normalize that
|
||||
// here too.
|
||||
mappingItems.push({
|
||||
...m,
|
||||
generatedColumn: m.generatedColumn + 1,
|
||||
originalColumn: m.originalColumn + 1,
|
||||
})
|
||||
},
|
||||
{},
|
||||
|
||||
// We're going to binary search through these later, so make sure they're
|
||||
// sorted by their order in the generated file.
|
||||
sourceMap.SourceMapConsumer.GENERATED_ORDER,
|
||||
)
|
||||
|
||||
const sourceMapFileNameWithoutExt = sourceMapFileName.replace(/\.[^/]*$/, '')
|
||||
|
||||
return (frame: Frame) => {
|
||||
let fileMatches = false
|
||||
if (contents?.file && contents?.file === frame.file) {
|
||||
fileMatches = true
|
||||
} else if (
|
||||
('/' + frame.file?.replace(/\.[^/]*$/, '')).endsWith('/' + sourceMapFileNameWithoutExt)
|
||||
) {
|
||||
fileMatches = true
|
||||
}
|
||||
if (!fileMatches) {
|
||||
// The source-map doesn't apply to the file this frame is defined in.
|
||||
return null
|
||||
}
|
||||
|
||||
if (frame.line == null || frame.col == null) {
|
||||
// If we don't have a line & column number for the frame, we can't
|
||||
// remap it.
|
||||
return null
|
||||
}
|
||||
|
||||
// If we got here, then we hopefully have an remapping.
|
||||
//
|
||||
// Ideally, we'd look up a symbol whose generatedLine & generatedColumn
|
||||
// match what we have in our profile, but unfortunately browsers don't do
|
||||
// this.
|
||||
//
|
||||
// Browsers set the column number for a function to the index of the
|
||||
// opening paren for the argument list, rather than the beginning of the
|
||||
// index of the name.
|
||||
//
|
||||
// function alpha() { ... }
|
||||
// ^
|
||||
//
|
||||
// const beta = function() { ... }
|
||||
// ^
|
||||
//
|
||||
// const gamma = () => { ... }
|
||||
// ^
|
||||
//
|
||||
// Since we don't have the source code being profiled, we unfortunately
|
||||
// can't normalize this to set the column to the first character of the
|
||||
// actual name.
|
||||
//
|
||||
// To work around this limitation, we'll search backwards from the first
|
||||
// mapping whose generatedLine & generatedColumn are beyond the location
|
||||
// in the profile.
|
||||
let mappingIndex = findIndexBisect(mappingItems, m => {
|
||||
if (m.generatedLine > frame.line!) return true
|
||||
if (m.generatedLine < frame.line!) return false
|
||||
if (m.generatedColumn >= frame.col!) return true
|
||||
return false
|
||||
})
|
||||
|
||||
if (mappingIndex === -1) {
|
||||
// There are no symbols following the given profile frame symbol, so try
|
||||
// to apply the very last mapping.
|
||||
mappingIndex = mappingItems.length - 1
|
||||
} else if (mappingIndex === 0) {
|
||||
// If the very first index in mappingItems is beyond the location in the
|
||||
// profile, it means the name we're looking for doesn't have a
|
||||
// corresponding entry in the source-map (this can happen if the
|
||||
// source-map isn't the right source-map)
|
||||
return null
|
||||
} else {
|
||||
mappingIndex--
|
||||
}
|
||||
|
||||
const sourceMapItem = mappingItems[mappingIndex]
|
||||
const remappedFrameInfo: {name?: string; file?: string; line?: number; col?: number} = {}
|
||||
|
||||
if (sourceMapItem.name != null) {
|
||||
remappedFrameInfo.name = sourceMapItem.name
|
||||
} else if (sourceMapItem.source != null) {
|
||||
// HACK: If the item name isn't specified, but the source is present, then
|
||||
// we're going to try to guess what the name is by using the originalLine
|
||||
// and originalColumn.
|
||||
|
||||
// The second argument here is "returnNullOnMissing". Without this, it
|
||||
// throws instead of returning null.
|
||||
const content = consumer?.sourceContentFor(sourceMapItem.source, true)
|
||||
if (content) {
|
||||
const lines = content.split('\n')
|
||||
const line = lines[sourceMapItem.originalLine - 1]
|
||||
if (line) {
|
||||
// It's possible this source map entry will contain stuff other than
|
||||
// the name, so let's only consider word-ish characters that are part
|
||||
// of the prefix.
|
||||
const identifierMatch = /\w+/.exec(line.substr(sourceMapItem.originalColumn - 1))
|
||||
if (identifierMatch) {
|
||||
remappedFrameInfo.name = identifierMatch[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (remappedFrameInfo.name) {
|
||||
case 'constructor': {
|
||||
// If the name was remapped to "constructor", then let's use the
|
||||
// original name, since "constructor" isn't very helpful.
|
||||
//
|
||||
// TODO(jlfwong): Search backwards for the class keyword and see if we
|
||||
// can guess the right name.
|
||||
remappedFrameInfo.name = frame.name + ' constructor'
|
||||
break
|
||||
}
|
||||
|
||||
case 'function': {
|
||||
// If the name is just "function", it probably means we either messed up
|
||||
// the remapping, or that we matched an anonymous function. In either
|
||||
// case, this isn't helpful, so put this back.
|
||||
remappedFrameInfo.name = frame.name
|
||||
break
|
||||
}
|
||||
|
||||
case 'const':
|
||||
case 'export': {
|
||||
// If we got this, we probably just did a bad job leveraging the hack
|
||||
// looking through the source code. Let's fall-back to whatever the
|
||||
// original name was.
|
||||
remappedFrameInfo.name = frame.name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (remappedFrameInfo.name && frame.name.includes(remappedFrameInfo.name)) {
|
||||
// If the remapped name is a substring of the original name, the original
|
||||
// name probably contains more useful information. In that case, just use
|
||||
// the original name instead.
|
||||
//
|
||||
// This can happen, for example, when remapping method names. If a
|
||||
// call stack says the symbol name is "n.zap" and we remapped it to a
|
||||
// function just called "zap", we might as well use the original name
|
||||
// instead.
|
||||
remappedFrameInfo.name = frame.name
|
||||
}
|
||||
|
||||
if (sourceMapItem.source != null) {
|
||||
remappedFrameInfo.file = sourceMapItem.source
|
||||
remappedFrameInfo.line = sourceMapItem.originalLine
|
||||
remappedFrameInfo.col = sourceMapItem.originalColumn
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
console.groupCollapsed(`Remapping "${frame.name}" -> "${remappedFrameInfo.name}"`)
|
||||
console.log('before', {...frame})
|
||||
console.log('item @ index', sourceMapItem)
|
||||
console.log('item @ index + 1', mappingItems[mappingIndex + 1])
|
||||
console.log('after', remappedFrameInfo)
|
||||
console.groupEnd()
|
||||
}
|
||||
|
||||
return remappedFrameInfo
|
||||
}
|
||||
}
|
||||
+31
-7
@@ -3,9 +3,6 @@ import {ValueFormatter, RawValueFormatter} from './value-formatters'
|
||||
import {FileFormat} from './file-format-spec'
|
||||
const demangleCppModule = import('./demangle-cpp')
|
||||
|
||||
// Force eager loading of the module
|
||||
demangleCppModule.then(() => {})
|
||||
|
||||
export interface FrameInfo {
|
||||
key: string | number
|
||||
|
||||
@@ -17,13 +14,17 @@ export interface FrameInfo {
|
||||
// call stack frame.
|
||||
file?: string
|
||||
|
||||
// Line in the given file where this frame occurs
|
||||
// Line in the given file where this frame occurs, 1-based.
|
||||
line?: number
|
||||
|
||||
// Column in the file
|
||||
// Column in the file, 1-based.
|
||||
col?: number
|
||||
}
|
||||
|
||||
export type SymbolRemapper = (
|
||||
frame: Frame,
|
||||
) => {name?: string; file?: string; line?: number; col?: number} | null
|
||||
|
||||
export class HasWeights {
|
||||
private selfWeight = 0
|
||||
private totalWeight = 0
|
||||
@@ -144,6 +145,12 @@ export class Profile {
|
||||
this.totalWeight = totalWeight
|
||||
}
|
||||
|
||||
shallowClone(): Profile {
|
||||
const profile = new Profile(this.totalWeight)
|
||||
Object.assign(profile, this)
|
||||
return profile
|
||||
}
|
||||
|
||||
formatValue(v: number) {
|
||||
return this.valueFormatter.format(v)
|
||||
}
|
||||
@@ -408,9 +415,25 @@ export class Profile {
|
||||
}
|
||||
}
|
||||
|
||||
remapNames(callback: (name: string) => string) {
|
||||
remapSymbols(callback: SymbolRemapper) {
|
||||
for (let frame of this.frames) {
|
||||
frame.name = callback(frame.name)
|
||||
const remapped = callback(frame)
|
||||
if (remapped == null) {
|
||||
continue
|
||||
}
|
||||
const {name, file, line, col} = remapped
|
||||
if (name != null) {
|
||||
frame.name = name
|
||||
}
|
||||
if (file != null) {
|
||||
frame.file = file
|
||||
}
|
||||
if (line != null) {
|
||||
frame.line = line
|
||||
}
|
||||
if (col != null) {
|
||||
frame.col = col
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -598,6 +621,7 @@ export class CallTreeProfileBuilder extends Profile {
|
||||
const frameCount = this.framesInStack.get(frame) || 0
|
||||
this.framesInStack.set(frame, frameCount + 1)
|
||||
this.lastValue = value
|
||||
this.totalWeight = Math.max(this.totalWeight, this.lastValue)
|
||||
}
|
||||
|
||||
private _leaveFrame(frame: Frame, value: number, useAppendOrder: boolean) {
|
||||
|
||||
@@ -90,6 +90,14 @@ test('remapRangesToTrimmedText', () => {
|
||||
expectedHighlightedTrimmed: `[he...]d`,
|
||||
})
|
||||
|
||||
assertTrimmedHighlight({
|
||||
text: 'xxhello world',
|
||||
pattern: 'hello',
|
||||
length: 6,
|
||||
expectedHighlighted: 'xx[hello] world',
|
||||
expectedHighlightedTrimmed: `xx[h...]ld`,
|
||||
})
|
||||
|
||||
assertTrimmedHighlight({
|
||||
text: 'hello world',
|
||||
pattern: 'hello world',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {binarySearch} from './utils'
|
||||
import {findValueBisect} from './utils'
|
||||
|
||||
export const ELLIPSIS = '\u2026'
|
||||
|
||||
@@ -65,7 +65,7 @@ export function trimTextMid(
|
||||
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) {
|
||||
return buildTrimmedText(text, text.length)
|
||||
}
|
||||
const [lo] = binarySearch(
|
||||
const [lo] = findValueBisect(
|
||||
0,
|
||||
text.length,
|
||||
n => {
|
||||
@@ -131,10 +131,7 @@ export function remapRangesToTrimmedText(
|
||||
case IndexTypeInTrimmed.ELIDED: {
|
||||
// The range starts in the prefix, but ends in the elided
|
||||
// section. Add just the prefix + one char for the ellipsis.
|
||||
rangesToHighlightInTrimmedText.push([
|
||||
origStart,
|
||||
origStart + trimmedText.prefixLength + 1,
|
||||
])
|
||||
rangesToHighlightInTrimmedText.push([origStart, trimmedText.prefixLength + 1])
|
||||
highlightedEllipsis = true
|
||||
break
|
||||
}
|
||||
|
||||
+40
-3
@@ -9,11 +9,12 @@ import {
|
||||
zeroPad,
|
||||
formatPercent,
|
||||
KeyedSet,
|
||||
binarySearch,
|
||||
findValueBisect,
|
||||
memoizeByReference,
|
||||
memoizeByShallowEquality,
|
||||
objectsHaveShallowEquality,
|
||||
decodeBase64,
|
||||
findIndexBisect,
|
||||
} from './utils'
|
||||
|
||||
import * as jsc from 'jsverify'
|
||||
@@ -109,13 +110,49 @@ test('formatPercent', () => {
|
||||
expect(formatPercent(100)).toBe('100%')
|
||||
})
|
||||
|
||||
test('binarySearch', () => {
|
||||
const [lo, hi] = binarySearch(0, 10, n => Math.log(n), 1, 0.0001)
|
||||
test('findValueBisect', () => {
|
||||
const [lo, hi] = findValueBisect(0, 10, n => Math.log(n), 1, 0.0001)
|
||||
expect(lo).toBeCloseTo(Math.E, 4)
|
||||
expect(lo).toBeLessThan(Math.E)
|
||||
expect(hi).toBeGreaterThan(Math.E)
|
||||
})
|
||||
|
||||
test('findIndexBisect', () => {
|
||||
const check = (haystack: number[], needle: number) => {
|
||||
const condition = (v: number) => v > needle
|
||||
expect(findIndexBisect(haystack, condition)).toEqual(haystack.findIndex(condition))
|
||||
}
|
||||
|
||||
check([], 0)
|
||||
|
||||
check([0], 0)
|
||||
check([0], -1)
|
||||
check([0], 1)
|
||||
|
||||
check([0, 1], 0)
|
||||
check([0, 1], 1)
|
||||
check([0, 1], 2)
|
||||
|
||||
check([0, 1, 2], 2)
|
||||
check([0, 1, 2], 2)
|
||||
check([0, 1, 2], 2)
|
||||
check([0, 1, 2], 2)
|
||||
|
||||
check([3, 5, 5, 7], 1)
|
||||
check([3, 5, 5, 7], 2)
|
||||
check([3, 5, 5, 7], 5)
|
||||
check([3, 5, 5, 7], 7)
|
||||
check([3, 5, 5, 7], 11)
|
||||
|
||||
jsc.assertForall(jsc.array(jsc.int8), jsc.int8, (haystack: number[], needle: number) => {
|
||||
haystack.sort((a, b) => a - b)
|
||||
|
||||
const fn = (v: number) => v > needle
|
||||
expect(findIndexBisect(haystack, fn)).toEqual(haystack.findIndex(fn))
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
test('memoizeByReference', () => {
|
||||
let hitCount = 0
|
||||
const identity = memoizeByReference((arg: number) => {
|
||||
|
||||
+34
-1
@@ -105,7 +105,7 @@ export function triangle(x: number) {
|
||||
return 2.0 * Math.abs(fract(x) - 0.5) - 1.0
|
||||
}
|
||||
|
||||
export function binarySearch(
|
||||
export function findValueBisect(
|
||||
lo: number,
|
||||
hi: number,
|
||||
f: (val: number) => number,
|
||||
@@ -122,6 +122,39 @@ export function binarySearch(
|
||||
}
|
||||
}
|
||||
|
||||
// Similar to Array.prototype.findIndex, except uses a binary search.
|
||||
//
|
||||
// This assumes that the condition transitions exactly once from false to true
|
||||
// in the list, e.g. the following is a valid input:
|
||||
//
|
||||
// ls = [a, b, c, d]
|
||||
// ls.map(f) = [false, false, true, true]
|
||||
//
|
||||
// The following is an invalid input:
|
||||
//
|
||||
// ls = [a, b, c, d]
|
||||
// ls.map(f) = [false, true, false, true]
|
||||
export function findIndexBisect<T>(ls: T[], f: (val: T) => boolean): number {
|
||||
if (ls.length === 0) return -1
|
||||
|
||||
let lo = 0
|
||||
let hi = ls.length - 1
|
||||
|
||||
while (hi !== lo) {
|
||||
const mid = Math.floor((lo + hi) / 2)
|
||||
|
||||
if (f(ls[mid])) {
|
||||
// The desired index is <= mid
|
||||
hi = mid
|
||||
} else {
|
||||
// The desired index is > mid
|
||||
lo = mid + 1
|
||||
}
|
||||
}
|
||||
|
||||
return f(ls[hi]) ? hi : -1
|
||||
}
|
||||
|
||||
export function noop(...args: any[]) {}
|
||||
|
||||
export function objectsHaveShallowEquality<T extends object>(a: T, b: T): boolean {
|
||||
|
||||
+4
-1
@@ -2,6 +2,7 @@ import {h, render} from 'preact'
|
||||
import {createAppStore} from './store'
|
||||
import {ApplicationContainer} from './views/application-container'
|
||||
import {Provider} from './lib/preact-redux'
|
||||
import {ThemeProvider} from './views/themes/theme'
|
||||
|
||||
console.log(`speedscope v${require('../package.json').version}`)
|
||||
|
||||
@@ -20,7 +21,9 @@ const store = lastStore ? createAppStore(lastStore.getState()) : createAppStore(
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ApplicationContainer />
|
||||
<ThemeProvider>
|
||||
<ApplicationContainer />
|
||||
</ThemeProvider>
|
||||
</Provider>,
|
||||
document.body,
|
||||
document.body.lastElementChild || undefined,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {actionCreator} from '../lib/typed-redux'
|
||||
import {CallTreeNode, Frame, ProfileGroup} from '../lib/profile'
|
||||
import {SortMethod} from '../views/profile-table-view'
|
||||
import {ViewMode} from '.'
|
||||
import {ColorScheme, ViewMode} from '.'
|
||||
import {FlamechartID} from './flamechart-view-state'
|
||||
import {Rect, Vec2} from '../lib/math'
|
||||
import {HashParams} from '../lib/hash-params'
|
||||
@@ -19,6 +19,7 @@ export namespace actions {
|
||||
export const setLoading = actionCreator<boolean>('setLoading')
|
||||
export const setError = actionCreator<boolean>('setError')
|
||||
export const setHashParams = actionCreator<HashParams>('setHashParams')
|
||||
export const setColorScheme = actionCreator<ColorScheme>('setColorScheme')
|
||||
|
||||
export namespace sandwichView {
|
||||
export const setTableSortMethod = actionCreator<SortMethod>('sandwichView.setTableSortMethod')
|
||||
|
||||
+16
-13
@@ -1,9 +1,9 @@
|
||||
import {Frame, Profile} from '../lib/profile'
|
||||
import {triangle, memoizeByReference, memoizeByShallowEquality} from '../lib/utils'
|
||||
import {memoizeByReference, memoizeByShallowEquality} from '../lib/utils'
|
||||
import {RowAtlas} from '../gl/row-atlas'
|
||||
import {CanvasContext} from '../gl/canvas-context'
|
||||
import {Color} from '../lib/color'
|
||||
import {FlamechartRowAtlasKey} from '../gl/flamechart-renderer'
|
||||
import {Theme} from '../views/themes/theme'
|
||||
|
||||
export const createGetColorBucketForFrame = memoizeByReference(
|
||||
(frameToColorBucket: Map<number | string, number>) => {
|
||||
@@ -13,24 +13,27 @@ export const createGetColorBucketForFrame = memoizeByReference(
|
||||
},
|
||||
)
|
||||
|
||||
export const createGetCSSColorForFrame = memoizeByReference(
|
||||
(frameToColorBucket: Map<number | string, number>) => {
|
||||
export const createGetCSSColorForFrame = memoizeByShallowEquality(
|
||||
({
|
||||
theme,
|
||||
frameToColorBucket,
|
||||
}: {
|
||||
theme: Theme
|
||||
frameToColorBucket: Map<number | string, number>
|
||||
}) => {
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
return (frame: Frame): string => {
|
||||
const t = getColorBucketForFrame(frame) / 255
|
||||
|
||||
const x = triangle(30.0 * t)
|
||||
const H = 360.0 * (0.9 * t)
|
||||
const C = 0.25 + 0.2 * x
|
||||
const L = 0.8 - 0.15 * x
|
||||
return Color.fromLumaChromaHue(L, C, H).toCSS()
|
||||
return theme.colorForBucket(t).toCSS()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const getCanvasContext = memoizeByReference((canvas: HTMLCanvasElement) => {
|
||||
return new CanvasContext(canvas)
|
||||
})
|
||||
export const getCanvasContext = memoizeByShallowEquality(
|
||||
({theme, canvas}: {theme: Theme; canvas: HTMLCanvasElement}) => {
|
||||
return new CanvasContext(canvas, theme)
|
||||
},
|
||||
)
|
||||
|
||||
export const getRowAtlas = memoizeByReference((canvasContext: CanvasContext) => {
|
||||
return new RowAtlas<FlamechartRowAtlasKey>(
|
||||
|
||||
+58
-1
@@ -6,7 +6,7 @@ import {actions} from './actions'
|
||||
*/
|
||||
|
||||
import * as redux from 'redux'
|
||||
import {setter, Reducer} from '../lib/typed-redux'
|
||||
import {setter, Reducer, Action} from '../lib/typed-redux'
|
||||
import {HashParams, getHashParams} from '../lib/hash-params'
|
||||
import {ProfileGroupState, profileGroup} from './profiles-state'
|
||||
import {SortMethod, SortField, SortDirection} from '../views/profile-table-view'
|
||||
@@ -22,6 +22,17 @@ export const enum ViewMode {
|
||||
SANDWICH_VIEW,
|
||||
}
|
||||
|
||||
export const enum ColorScheme {
|
||||
// Default: respect prefers-color-schema
|
||||
SYSTEM,
|
||||
|
||||
// Use dark theme
|
||||
DARK,
|
||||
|
||||
// use light theme
|
||||
LIGHT,
|
||||
}
|
||||
|
||||
export interface ApplicationState {
|
||||
// The top-level profile group from which most other data will be derived
|
||||
profileGroup: ProfileGroupState
|
||||
@@ -60,6 +71,9 @@ export interface ApplicationState {
|
||||
// The table sorting method using for the sandwich view, specifying the column
|
||||
// to sort by, and the direction to sort that clumn.
|
||||
tableSortMethod: SortMethod
|
||||
|
||||
// The color scheme to use for the entire UI
|
||||
colorScheme: ColorScheme
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol
|
||||
@@ -69,6 +83,47 @@ const protocol = window.location.protocol
|
||||
// however, XHR will be unavailable to fetching files in adjacent directories.
|
||||
export const canUseXHR = protocol === 'http:' || protocol === 'https:'
|
||||
|
||||
function colorScheme(state: ColorScheme | undefined, action: Action<any>): ColorScheme {
|
||||
const localStorageKey = 'speedscope-color-scheme'
|
||||
|
||||
if (state === undefined) {
|
||||
const storedPreference = window.localStorage && window.localStorage[localStorageKey]
|
||||
if (storedPreference === 'DARK') {
|
||||
return ColorScheme.DARK
|
||||
} else if (storedPreference === 'LIGHT') {
|
||||
return ColorScheme.LIGHT
|
||||
} else {
|
||||
return ColorScheme.SYSTEM
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.setColorScheme.matches(action)) {
|
||||
const value = action.payload
|
||||
|
||||
switch (value) {
|
||||
case ColorScheme.DARK: {
|
||||
window.localStorage[localStorageKey] = 'DARK'
|
||||
break
|
||||
}
|
||||
case ColorScheme.LIGHT: {
|
||||
window.localStorage[localStorageKey] = 'LIGHT'
|
||||
break
|
||||
}
|
||||
case ColorScheme.SYSTEM: {
|
||||
delete window.localStorage[localStorageKey]
|
||||
break
|
||||
}
|
||||
default: {
|
||||
const _exhaustiveCheck: never = value
|
||||
return _exhaustiveCheck
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export function createAppStore(initialState?: ApplicationState): redux.Store<ApplicationState> {
|
||||
const hashParams = getHashParams()
|
||||
|
||||
@@ -96,6 +151,8 @@ export function createAppStore(initialState?: ApplicationState): redux.Store<App
|
||||
field: SortField.SELF,
|
||||
direction: SortDirection.DESCENDING,
|
||||
}),
|
||||
|
||||
colorScheme,
|
||||
})
|
||||
|
||||
return redux.createStore(reducer, initialState)
|
||||
|
||||
@@ -6,6 +6,7 @@ import {useActionCreator} from '../lib/preact-redux'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useAppSelector, useActiveProfileState} from '../store'
|
||||
import {ProfileSearchContextProvider} from './search-view'
|
||||
import {useTheme} from './themes/theme'
|
||||
|
||||
const {
|
||||
setLoading,
|
||||
@@ -20,9 +21,10 @@ const {
|
||||
|
||||
export const ApplicationContainer = memo(() => {
|
||||
const appState = useAppSelector(state => state, [])
|
||||
const theme = useTheme()
|
||||
const canvasContext = useAppSelector(
|
||||
state => (state.glCanvas ? getCanvasContext(state.glCanvas) : null),
|
||||
[],
|
||||
state => (state.glCanvas ? getCanvasContext({theme, canvas: state.glCanvas}) : null),
|
||||
[theme],
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -38,6 +40,7 @@ export const ApplicationContainer = memo(() => {
|
||||
setViewMode={useActionCreator(setViewMode, [])}
|
||||
setFlattenRecursion={useActionCreator(setFlattenRecursion, [])}
|
||||
setProfileIndexToView={useActionCreator(setProfileIndexToView, [])}
|
||||
theme={theme}
|
||||
{...appState}
|
||||
/>
|
||||
</ProfileSearchContextProvider>
|
||||
|
||||
+167
-116
@@ -2,21 +2,28 @@ import {h} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {FileSystemDirectoryEntry} from '../import/file-system-entry'
|
||||
|
||||
import {ProfileGroup} from '../lib/profile'
|
||||
import {FontFamily, FontSize, Colors, Duration} from './style'
|
||||
import {importEmscriptenSymbolMap} from '../lib/emscripten'
|
||||
import {ProfileGroup, SymbolRemapper} from '../lib/profile'
|
||||
import {FontFamily, FontSize, Duration} from './style'
|
||||
import {importEmscriptenSymbolMap as importEmscriptenSymbolRemapper} from '../lib/emscripten'
|
||||
import {SandwichViewContainer} from './sandwich-view'
|
||||
import {saveToFile} from '../lib/file-format'
|
||||
import {ApplicationState, ViewMode, canUseXHR, ActiveProfileState} from '../store'
|
||||
import {StatelessComponent} from '../lib/typed-redux'
|
||||
import {LeftHeavyFlamechartView, ChronoFlamechartView} from './flamechart-view-container'
|
||||
import {CanvasContext} from '../gl/canvas-context'
|
||||
import {Graphics} from '../gl/graphics'
|
||||
import {Toolbar} from './toolbar'
|
||||
import {importJavaScriptSourceMapSymbolRemapper} from '../lib/js-source-map'
|
||||
import {Theme, withTheme} from './themes/theme'
|
||||
|
||||
const importModule = import('../import')
|
||||
// Force eager loading of the module
|
||||
|
||||
// Force eager loading of a few code-split modules.
|
||||
//
|
||||
// We put them all in one place so we can directly control the relative priority
|
||||
// of these.
|
||||
importModule.then(() => {})
|
||||
import('../lib/demangle-cpp').then(() => {})
|
||||
import('source-map').then(() => {})
|
||||
|
||||
async function importProfilesFromText(
|
||||
fileName: string,
|
||||
@@ -51,6 +58,7 @@ const exampleProfileURL = require('../../sample/profiles/stackcollapse/perf-vert
|
||||
|
||||
interface GLCanvasProps {
|
||||
canvasContext: CanvasContext | null
|
||||
theme: Theme
|
||||
setGLCanvas: (canvas: HTMLCanvasElement | null) => void
|
||||
}
|
||||
export class GLCanvas extends StatelessComponent<GLCanvasProps> {
|
||||
@@ -92,7 +100,6 @@ export class GLCanvas extends StatelessComponent<GLCanvasProps> {
|
||||
widthInAppUnits,
|
||||
heightInAppUnits,
|
||||
)
|
||||
this.props.canvasContext.gl.clear(new Graphics.Color(1, 1, 1, 1))
|
||||
}
|
||||
|
||||
onWindowResize = () => {
|
||||
@@ -121,6 +128,7 @@ export class GLCanvas extends StatelessComponent<GLCanvasProps> {
|
||||
window.removeEventListener('resize', this.onWindowResize)
|
||||
}
|
||||
render() {
|
||||
const style = getStyle(this.props.theme)
|
||||
return (
|
||||
<div ref={this.containerRef} className={css(style.glCanvasView)}>
|
||||
<canvas ref={this.ref} width={1} height={1} />
|
||||
@@ -140,6 +148,7 @@ export type ApplicationProps = ApplicationState & {
|
||||
setProfileIndexToView: (profileIndex: number) => void
|
||||
activeProfileState: ActiveProfileState | null
|
||||
canvasContext: CanvasContext | null
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
export class Application extends StatelessComponent<ApplicationProps> {
|
||||
@@ -194,6 +203,10 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
this.props.setLoading(false)
|
||||
}
|
||||
|
||||
getStyle(): ReturnType<typeof getStyle> {
|
||||
return getStyle(this.props.theme)
|
||||
}
|
||||
|
||||
loadFromFile(file: File) {
|
||||
this.loadProfile(async () => {
|
||||
const profiles = await importProfilesFromFile(file)
|
||||
@@ -223,15 +236,36 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
reader.readAsText(file)
|
||||
const fileContents = await fileContentsPromise
|
||||
|
||||
const map = importEmscriptenSymbolMap(fileContents)
|
||||
if (map) {
|
||||
const {profile, index} = this.props.activeProfileState
|
||||
let symbolRemapper: SymbolRemapper | null = null
|
||||
|
||||
const emscriptenSymbolRemapper = importEmscriptenSymbolRemapper(fileContents)
|
||||
if (emscriptenSymbolRemapper) {
|
||||
console.log('Importing as emscripten symbol map')
|
||||
profile.remapNames(name => map.get(name) || name)
|
||||
symbolRemapper = emscriptenSymbolRemapper
|
||||
}
|
||||
|
||||
const jsSourceMapRemapper = await importJavaScriptSourceMapSymbolRemapper(
|
||||
fileContents,
|
||||
file.name,
|
||||
)
|
||||
if (!symbolRemapper && jsSourceMapRemapper) {
|
||||
console.log('Importing as JavaScript source map')
|
||||
symbolRemapper = jsSourceMapRemapper
|
||||
}
|
||||
|
||||
if (symbolRemapper != null) {
|
||||
return {
|
||||
name: this.props.profileGroup.name || 'profile',
|
||||
indexToView: index,
|
||||
profiles: [profile],
|
||||
indexToView: this.props.profileGroup.indexToView,
|
||||
profiles: this.props.profileGroup.profiles.map(profileState => {
|
||||
// We do a shallow clone here to invalidate certain caches keyed
|
||||
// on a reference to the profile group under the assumption that
|
||||
// profiles are immutable. Symbol remapping is (at time of
|
||||
// writing) the only exception to that immutability.
|
||||
const p = profileState.profile.shallowClone()
|
||||
p.remapSymbols(symbolRemapper!)
|
||||
return p
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -403,6 +437,8 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
}
|
||||
|
||||
renderLanding() {
|
||||
const style = this.getStyle()
|
||||
|
||||
return (
|
||||
<div className={css(style.landingContainer)}>
|
||||
<div className={css(style.landingMessage)}>
|
||||
@@ -474,6 +510,8 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
}
|
||||
|
||||
renderError() {
|
||||
const style = this.getStyle()
|
||||
|
||||
return (
|
||||
<div className={css(style.error)}>
|
||||
<div>😿 Something went wrong.</div>
|
||||
@@ -483,6 +521,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
}
|
||||
|
||||
renderLoadingBar() {
|
||||
const style = this.getStyle()
|
||||
return <div className={css(style.loading)} />
|
||||
}
|
||||
|
||||
@@ -517,6 +556,7 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.getStyle()
|
||||
return (
|
||||
<div
|
||||
onDrop={this.onDrop}
|
||||
@@ -524,7 +564,11 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
onDragLeave={this.onDragLeave}
|
||||
className={css(style.root, this.props.dragActive && style.dragTargetRoot)}
|
||||
>
|
||||
<GLCanvas setGLCanvas={this.props.setGLCanvas} canvasContext={this.props.canvasContext} />
|
||||
<GLCanvas
|
||||
setGLCanvas={this.props.setGLCanvas}
|
||||
canvasContext={this.props.canvasContext}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
<Toolbar
|
||||
saveFile={this.saveFile}
|
||||
browseForFile={this.browseForFile}
|
||||
@@ -537,107 +581,114 @@ export class Application extends StatelessComponent<ApplicationProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
glCanvasView: {
|
||||
position: 'absolute',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
zIndex: -1,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
error: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
loading: {
|
||||
height: 3,
|
||||
marginBottom: -3,
|
||||
background: Colors.DARK_BLUE,
|
||||
transformOrigin: '0% 50%',
|
||||
animationName: [
|
||||
{
|
||||
from: {
|
||||
transform: `scaleX(0)`,
|
||||
},
|
||||
to: {
|
||||
transform: `scaleX(1)`,
|
||||
},
|
||||
},
|
||||
],
|
||||
animationTimingFunction: 'cubic-bezier(0, 1, 0, 1)',
|
||||
animationDuration: '30s',
|
||||
},
|
||||
root: {
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
lineHeight: '20px',
|
||||
},
|
||||
dragTargetRoot: {
|
||||
cursor: 'copy',
|
||||
},
|
||||
dragTarget: {
|
||||
boxSizing: 'border-box',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: `5px dashed ${Colors.DARK_BLUE}`,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
contentContainer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
},
|
||||
landingContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
landingMessage: {
|
||||
maxWidth: 600,
|
||||
},
|
||||
landingP: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
hide: {
|
||||
display: 'none',
|
||||
},
|
||||
browseButtonContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
browseButton: {
|
||||
marginBottom: 16,
|
||||
height: 72,
|
||||
flex: 1,
|
||||
maxWidth: 256,
|
||||
textAlign: 'center',
|
||||
fontSize: FontSize.BIG_BUTTON,
|
||||
lineHeight: '72px',
|
||||
background: Colors.DARK_BLUE,
|
||||
color: Colors.WHITE,
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
':hover': {
|
||||
background: Colors.BRIGHT_BLUE,
|
||||
const getStyle = withTheme(theme =>
|
||||
StyleSheet.create({
|
||||
glCanvasView: {
|
||||
position: 'absolute',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
zIndex: -1,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
link: {
|
||||
color: Colors.BRIGHT_BLUE,
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
})
|
||||
error: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
loading: {
|
||||
height: 3,
|
||||
marginBottom: -3,
|
||||
background: theme.selectionPrimaryColor,
|
||||
transformOrigin: '0% 50%',
|
||||
animationName: [
|
||||
{
|
||||
from: {
|
||||
transform: `scaleX(0)`,
|
||||
},
|
||||
to: {
|
||||
transform: `scaleX(1)`,
|
||||
},
|
||||
},
|
||||
],
|
||||
animationTimingFunction: 'cubic-bezier(0, 1, 0, 1)',
|
||||
animationDuration: '30s',
|
||||
},
|
||||
root: {
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
lineHeight: '20px',
|
||||
color: theme.fgPrimaryColor,
|
||||
},
|
||||
dragTargetRoot: {
|
||||
cursor: 'copy',
|
||||
},
|
||||
dragTarget: {
|
||||
boxSizing: 'border-box',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: `5px dashed ${theme.selectionPrimaryColor}`,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
contentContainer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
},
|
||||
landingContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
landingMessage: {
|
||||
maxWidth: 600,
|
||||
},
|
||||
landingP: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
hide: {
|
||||
display: 'none',
|
||||
},
|
||||
browseButtonContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
browseButton: {
|
||||
marginBottom: 16,
|
||||
height: 72,
|
||||
flex: 1,
|
||||
maxWidth: 256,
|
||||
textAlign: 'center',
|
||||
fontSize: FontSize.BIG_BUTTON,
|
||||
lineHeight: '72px',
|
||||
background: theme.selectionPrimaryColor,
|
||||
color: theme.altFgPrimaryColor,
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
':hover': {
|
||||
background: theme.selectionSecondaryColor,
|
||||
},
|
||||
},
|
||||
link: {
|
||||
color: theme.selectionPrimaryColor,
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none',
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
':hover': {
|
||||
color: theme.selectionSecondaryColor,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
import {useAppSelector} from '../store'
|
||||
import {h} from 'preact'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useTheme} from './themes/theme'
|
||||
|
||||
const getCalleeProfile = memoizeByShallowEquality<
|
||||
{
|
||||
@@ -52,6 +53,7 @@ export const CalleeFlamegraphView = memo((ownProps: FlamechartViewContainerProps
|
||||
const {index, profile, sandwichViewState} = activeProfileState
|
||||
const flattenRecursion = useAppSelector(state => state.flattenRecursion, [])
|
||||
const glCanvas = useAppSelector(state => state.glCanvas, [])
|
||||
const theme = useTheme()
|
||||
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
@@ -61,8 +63,8 @@ export const CalleeFlamegraphView = memo((ownProps: FlamechartViewContainerProps
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame({theme, frameToColorBucket})
|
||||
const canvasContext = getCanvasContext({theme, canvas: glCanvas})
|
||||
|
||||
const flamechart = getCalleeFlamegraph({
|
||||
calleeProfile: getCalleeProfile({profile, frame: selectedFrame, flattenRecursion}),
|
||||
@@ -72,6 +74,7 @@ export const CalleeFlamegraphView = memo((ownProps: FlamechartViewContainerProps
|
||||
|
||||
return (
|
||||
<FlamechartWrapper
|
||||
theme={theme}
|
||||
renderInverted={false}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
|
||||
+17
-13
@@ -1,24 +1,28 @@
|
||||
import {h} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {Colors, FontSize} from './style'
|
||||
import {FontSize} from './style'
|
||||
import {useTheme, withTheme} from './themes/theme'
|
||||
|
||||
interface ColorChitProps {
|
||||
color: string
|
||||
}
|
||||
|
||||
export function ColorChit(props: ColorChitProps) {
|
||||
const style = getStyle(useTheme())
|
||||
return <span className={css(style.stackChit)} style={{backgroundColor: props.color}} />
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
stackChit: {
|
||||
position: 'relative',
|
||||
top: -1,
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
marginRight: '0.5em',
|
||||
border: `1px solid ${Colors.LIGHT_GRAY}`,
|
||||
width: FontSize.LABEL - 2,
|
||||
height: FontSize.LABEL - 2,
|
||||
},
|
||||
})
|
||||
const getStyle = withTheme(theme =>
|
||||
StyleSheet.create({
|
||||
stackChit: {
|
||||
position: 'relative',
|
||||
top: -1,
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
marginRight: '0.5em',
|
||||
border: `1px solid ${theme.fgSecondaryColor}`,
|
||||
width: FontSize.LABEL - 2,
|
||||
height: FontSize.LABEL - 2,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {StyleDeclarationValue, css} from 'aphrodite'
|
||||
import {h, Component, JSX} from 'preact'
|
||||
import {style} from './flamechart-style'
|
||||
import {h, JSX} from 'preact'
|
||||
import {getFlamechartStyle} from './flamechart-style'
|
||||
import {formatPercent} from '../lib/utils'
|
||||
import {Frame, CallTreeNode} from '../lib/profile'
|
||||
import {ColorChit} from './color-chit'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {useTheme} from './themes/theme'
|
||||
|
||||
interface StatisticsTableProps {
|
||||
title: string
|
||||
@@ -15,75 +16,75 @@ interface StatisticsTableProps {
|
||||
formatter: (v: number) => string
|
||||
}
|
||||
|
||||
class StatisticsTable extends Component<StatisticsTableProps, {}> {
|
||||
render() {
|
||||
const total = this.props.formatter(this.props.selectedTotal)
|
||||
const self = this.props.formatter(this.props.selectedSelf)
|
||||
const totalPerc = (100.0 * this.props.selectedTotal) / this.props.grandTotal
|
||||
const selfPerc = (100.0 * this.props.selectedSelf) / this.props.grandTotal
|
||||
function StatisticsTable(props: StatisticsTableProps) {
|
||||
const style = getFlamechartStyle(useTheme())
|
||||
|
||||
return (
|
||||
<div className={css(style.statsTable)}>
|
||||
<div className={css(this.props.cellStyle, style.statsTableCell, style.statsTableHeader)}>
|
||||
{this.props.title}
|
||||
</div>
|
||||
const total = props.formatter(props.selectedTotal)
|
||||
const self = props.formatter(props.selectedSelf)
|
||||
const totalPerc = (100.0 * props.selectedTotal) / props.grandTotal
|
||||
const selfPerc = (100.0 * props.selectedSelf) / props.grandTotal
|
||||
|
||||
<div className={css(this.props.cellStyle, style.statsTableCell)}>Total</div>
|
||||
<div className={css(this.props.cellStyle, style.statsTableCell)}>Self</div>
|
||||
|
||||
<div className={css(this.props.cellStyle, style.statsTableCell)}>{total}</div>
|
||||
<div className={css(this.props.cellStyle, style.statsTableCell)}>{self}</div>
|
||||
|
||||
<div className={css(this.props.cellStyle, style.statsTableCell)}>
|
||||
{formatPercent(totalPerc)}
|
||||
<div className={css(style.barDisplay)} style={{height: `${totalPerc}%`}} />
|
||||
</div>
|
||||
<div className={css(this.props.cellStyle, style.statsTableCell)}>
|
||||
{formatPercent(selfPerc)}
|
||||
<div className={css(style.barDisplay)} style={{height: `${selfPerc}%`}} />
|
||||
</div>
|
||||
return (
|
||||
<div className={css(style.statsTable)}>
|
||||
<div className={css(props.cellStyle, style.statsTableCell, style.statsTableHeader)}>
|
||||
{props.title}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className={css(props.cellStyle, style.statsTableCell)}>Total</div>
|
||||
<div className={css(props.cellStyle, style.statsTableCell)}>Self</div>
|
||||
|
||||
<div className={css(props.cellStyle, style.statsTableCell)}>{total}</div>
|
||||
<div className={css(props.cellStyle, style.statsTableCell)}>{self}</div>
|
||||
|
||||
<div className={css(props.cellStyle, style.statsTableCell)}>
|
||||
{formatPercent(totalPerc)}
|
||||
<div className={css(style.barDisplay)} style={{height: `${totalPerc}%`}} />
|
||||
</div>
|
||||
<div className={css(props.cellStyle, style.statsTableCell)}>
|
||||
{formatPercent(selfPerc)}
|
||||
<div className={css(style.barDisplay)} style={{height: `${selfPerc}%`}} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface StackTraceViewProps {
|
||||
getFrameColor: (frame: Frame) => string
|
||||
node: CallTreeNode
|
||||
}
|
||||
class StackTraceView extends Component<StackTraceViewProps, {}> {
|
||||
render() {
|
||||
const rows: JSX.Element[] = []
|
||||
let node: CallTreeNode | null = this.props.node
|
||||
for (; node && !node.isRoot(); node = node.parent) {
|
||||
const row: (JSX.Element | string)[] = []
|
||||
const {frame} = node
|
||||
function StackTraceView(props: StackTraceViewProps) {
|
||||
const style = getFlamechartStyle(useTheme())
|
||||
|
||||
row.push(<ColorChit color={this.props.getFrameColor(frame)} />)
|
||||
const rows: JSX.Element[] = []
|
||||
let node: CallTreeNode | null = props.node
|
||||
for (; node && !node.isRoot(); node = node.parent) {
|
||||
const row: (JSX.Element | string)[] = []
|
||||
const {frame} = node
|
||||
|
||||
if (rows.length) {
|
||||
row.push(<span className={css(style.stackFileLine)}>> </span>)
|
||||
}
|
||||
row.push(frame.name)
|
||||
row.push(<ColorChit color={props.getFrameColor(frame)} />)
|
||||
|
||||
if (frame.file) {
|
||||
let pos = frame.file
|
||||
if (frame.line) {
|
||||
pos += `:${frame.line}`
|
||||
if (frame.col) {
|
||||
pos += `:${frame.col}`
|
||||
}
|
||||
}
|
||||
row.push(<span className={css(style.stackFileLine)}> ({pos})</span>)
|
||||
}
|
||||
rows.push(<div className={css(style.stackLine)}>{row}</div>)
|
||||
if (rows.length) {
|
||||
row.push(<span className={css(style.stackFileLine)}>> </span>)
|
||||
}
|
||||
return (
|
||||
<div className={css(style.stackTraceView)}>
|
||||
<div className={css(style.stackTraceViewPadding)}>{rows}</div>
|
||||
</div>
|
||||
)
|
||||
row.push(frame.name)
|
||||
|
||||
if (frame.file) {
|
||||
let pos = frame.file
|
||||
if (frame.line != null) {
|
||||
pos += `:${frame.line}`
|
||||
if (frame.col != null) {
|
||||
pos += `:${frame.col}`
|
||||
}
|
||||
}
|
||||
row.push(<span className={css(style.stackFileLine)}> ({pos})</span>)
|
||||
}
|
||||
rows.push(<div className={css(style.stackLine)}>{row}</div>)
|
||||
}
|
||||
return (
|
||||
<div className={css(style.stackTraceView)}>
|
||||
<div className={css(style.stackTraceViewPadding)}>{rows}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface FlamechartDetailViewProps {
|
||||
@@ -92,31 +93,31 @@ interface FlamechartDetailViewProps {
|
||||
selectedNode: CallTreeNode
|
||||
}
|
||||
|
||||
export class FlamechartDetailView extends Component<FlamechartDetailViewProps, {}> {
|
||||
render() {
|
||||
const {flamechart, selectedNode} = this.props
|
||||
const {frame} = selectedNode
|
||||
export function FlamechartDetailView(props: FlamechartDetailViewProps) {
|
||||
const style = getFlamechartStyle(useTheme())
|
||||
|
||||
return (
|
||||
<div className={css(style.detailView)}>
|
||||
<StatisticsTable
|
||||
title={'This Instance'}
|
||||
cellStyle={style.thisInstanceCell}
|
||||
grandTotal={flamechart.getTotalWeight()}
|
||||
selectedTotal={selectedNode.getTotalWeight()}
|
||||
selectedSelf={selectedNode.getSelfWeight()}
|
||||
formatter={flamechart.formatValue.bind(flamechart)}
|
||||
/>
|
||||
<StatisticsTable
|
||||
title={'All Instances'}
|
||||
cellStyle={style.allInstancesCell}
|
||||
grandTotal={flamechart.getTotalWeight()}
|
||||
selectedTotal={frame.getTotalWeight()}
|
||||
selectedSelf={frame.getSelfWeight()}
|
||||
formatter={flamechart.formatValue.bind(flamechart)}
|
||||
/>
|
||||
<StackTraceView node={selectedNode} getFrameColor={this.props.getCSSColorForFrame} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const {flamechart, selectedNode} = props
|
||||
const {frame} = selectedNode
|
||||
|
||||
return (
|
||||
<div className={css(style.detailView)}>
|
||||
<StatisticsTable
|
||||
title={'This Instance'}
|
||||
cellStyle={style.thisInstanceCell}
|
||||
grandTotal={flamechart.getTotalWeight()}
|
||||
selectedTotal={selectedNode.getTotalWeight()}
|
||||
selectedSelf={selectedNode.getSelfWeight()}
|
||||
formatter={flamechart.formatValue.bind(flamechart)}
|
||||
/>
|
||||
<StatisticsTable
|
||||
title={'All Instances'}
|
||||
cellStyle={style.allInstancesCell}
|
||||
grandTotal={flamechart.getTotalWeight()}
|
||||
selectedTotal={frame.getTotalWeight()}
|
||||
selectedSelf={frame.getSelfWeight()}
|
||||
formatter={flamechart.formatValue.bind(flamechart)}
|
||||
/>
|
||||
<StackTraceView node={selectedNode} getFrameColor={props.getCSSColorForFrame} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@ import {css} from 'aphrodite'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {Rect, Vec2, AffineTransform, clamp} from '../lib/math'
|
||||
import {FlamechartRenderer} from '../gl/flamechart-renderer'
|
||||
import {style} from './flamechart-style'
|
||||
import {FontFamily, FontSize, Colors, Sizes, commonStyle} from './style'
|
||||
import {getFlamechartStyle} from './flamechart-style'
|
||||
import {FontFamily, FontSize, Sizes, commonStyle} from './style'
|
||||
import {CanvasContext} from '../gl/canvas-context'
|
||||
import {cachedMeasureTextWidth} from '../lib/text-utils'
|
||||
import {Color} from '../lib/color'
|
||||
import {Theme} from './themes/theme'
|
||||
|
||||
interface FlamechartMinimapViewProps {
|
||||
theme: Theme
|
||||
|
||||
flamechart: Flamechart
|
||||
configSpaceViewportRect: Rect
|
||||
|
||||
@@ -40,6 +44,10 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
)
|
||||
}
|
||||
|
||||
private getStyle() {
|
||||
return getFlamechartStyle(this.props.theme)
|
||||
}
|
||||
|
||||
private minimapOrigin() {
|
||||
return new Vec2(0, Sizes.FRAME_HEIGHT * window.devicePixelRatio)
|
||||
}
|
||||
@@ -132,19 +140,22 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
interval *= 2
|
||||
}
|
||||
|
||||
const theme = this.props.theme
|
||||
|
||||
{
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
ctx.fillStyle = Color.fromCSSHex(theme.bgPrimaryColor).withAlpha(0.8).toCSS()
|
||||
ctx.fillRect(0, 0, physicalViewSize.x, physicalViewSpaceFrameHeight)
|
||||
ctx.textBaseline = 'top'
|
||||
|
||||
ctx.fillStyle = Colors.DARK_GRAY
|
||||
for (let x = Math.ceil(left / interval) * interval; x < right; x += interval) {
|
||||
// TODO(jlfwong): Ensure that labels do not overlap
|
||||
const pos = Math.round(configToPhysical.transformPosition(new Vec2(x, 0)).x)
|
||||
const labelText = this.props.flamechart.formatValue(x)
|
||||
const textWidth = Math.ceil(cachedMeasureTextWidth(ctx, labelText))
|
||||
|
||||
ctx.fillStyle = theme.fgPrimaryColor
|
||||
ctx.fillText(labelText, pos - textWidth - labelPaddingPx, labelPaddingPx)
|
||||
ctx.fillStyle = theme.fgSecondaryColor
|
||||
ctx.fillRect(pos, 0, 1, physicalViewSize.y)
|
||||
}
|
||||
}
|
||||
@@ -159,6 +170,14 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
this.renderCanvas()
|
||||
} else if (this.props.configSpaceViewportRect != nextProps.configSpaceViewportRect) {
|
||||
this.renderCanvas()
|
||||
} else if (this.props.canvasContext !== nextProps.canvasContext) {
|
||||
if (this.props.canvasContext) {
|
||||
this.props.canvasContext.removeBeforeFrameHandler(this.onBeforeFrame)
|
||||
}
|
||||
if (nextProps.canvasContext) {
|
||||
nextProps.canvasContext.addBeforeFrameHandler(this.onBeforeFrame)
|
||||
nextProps.canvasContext.requestFrame()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,6 +425,8 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.getStyle()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
|
||||
@@ -3,18 +3,20 @@ import {CallTreeNode} from '../lib/profile'
|
||||
import {Flamechart, FlamechartFrame} from '../lib/flamechart'
|
||||
import {CanvasContext} from '../gl/canvas-context'
|
||||
import {FlamechartRenderer} from '../gl/flamechart-renderer'
|
||||
import {Sizes, FontSize, Colors, FontFamily, commonStyle} from './style'
|
||||
import {Sizes, FontSize, FontFamily, commonStyle} from './style'
|
||||
import {
|
||||
cachedMeasureTextWidth,
|
||||
ELLIPSIS,
|
||||
trimTextMid,
|
||||
remapRangesToTrimmedText,
|
||||
} from '../lib/text-utils'
|
||||
import {style} from './flamechart-style'
|
||||
import {getFlamechartStyle} from './flamechart-style'
|
||||
import {h, Component} from 'preact'
|
||||
import {css} from 'aphrodite'
|
||||
import {ProfileSearchResults} from '../lib/profile-search'
|
||||
import {BatchCanvasTextRenderer, BatchCanvasRectRenderer} from '../lib/canvas-2d-batch-renderers'
|
||||
import {Color} from '../lib/color'
|
||||
import {Theme} from './themes/theme'
|
||||
|
||||
interface FlamechartFrameLabel {
|
||||
configSpaceBounds: Rect
|
||||
@@ -45,6 +47,7 @@ export interface FlamechartPanZoomViewProps {
|
||||
flamechartRenderer: FlamechartRenderer
|
||||
renderInverted: boolean
|
||||
selectedNode: CallTreeNode | null
|
||||
theme: Theme
|
||||
|
||||
onNodeHover: (hover: {node: CallTreeNode; event: MouseEvent} | null) => void
|
||||
onNodeSelect: (node: CallTreeNode | null) => void
|
||||
@@ -70,6 +73,10 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
|
||||
private hoveredLabel: FlamechartFrameLabel | null = null
|
||||
|
||||
private getStyle() {
|
||||
return getFlamechartStyle(this.props.theme)
|
||||
}
|
||||
|
||||
private setConfigSpaceViewportRect(r: Rect) {
|
||||
this.props.setConfigSpaceViewportRect(r)
|
||||
}
|
||||
@@ -283,7 +290,7 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
}
|
||||
|
||||
const frameOutlineWidth = 2 * window.devicePixelRatio
|
||||
ctx.strokeStyle = Colors.PALE_DARK_BLUE
|
||||
ctx.strokeStyle = this.props.theme.selectionSecondaryColor
|
||||
const minConfigSpaceWidthToRenderOutline = (
|
||||
configToPhysical.inverseTransformVector(new Vec2(1, 0)) || new Vec2(0, 0)
|
||||
).x
|
||||
@@ -338,17 +345,22 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
renderFrameLabelAndChildren(frame)
|
||||
}
|
||||
|
||||
matchedFrameBatch.fill(ctx, Colors.ORANGE)
|
||||
matchedTextHighlightBatch.fill(ctx, Colors.YELLOW)
|
||||
fadedLabelBatch.fill(ctx, Colors.LIGHT_GRAY)
|
||||
labelBatch.fill(ctx, Colors.BLACK)
|
||||
indirectlySelectedOutlineBatch.stroke(ctx, Colors.PALE_DARK_BLUE, frameOutlineWidth)
|
||||
directlySelectedOutlineBatch.stroke(ctx, Colors.DARK_BLUE, frameOutlineWidth)
|
||||
const theme = this.props.theme
|
||||
|
||||
matchedFrameBatch.fill(ctx, theme.searchMatchPrimaryColor)
|
||||
matchedTextHighlightBatch.fill(ctx, theme.searchMatchSecondaryColor)
|
||||
fadedLabelBatch.fill(ctx, theme.fgSecondaryColor)
|
||||
labelBatch.fill(
|
||||
ctx,
|
||||
this.props.searchResults != null ? theme.searchMatchTextColor : theme.fgPrimaryColor,
|
||||
)
|
||||
indirectlySelectedOutlineBatch.stroke(ctx, theme.selectionSecondaryColor, frameOutlineWidth)
|
||||
directlySelectedOutlineBatch.stroke(ctx, theme.selectionPrimaryColor, frameOutlineWidth)
|
||||
|
||||
if (this.hoveredLabel) {
|
||||
let color = Colors.DARK_GRAY
|
||||
let color: string = theme.fgPrimaryColor
|
||||
if (this.props.selectedNode === this.hoveredLabel.node) {
|
||||
color = Colors.DARK_BLUE
|
||||
color = theme.selectionPrimaryColor
|
||||
}
|
||||
|
||||
ctx.lineWidth = 2 * devicePixelRatio
|
||||
@@ -394,19 +406,22 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
interval *= 2
|
||||
}
|
||||
|
||||
const theme = this.props.theme
|
||||
|
||||
{
|
||||
const y = this.props.renderInverted ? physicalViewSize.y - physicalViewSpaceFrameHeight : 0
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
ctx.fillStyle = Color.fromCSSHex(theme.bgPrimaryColor).withAlpha(0.8).toCSS()
|
||||
ctx.fillRect(0, y, physicalViewSize.x, physicalViewSpaceFrameHeight)
|
||||
ctx.fillStyle = Colors.DARK_GRAY
|
||||
ctx.textBaseline = 'top'
|
||||
for (let x = Math.ceil(left / interval) * interval; x < right; x += interval) {
|
||||
// TODO(jlfwong): Ensure that labels do not overlap
|
||||
const pos = Math.round(configToPhysical.transformPosition(new Vec2(x, 0)).x)
|
||||
const labelText = this.props.flamechart.formatValue(x)
|
||||
const textWidth = cachedMeasureTextWidth(ctx, labelText)
|
||||
ctx.fillStyle = theme.fgPrimaryColor
|
||||
ctx.fillText(labelText, pos - textWidth - labelPaddingPx, y + labelPaddingPx)
|
||||
ctx.fillStyle = theme.fgSecondaryColor
|
||||
ctx.fillRect(pos, 0, 1, physicalViewSize.y)
|
||||
}
|
||||
}
|
||||
@@ -749,6 +764,14 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
this.renderCanvas()
|
||||
} else if (this.props.configSpaceViewportRect !== nextProps.configSpaceViewportRect) {
|
||||
this.renderCanvas()
|
||||
} else if (this.props.canvasContext !== nextProps.canvasContext) {
|
||||
if (this.props.canvasContext) {
|
||||
this.props.canvasContext.removeBeforeFrameHandler(this.onBeforeFrame)
|
||||
}
|
||||
if (nextProps.canvasContext) {
|
||||
nextProps.canvasContext.addBeforeFrameHandler(this.onBeforeFrame)
|
||||
nextProps.canvasContext.requestFrame()
|
||||
}
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
@@ -763,6 +786,8 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.getStyle()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css(style.panZoomView, commonStyle.vbox)}
|
||||
|
||||
@@ -1,82 +1,96 @@
|
||||
import {StyleSheet} from 'aphrodite'
|
||||
import {FontSize, Colors, Sizes} from './style'
|
||||
import {FontSize, Sizes} from './style'
|
||||
import {withTheme} from './themes/theme'
|
||||
|
||||
export const style = StyleSheet.create({
|
||||
hoverCount: {
|
||||
color: Colors.GREEN,
|
||||
},
|
||||
fill: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
minimap: {
|
||||
height: Sizes.MINIMAP_HEIGHT,
|
||||
borderBottom: `${Sizes.SEPARATOR_HEIGHT}px solid ${Colors.LIGHT_GRAY}`,
|
||||
},
|
||||
panZoomView: {
|
||||
flex: 1,
|
||||
},
|
||||
export const getFlamechartStyle = withTheme(theme =>
|
||||
StyleSheet.create({
|
||||
hoverCount: {
|
||||
color: theme.weightColor,
|
||||
},
|
||||
fill: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
minimap: {
|
||||
height: Sizes.MINIMAP_HEIGHT,
|
||||
borderBottom: `${Sizes.SEPARATOR_HEIGHT}px solid ${theme.fgSecondaryColor}`,
|
||||
},
|
||||
panZoomView: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
detailView: {
|
||||
display: 'grid',
|
||||
height: Sizes.DETAIL_VIEW_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
gridTemplateColumns: '120px 120px 1fr',
|
||||
gridTemplateRows: 'repeat(4, 1fr)',
|
||||
borderTop: `${Sizes.SEPARATOR_HEIGHT}px solid ${Colors.LIGHT_GRAY}`,
|
||||
fontSize: FontSize.LABEL,
|
||||
position: 'absolute',
|
||||
background: Colors.WHITE,
|
||||
width: '100vw',
|
||||
bottom: 0,
|
||||
},
|
||||
stackTraceViewPadding: {
|
||||
padding: 5,
|
||||
},
|
||||
stackTraceView: {
|
||||
height: Sizes.DETAIL_VIEW_HEIGHT,
|
||||
lineHeight: `${FontSize.LABEL + 2}px`,
|
||||
overflow: 'auto',
|
||||
},
|
||||
stackLine: {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
stackFileLine: {
|
||||
color: Colors.LIGHT_GRAY,
|
||||
},
|
||||
statsTable: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gridTemplateRows: `repeat(3, ${FontSize.LABEL + 10}px)`,
|
||||
gridGap: '1px 1px',
|
||||
textAlign: 'center',
|
||||
paddingRight: 1,
|
||||
},
|
||||
statsTableHeader: {
|
||||
gridColumn: '1 / 3',
|
||||
},
|
||||
statsTableCell: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
thisInstanceCell: {
|
||||
background: Colors.DARK_BLUE,
|
||||
color: Colors.WHITE,
|
||||
},
|
||||
allInstancesCell: {
|
||||
background: Colors.PALE_DARK_BLUE,
|
||||
color: Colors.WHITE,
|
||||
},
|
||||
barDisplay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
width: '100%',
|
||||
},
|
||||
})
|
||||
detailView: {
|
||||
display: 'grid',
|
||||
height: Sizes.DETAIL_VIEW_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
gridTemplateColumns: '120px 120px 1fr',
|
||||
gridTemplateRows: 'repeat(4, 1fr)',
|
||||
borderTop: `${Sizes.SEPARATOR_HEIGHT}px solid ${theme.fgSecondaryColor}`,
|
||||
fontSize: FontSize.LABEL,
|
||||
position: 'absolute',
|
||||
background: theme.bgPrimaryColor,
|
||||
width: '100vw',
|
||||
bottom: 0,
|
||||
},
|
||||
stackTraceViewPadding: {
|
||||
padding: 5,
|
||||
},
|
||||
stackTraceView: {
|
||||
height: Sizes.DETAIL_VIEW_HEIGHT,
|
||||
lineHeight: `${FontSize.LABEL + 2}px`,
|
||||
overflow: 'auto',
|
||||
'::-webkit-scrollbar': {
|
||||
background: theme.bgPrimaryColor,
|
||||
},
|
||||
'::-webkit-scrollbar-thumb': {
|
||||
background: theme.fgSecondaryColor,
|
||||
borderRadius: 20,
|
||||
border: `3px solid ${theme.bgPrimaryColor}`,
|
||||
':hover': {
|
||||
background: theme.fgPrimaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
stackLine: {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
stackFileLine: {
|
||||
color: theme.fgSecondaryColor,
|
||||
},
|
||||
statsTable: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gridTemplateRows: `repeat(3, ${FontSize.LABEL + 10}px)`,
|
||||
gridGap: '1px 1px',
|
||||
textAlign: 'center',
|
||||
paddingRight: 1,
|
||||
},
|
||||
statsTableHeader: {
|
||||
gridColumn: '1 / 3',
|
||||
},
|
||||
statsTableCell: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
thisInstanceCell: {
|
||||
background: theme.selectionPrimaryColor,
|
||||
color: theme.altFgPrimaryColor,
|
||||
},
|
||||
allInstancesCell: {
|
||||
background: theme.selectionSecondaryColor,
|
||||
color: theme.altFgPrimaryColor,
|
||||
},
|
||||
barDisplay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
width: '100%',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import {actions} from '../store/actions'
|
||||
import {memo} from 'preact/compat'
|
||||
import {ActiveProfileState} from '../store'
|
||||
import {FlamechartSearchContextProvider} from './flamechart-search-view'
|
||||
import {Theme, useTheme} from './themes/theme'
|
||||
|
||||
interface FlamechartSetters {
|
||||
setLogicalSpaceViewportSize: (logicalSpaceViewportSize: Vec2) => void
|
||||
@@ -60,6 +61,7 @@ export function useFlamechartSetters(id: FlamechartID, profileIndex: number): Fl
|
||||
}
|
||||
|
||||
export type FlamechartViewProps = {
|
||||
theme: Theme
|
||||
canvasContext: CanvasContext
|
||||
flamechart: Flamechart
|
||||
flamechartRenderer: FlamechartRenderer
|
||||
@@ -116,10 +118,12 @@ export const ChronoFlamechartView = memo((props: FlamechartViewContainerProps) =
|
||||
const {activeProfileState, glCanvas} = props
|
||||
const {index, profile, chronoViewState} = activeProfileState
|
||||
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const theme = useTheme()
|
||||
|
||||
const canvasContext = getCanvasContext({theme, canvas: glCanvas})
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame({theme, frameToColorBucket})
|
||||
|
||||
const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame})
|
||||
const flamechartRenderer = getChronoViewFlamechartRenderer({
|
||||
@@ -138,6 +142,7 @@ export const ChronoFlamechartView = memo((props: FlamechartViewContainerProps) =
|
||||
setConfigSpaceViewportRect={setters.setConfigSpaceViewportRect}
|
||||
>
|
||||
<FlamechartView
|
||||
theme={theme}
|
||||
renderInverted={false}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
@@ -174,10 +179,12 @@ export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerPr
|
||||
|
||||
const {index, profile, leftHeavyViewState} = activeProfileState
|
||||
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const theme = useTheme()
|
||||
|
||||
const canvasContext = getCanvasContext({theme, canvas: glCanvas})
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame({theme, frameToColorBucket})
|
||||
|
||||
const flamechart = getLeftHeavyFlamechart({
|
||||
profile,
|
||||
@@ -199,6 +206,7 @@ export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerPr
|
||||
setConfigSpaceViewportRect={setters.setConfigSpaceViewportRect}
|
||||
>
|
||||
<FlamechartView
|
||||
theme={theme}
|
||||
renderInverted={false}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {Rect, Vec2, AffineTransform} from '../lib/math'
|
||||
import {formatPercent} from '../lib/utils'
|
||||
import {FlamechartMinimapView} from './flamechart-minimap-view'
|
||||
|
||||
import {style} from './flamechart-style'
|
||||
import {Sizes, commonStyle} from './style'
|
||||
import {FlamechartDetailView} from './flamechart-detail-view'
|
||||
import {FlamechartPanZoomView} from './flamechart-pan-zoom-view'
|
||||
@@ -16,8 +15,13 @@ import {FlamechartViewProps} from './flamechart-view-container'
|
||||
import {StatelessComponent} from '../lib/typed-redux'
|
||||
import {ProfileSearchContext} from './search-view'
|
||||
import {FlamechartSearchView} from './flamechart-search-view'
|
||||
import {getFlamechartStyle} from './flamechart-style'
|
||||
|
||||
export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
private getStyle() {
|
||||
return getFlamechartStyle(this.props.theme)
|
||||
}
|
||||
|
||||
private configSpaceSize() {
|
||||
return new Vec2(
|
||||
this.props.flamechart.getTotalWeight(),
|
||||
@@ -77,6 +81,8 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
const {width, height, left, top} = this.container.getBoundingClientRect()
|
||||
const offset = new Vec2(hover.event.clientX - left, hover.event.clientY - top)
|
||||
|
||||
const style = this.getStyle()
|
||||
|
||||
return (
|
||||
<Hovertip containerSize={new Vec2(width, height)} offset={offset}>
|
||||
<span className={css(style.hoverCount)}>
|
||||
@@ -93,9 +99,12 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.getStyle()
|
||||
|
||||
return (
|
||||
<div className={css(style.fill, commonStyle.vbox)} ref={this.containerRef}>
|
||||
<FlamechartMinimapView
|
||||
theme={this.props.theme}
|
||||
configSpaceViewportRect={this.props.configSpaceViewportRect}
|
||||
transformViewport={this.transformViewport}
|
||||
flamechart={this.props.flamechart}
|
||||
@@ -107,6 +116,7 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
{searchResults => (
|
||||
<Fragment>
|
||||
<FlamechartPanZoomView
|
||||
theme={this.props.theme}
|
||||
canvasContext={this.props.canvasContext}
|
||||
flamechart={this.props.flamechart}
|
||||
flamechartRenderer={this.props.flamechartRenderer}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {CallTreeNode} from '../lib/profile'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {h} from 'preact'
|
||||
import {commonStyle, Colors} from './style'
|
||||
import {commonStyle} from './style'
|
||||
import {Rect, AffineTransform, Vec2} from '../lib/math'
|
||||
import {FlamechartPanZoomView} from './flamechart-pan-zoom-view'
|
||||
import {noop, formatPercent} from '../lib/utils'
|
||||
import {Hovertip} from './hovertip'
|
||||
import {FlamechartViewProps} from './flamechart-view-container'
|
||||
import {StatelessComponent} from '../lib/typed-redux'
|
||||
import {withTheme} from './themes/theme'
|
||||
|
||||
export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
private clampViewportToFlamegraph(viewportRect: Rect) {
|
||||
@@ -39,6 +40,8 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
if (!hover) return null
|
||||
const {width, height, left, top} = this.container.getBoundingClientRect()
|
||||
const offset = new Vec2(hover.event.clientX - left, hover.event.clientY - top)
|
||||
const style = getStyle(this.props.theme)
|
||||
|
||||
return (
|
||||
<Hovertip containerSize={new Vec2(width, height)} offset={offset}>
|
||||
<span className={css(style.hoverCount)}>
|
||||
@@ -67,6 +70,7 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
ref={this.containerRef}
|
||||
>
|
||||
<FlamechartPanZoomView
|
||||
theme={this.props.theme}
|
||||
selectedNode={null}
|
||||
onNodeHover={this.setNodeHover}
|
||||
onNodeSelect={noop}
|
||||
@@ -87,8 +91,10 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export const style = StyleSheet.create({
|
||||
hoverCount: {
|
||||
color: Colors.GREEN,
|
||||
},
|
||||
})
|
||||
export const getStyle = withTheme(theme =>
|
||||
StyleSheet.create({
|
||||
hoverCount: {
|
||||
color: theme.weightColor,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
+52
-48
@@ -1,64 +1,68 @@
|
||||
import {Vec2} from '../lib/math'
|
||||
import {Sizes, Colors, FontSize, FontFamily, ZIndex} from './style'
|
||||
import {Sizes, FontSize, FontFamily, ZIndex} from './style'
|
||||
import {css, StyleSheet} from 'aphrodite'
|
||||
import {h, Component} from 'preact'
|
||||
import {ComponentChildren, h} from 'preact'
|
||||
import {useTheme, withTheme} from './themes/theme'
|
||||
|
||||
interface HovertipProps {
|
||||
containerSize: Vec2
|
||||
offset: Vec2
|
||||
children?: ComponentChildren
|
||||
}
|
||||
|
||||
export class Hovertip extends Component<HovertipProps, {}> {
|
||||
render() {
|
||||
const {containerSize, offset} = this.props
|
||||
const width = containerSize.x
|
||||
const height = containerSize.y
|
||||
export function Hovertip(props: HovertipProps) {
|
||||
const style = getStyle(useTheme())
|
||||
|
||||
const positionStyle: {[key: string]: number} = {}
|
||||
const {containerSize, offset} = props
|
||||
const width = containerSize.x
|
||||
const height = containerSize.y
|
||||
|
||||
const OFFSET_FROM_MOUSE = 7
|
||||
if (offset.x + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_WIDTH_MAX < width) {
|
||||
positionStyle.left = offset.x + OFFSET_FROM_MOUSE
|
||||
} else {
|
||||
positionStyle.right = width - offset.x + 1
|
||||
}
|
||||
const positionStyle: {[key: string]: number} = {}
|
||||
|
||||
if (offset.y + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_HEIGHT_MAX < height) {
|
||||
positionStyle.top = offset.y + OFFSET_FROM_MOUSE
|
||||
} else {
|
||||
positionStyle.bottom = height - offset.y + 1
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css(style.hoverTip)} style={positionStyle}>
|
||||
<div className={css(style.hoverTipRow)}>{this.props.children}</div>
|
||||
</div>
|
||||
)
|
||||
const OFFSET_FROM_MOUSE = 7
|
||||
if (offset.x + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_WIDTH_MAX < width) {
|
||||
positionStyle.left = offset.x + OFFSET_FROM_MOUSE
|
||||
} else {
|
||||
positionStyle.right = width - offset.x + 1
|
||||
}
|
||||
|
||||
if (offset.y + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_HEIGHT_MAX < height) {
|
||||
positionStyle.top = offset.y + OFFSET_FROM_MOUSE
|
||||
} else {
|
||||
positionStyle.bottom = height - offset.y + 1
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css(style.hoverTip)} style={positionStyle}>
|
||||
<div className={css(style.hoverTipRow)}>{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const HOVERTIP_PADDING = 2
|
||||
|
||||
const style = StyleSheet.create({
|
||||
hoverTip: {
|
||||
position: 'absolute',
|
||||
background: Colors.WHITE,
|
||||
border: '1px solid black',
|
||||
maxWidth: Sizes.TOOLTIP_WIDTH_MAX,
|
||||
paddingTop: HOVERTIP_PADDING,
|
||||
paddingBottom: HOVERTIP_PADDING,
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
fontSize: FontSize.LABEL,
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
zIndex: ZIndex.HOVERTIP,
|
||||
},
|
||||
hoverTipRow: {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflowX: 'hidden',
|
||||
paddingLeft: HOVERTIP_PADDING,
|
||||
paddingRight: HOVERTIP_PADDING,
|
||||
maxWidth: Sizes.TOOLTIP_WIDTH_MAX,
|
||||
},
|
||||
})
|
||||
const getStyle = withTheme(theme =>
|
||||
StyleSheet.create({
|
||||
hoverTip: {
|
||||
position: 'absolute',
|
||||
background: theme.bgPrimaryColor,
|
||||
border: '1px solid black',
|
||||
maxWidth: Sizes.TOOLTIP_WIDTH_MAX,
|
||||
paddingTop: HOVERTIP_PADDING,
|
||||
paddingBottom: HOVERTIP_PADDING,
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
fontSize: FontSize.LABEL,
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
zIndex: ZIndex.HOVERTIP,
|
||||
},
|
||||
hoverTipRow: {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflowX: 'hidden',
|
||||
paddingLeft: HOVERTIP_PADDING,
|
||||
paddingRight: HOVERTIP_PADDING,
|
||||
maxWidth: Sizes.TOOLTIP_WIDTH_MAX,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ import {useAppSelector} from '../store'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
import {h} from 'preact'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useTheme} from './themes/theme'
|
||||
|
||||
const getInvertedCallerProfile = memoizeByShallowEquality(
|
||||
({
|
||||
@@ -57,6 +58,7 @@ export const InvertedCallerFlamegraphView = memo((ownProps: FlamechartViewContai
|
||||
let {profile, sandwichViewState, index} = activeProfileState
|
||||
const flattenRecursion = useAppSelector(state => state.flattenRecursion, [])
|
||||
const glCanvas = useAppSelector(state => state.glCanvas, [])
|
||||
const theme = useTheme()
|
||||
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
@@ -66,8 +68,8 @@ export const InvertedCallerFlamegraphView = memo((ownProps: FlamechartViewContai
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame({theme, frameToColorBucket})
|
||||
const canvasContext = getCanvasContext({theme, canvas: glCanvas})
|
||||
|
||||
const flamechart = getInvertedCallerFlamegraph({
|
||||
invertedCallerProfile: getInvertedCallerProfile({
|
||||
@@ -81,6 +83,7 @@ export const InvertedCallerFlamegraphView = memo((ownProps: FlamechartViewContai
|
||||
|
||||
return (
|
||||
<FlamechartWrapper
|
||||
theme={theme}
|
||||
renderInverted={true}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
|
||||
+120
-76
@@ -2,9 +2,10 @@ import {Profile} from '../lib/profile'
|
||||
import {h, JSX, ComponentChild, Ref} from 'preact'
|
||||
import {useCallback, useState, useMemo, useEffect, useRef} from 'preact/hooks'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {Colors, ZIndex, Sizes} from './style'
|
||||
import {ZIndex, Sizes} from './style'
|
||||
import {fuzzyMatchStrings} from '../lib/fuzzy-find'
|
||||
import {sortBy} from '../lib/utils'
|
||||
import {useTheme, withTheme} from './themes/theme'
|
||||
|
||||
interface ProfileSelectRowProps {
|
||||
setProfileIndexToView: (profileIndex: number) => void
|
||||
@@ -20,12 +21,16 @@ interface ProfileSelectRowProps {
|
||||
closeProfileSelect: () => void
|
||||
}
|
||||
|
||||
function highlightRanges(text: string, ranges: [number, number][]): JSX.Element {
|
||||
function highlightRanges(
|
||||
text: string,
|
||||
ranges: [number, number][],
|
||||
highlightedClassName: string,
|
||||
): JSX.Element {
|
||||
const spans: ComponentChild[] = []
|
||||
let last = 0
|
||||
for (let range of ranges) {
|
||||
spans.push(text.slice(last, range[0]))
|
||||
spans.push(<span className={css(style.highlighted)}>{text.slice(range[0], range[1])}</span>)
|
||||
spans.push(<span className={highlightedClassName}>{text.slice(range[0], range[1])}</span>)
|
||||
last = range[1]
|
||||
}
|
||||
spans.push(text.slice(last))
|
||||
@@ -46,6 +51,8 @@ export function ProfileSelectRow({
|
||||
matchedRanges,
|
||||
indexInFilteredListView,
|
||||
}: ProfileSelectRowProps) {
|
||||
const style = getStyle(useTheme())
|
||||
|
||||
const onMouseUp = useCallback(() => {
|
||||
closeProfileSelect()
|
||||
setProfileIndexToView(indexInProfileGroup)
|
||||
@@ -62,10 +69,11 @@ export function ProfileSelectRow({
|
||||
|
||||
const maxDigits = 1 + Math.floor(Math.log10(profileCount))
|
||||
|
||||
const highlightedClassName = css(style.highlighted)
|
||||
const highlighted = useMemo(() => {
|
||||
const result = highlightRanges(name, matchedRanges)
|
||||
const result = highlightRanges(name, matchedRanges, highlightedClassName)
|
||||
return result
|
||||
}, [name, matchedRanges])
|
||||
}, [name, matchedRanges, highlightedClassName])
|
||||
|
||||
// TODO(jlfwong): There's a really gnarly edge-case here where the highlighted
|
||||
// ranges are part of the text truncated by ellipsis. I'm just going to punt
|
||||
@@ -83,7 +91,10 @@ export function ProfileSelectRow({
|
||||
hovered && style.profileRowHovered,
|
||||
)}
|
||||
>
|
||||
<span className={css(style.profileIndex)} style={{width: maxDigits + 'em'}}>
|
||||
<span
|
||||
className={css(style.profileIndex, selected && style.profileIndexSelected)}
|
||||
style={{width: maxDigits + 'em'}}
|
||||
>
|
||||
{indexInProfileGroup + 1}:
|
||||
</span>{' '}
|
||||
{highlighted}
|
||||
@@ -133,6 +144,8 @@ export function ProfileSelect({
|
||||
visible,
|
||||
setProfileIndexToView,
|
||||
}: ProfileSelectProps) {
|
||||
const style = getStyle(useTheme())
|
||||
|
||||
const [filterText, setFilterText] = useState('')
|
||||
|
||||
const onFilterTextChange = useCallback(
|
||||
@@ -290,6 +303,7 @@ export function ProfileSelect({
|
||||
<div className={css(style.filterInputContainer)}>
|
||||
<input
|
||||
type="text"
|
||||
className={css(style.filterInput)}
|
||||
ref={focusFilterInput}
|
||||
placeholder={'Filter...'}
|
||||
value={filterText}
|
||||
@@ -338,73 +352,103 @@ export function ProfileSelect({
|
||||
|
||||
const paddingHeight = 10
|
||||
|
||||
const style = StyleSheet.create({
|
||||
filterInputContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: 10,
|
||||
alignItems: 'stretch',
|
||||
},
|
||||
caret: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: '5px solid transparent',
|
||||
borderRight: '5px solid transparent',
|
||||
borderBottom: '5px solid black',
|
||||
},
|
||||
highlighted: {
|
||||
background: Colors.PALE_DARK_BLUE,
|
||||
},
|
||||
padding: {
|
||||
height: paddingHeight,
|
||||
background: Colors.BLACK,
|
||||
},
|
||||
profileRow: {
|
||||
height: Sizes.FRAME_HEIGHT - 2,
|
||||
border: '1px solid transparent',
|
||||
textAlign: 'left',
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
background: Colors.BLACK,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
profileRowHovered: {
|
||||
border: `1px solid ${Colors.DARK_BLUE}`,
|
||||
},
|
||||
profileRowSelected: {
|
||||
background: Colors.DARK_BLUE,
|
||||
},
|
||||
profileRowEven: {
|
||||
background: Colors.DARK_GRAY,
|
||||
},
|
||||
profileSelectScrolling: {
|
||||
maxHeight: `min(calc(100vh - ${Sizes.TOOLBAR_HEIGHT - 2 * paddingHeight}px), ${
|
||||
20 * Sizes.FRAME_HEIGHT
|
||||
}px)`,
|
||||
overflow: 'auto',
|
||||
},
|
||||
profileSelectBox: {
|
||||
width: '100%',
|
||||
paddingBottom: 10,
|
||||
background: Colors.BLACK,
|
||||
color: Colors.WHITE,
|
||||
},
|
||||
profileSelectOuter: {
|
||||
width: '100%',
|
||||
maxWidth: 480,
|
||||
margin: '0 auto',
|
||||
position: 'relative',
|
||||
zIndex: ZIndex.PROFILE_SELECT,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
profileIndex: {
|
||||
textAlign: 'right',
|
||||
display: 'inline-block',
|
||||
color: Colors.LIGHT_GRAY,
|
||||
},
|
||||
})
|
||||
const getStyle = withTheme(theme =>
|
||||
StyleSheet.create({
|
||||
filterInputContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: 5,
|
||||
alignItems: 'stretch',
|
||||
},
|
||||
filterInput: {
|
||||
color: theme.altFgPrimaryColor,
|
||||
background: theme.altBgSecondaryColor,
|
||||
borderRadius: 5,
|
||||
padding: 5,
|
||||
':focus': {
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
},
|
||||
'::selection': {
|
||||
color: theme.altFgPrimaryColor,
|
||||
background: theme.selectionPrimaryColor,
|
||||
},
|
||||
},
|
||||
caret: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: '5px solid transparent',
|
||||
borderRight: '5px solid transparent',
|
||||
borderBottom: '5px solid black',
|
||||
},
|
||||
highlighted: {
|
||||
background: theme.selectionSecondaryColor,
|
||||
},
|
||||
padding: {
|
||||
height: paddingHeight,
|
||||
background: theme.altBgPrimaryColor,
|
||||
},
|
||||
profileRow: {
|
||||
height: Sizes.FRAME_HEIGHT - 2,
|
||||
border: '1px solid transparent',
|
||||
textAlign: 'left',
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
background: theme.altBgPrimaryColor,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
profileRowHovered: {
|
||||
border: `1px solid ${theme.selectionPrimaryColor}`,
|
||||
},
|
||||
profileRowSelected: {
|
||||
background: theme.selectionPrimaryColor,
|
||||
},
|
||||
profileRowEven: {
|
||||
background: theme.altBgSecondaryColor,
|
||||
},
|
||||
profileSelectScrolling: {
|
||||
maxHeight: `min(calc(100vh - ${Sizes.TOOLBAR_HEIGHT - 2 * paddingHeight}px), ${
|
||||
20 * Sizes.FRAME_HEIGHT
|
||||
}px)`,
|
||||
overflow: 'auto',
|
||||
'::-webkit-scrollbar': {
|
||||
background: theme.altBgPrimaryColor,
|
||||
},
|
||||
'::-webkit-scrollbar-thumb': {
|
||||
background: theme.altFgSecondaryColor,
|
||||
borderRadius: 20,
|
||||
border: `3px solid ${theme.altBgPrimaryColor}`,
|
||||
':hover': {
|
||||
background: theme.altBgPrimaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
profileSelectBox: {
|
||||
width: '100%',
|
||||
paddingBottom: 10,
|
||||
background: theme.altBgPrimaryColor,
|
||||
color: theme.altFgPrimaryColor,
|
||||
},
|
||||
profileSelectOuter: {
|
||||
width: '100%',
|
||||
maxWidth: 480,
|
||||
margin: '0 auto',
|
||||
position: 'relative',
|
||||
zIndex: ZIndex.PROFILE_SELECT,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
profileIndex: {
|
||||
textAlign: 'right',
|
||||
display: 'inline-block',
|
||||
color: theme.altFgSecondaryColor,
|
||||
},
|
||||
profileIndexSelected: {
|
||||
color: theme.altFgPrimaryColor,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
+128
-100
@@ -1,8 +1,8 @@
|
||||
import {h, Component, JSX, ComponentChild} from 'preact'
|
||||
import {h, JSX, ComponentChild} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {formatPercent} from '../lib/utils'
|
||||
import {FontSize, Colors, Sizes, commonStyle} from './style'
|
||||
import {FontSize, Sizes, commonStyle} from './style'
|
||||
import {ColorChit} from './color-chit'
|
||||
import {ListItem, ScrollableListView} from './scrollable-list-view'
|
||||
import {actions} from '../store/actions'
|
||||
@@ -12,6 +12,8 @@ import {useAppSelector, ActiveProfileState} from '../store'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useCallback, useMemo, useContext} from 'preact/hooks'
|
||||
import {SandwichViewContext} from './sandwich-view'
|
||||
import {Color} from '../lib/color'
|
||||
import {useTheme, withTheme} from './themes/theme'
|
||||
|
||||
export enum SortField {
|
||||
SYMBOL_NAME,
|
||||
@@ -34,6 +36,8 @@ interface HBarProps {
|
||||
}
|
||||
|
||||
function HBarDisplay(props: HBarProps) {
|
||||
const style = getStyle(useTheme())
|
||||
|
||||
return (
|
||||
<div className={css(style.hBarDisplay)}>
|
||||
<div className={css(style.hBarDisplayFilled)} style={{width: `${props.perc}%`}} />
|
||||
@@ -45,26 +49,29 @@ interface SortIconProps {
|
||||
activeDirection: SortDirection | null
|
||||
}
|
||||
|
||||
class SortIcon extends Component<SortIconProps, {}> {
|
||||
render() {
|
||||
const {activeDirection} = this.props
|
||||
const upFill = activeDirection === SortDirection.ASCENDING ? Colors.GRAY : Colors.LIGHT_GRAY
|
||||
const downFill = activeDirection === SortDirection.DESCENDING ? Colors.GRAY : Colors.LIGHT_GRAY
|
||||
function SortIcon(props: SortIconProps) {
|
||||
const theme = useTheme()
|
||||
const style = getStyle(theme)
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="8"
|
||||
height="10"
|
||||
viewBox="0 0 8 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={css(style.sortIcon)}
|
||||
>
|
||||
<path d="M0 4L4 0L8 4H0Z" fill={upFill} />
|
||||
<path d="M0 4L4 0L8 4H0Z" transform="translate(0 10) scale(1 -1)" fill={downFill} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
const {activeDirection} = props
|
||||
const upFill =
|
||||
activeDirection === SortDirection.ASCENDING ? theme.fgPrimaryColor : theme.fgSecondaryColor
|
||||
const downFill =
|
||||
activeDirection === SortDirection.DESCENDING ? theme.fgPrimaryColor : theme.fgSecondaryColor
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="8"
|
||||
height="10"
|
||||
viewBox="0 0 8 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={css(style.sortIcon)}
|
||||
>
|
||||
<path d="M0 4L4 0L8 4H0Z" fill={upFill} />
|
||||
<path d="M0 4L4 0L8 4H0Z" transform="translate(0 10) scale(1 -1)" fill={downFill} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
interface ProfileTableRowViewProps {
|
||||
@@ -103,6 +110,8 @@ const ProfileTableRowView = ({
|
||||
setSelectedFrame,
|
||||
getCSSColorForFrame,
|
||||
}: ProfileTableRowViewProps) => {
|
||||
const style = getStyle(useTheme())
|
||||
|
||||
const totalWeight = frame.getTotalWeight()
|
||||
const selfWeight = frame.getSelfWeight()
|
||||
const totalPerc = (100.0 * totalWeight) / profile.getTotalNonIdleWeight()
|
||||
@@ -166,6 +175,8 @@ export const ProfileTableView = memo(
|
||||
searchQuery,
|
||||
searchIsActive,
|
||||
}: ProfileTableViewProps) => {
|
||||
const style = getStyle(useTheme())
|
||||
|
||||
const onSortClick = useCallback(
|
||||
(field: SortField, ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
@@ -252,6 +263,8 @@ export const ProfileTableView = memo(
|
||||
getCSSColorForFrame,
|
||||
searchIsActive,
|
||||
searchQuery,
|
||||
style.emptyState,
|
||||
style.tableView,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -320,84 +333,98 @@ export const ProfileTableView = memo(
|
||||
},
|
||||
)
|
||||
|
||||
const style = StyleSheet.create({
|
||||
profileTableView: {
|
||||
background: Colors.WHITE,
|
||||
height: '100%',
|
||||
},
|
||||
scrollView: {
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
flexGrow: 1,
|
||||
},
|
||||
tableView: {
|
||||
width: '100%',
|
||||
fontSize: FontSize.LABEL,
|
||||
background: Colors.WHITE,
|
||||
},
|
||||
tableHeader: {
|
||||
borderBottom: `2px solid ${Colors.LIGHT_GRAY}`,
|
||||
textAlign: 'left',
|
||||
color: Colors.GRAY,
|
||||
userSelect: 'none',
|
||||
},
|
||||
sortIcon: {
|
||||
position: 'relative',
|
||||
top: 1,
|
||||
marginRight: Sizes.FRAME_HEIGHT / 4,
|
||||
},
|
||||
tableRow: {
|
||||
height: Sizes.FRAME_HEIGHT,
|
||||
},
|
||||
tableRowEven: {
|
||||
background: Colors.OFF_WHITE,
|
||||
},
|
||||
tableRowSelected: {
|
||||
background: Colors.DARK_BLUE,
|
||||
color: Colors.WHITE,
|
||||
},
|
||||
numericCell: {
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
position: 'relative',
|
||||
textAlign: 'right',
|
||||
paddingRight: Sizes.FRAME_HEIGHT,
|
||||
width: 6 * Sizes.FRAME_HEIGHT,
|
||||
minWidth: 6 * Sizes.FRAME_HEIGHT,
|
||||
},
|
||||
textCell: {
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
maxWidth: 0,
|
||||
},
|
||||
hBarDisplay: {
|
||||
position: 'absolute',
|
||||
background: Colors.TRANSPARENT_GREEN,
|
||||
bottom: 2,
|
||||
height: 2,
|
||||
width: `calc(100% - ${2 * Sizes.FRAME_HEIGHT}px)`,
|
||||
right: Sizes.FRAME_HEIGHT,
|
||||
},
|
||||
hBarDisplayFilled: {
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
background: Colors.GREEN,
|
||||
right: 0,
|
||||
},
|
||||
matched: {
|
||||
borderBottom: `2px solid ${Colors.BLACK}`,
|
||||
},
|
||||
matchedSelected: {
|
||||
borderColor: Colors.WHITE,
|
||||
},
|
||||
emptyState: {
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
})
|
||||
const getStyle = withTheme(theme =>
|
||||
StyleSheet.create({
|
||||
profileTableView: {
|
||||
background: theme.bgPrimaryColor,
|
||||
height: '100%',
|
||||
},
|
||||
scrollView: {
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
flexGrow: 1,
|
||||
'::-webkit-scrollbar': {
|
||||
background: theme.bgPrimaryColor,
|
||||
},
|
||||
'::-webkit-scrollbar-thumb': {
|
||||
background: theme.fgSecondaryColor,
|
||||
borderRadius: 20,
|
||||
border: `3px solid ${theme.bgPrimaryColor}`,
|
||||
':hover': {
|
||||
background: theme.fgPrimaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
tableView: {
|
||||
width: '100%',
|
||||
fontSize: FontSize.LABEL,
|
||||
background: theme.bgPrimaryColor,
|
||||
},
|
||||
tableHeader: {
|
||||
borderBottom: `2px solid ${theme.bgSecondaryColor}`,
|
||||
textAlign: 'left',
|
||||
color: theme.fgPrimaryColor,
|
||||
userSelect: 'none',
|
||||
},
|
||||
sortIcon: {
|
||||
position: 'relative',
|
||||
top: 1,
|
||||
marginRight: Sizes.FRAME_HEIGHT / 4,
|
||||
},
|
||||
tableRow: {
|
||||
background: theme.bgPrimaryColor,
|
||||
height: Sizes.FRAME_HEIGHT,
|
||||
},
|
||||
tableRowEven: {
|
||||
background: theme.bgSecondaryColor,
|
||||
},
|
||||
tableRowSelected: {
|
||||
background: theme.selectionPrimaryColor,
|
||||
color: theme.altFgPrimaryColor,
|
||||
},
|
||||
numericCell: {
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
position: 'relative',
|
||||
textAlign: 'right',
|
||||
paddingRight: Sizes.FRAME_HEIGHT,
|
||||
width: 6 * Sizes.FRAME_HEIGHT,
|
||||
minWidth: 6 * Sizes.FRAME_HEIGHT,
|
||||
},
|
||||
textCell: {
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
maxWidth: 0,
|
||||
},
|
||||
hBarDisplay: {
|
||||
position: 'absolute',
|
||||
background: Color.fromCSSHex(theme.weightColor).withAlpha(0.2).toCSS(),
|
||||
bottom: 2,
|
||||
height: 2,
|
||||
width: `calc(100% - ${2 * Sizes.FRAME_HEIGHT}px)`,
|
||||
right: Sizes.FRAME_HEIGHT,
|
||||
},
|
||||
hBarDisplayFilled: {
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
background: theme.weightColor,
|
||||
right: 0,
|
||||
},
|
||||
matched: {
|
||||
borderBottom: `2px solid ${theme.fgPrimaryColor}`,
|
||||
},
|
||||
matchedSelected: {
|
||||
borderColor: theme.altFgPrimaryColor,
|
||||
},
|
||||
emptyState: {
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface ProfileTableViewContainerProps {
|
||||
activeProfileState: ActiveProfileState
|
||||
@@ -410,10 +437,11 @@ export const ProfileTableViewContainer = memo((ownProps: ProfileTableViewContain
|
||||
const {profile, sandwichViewState, index} = activeProfileState
|
||||
if (!profile) throw new Error('profile missing')
|
||||
const tableSortMethod = useAppSelector(state => state.tableSortMethod, [])
|
||||
const theme = useTheme()
|
||||
const {callerCallee} = sandwichViewState
|
||||
const selectedFrame = callerCallee ? callerCallee.selectedFrame : null
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame({theme, frameToColorBucket})
|
||||
|
||||
const setSelectedFrame = useActionCreator(
|
||||
(selectedFrame: Frame | null) => {
|
||||
|
||||
+49
-41
@@ -4,7 +4,7 @@ import {ProfileTableViewContainer, SortField, SortDirection} from './profile-tab
|
||||
import {h, JSX, createContext} from 'preact'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useCallback, useMemo, useContext} from 'preact/hooks'
|
||||
import {commonStyle, Sizes, Colors, FontSize} from './style'
|
||||
import {commonStyle, Sizes, FontSize} from './style'
|
||||
import {actions} from '../store/actions'
|
||||
import {StatelessComponent} from '../lib/typed-redux'
|
||||
import {InvertedCallerFlamegraphView} from './inverted-caller-flamegraph-view'
|
||||
@@ -15,10 +15,12 @@ import {useAppSelector, ActiveProfileState} from '../store'
|
||||
import {sortBy} from '../lib/utils'
|
||||
import {ProfileSearchContext} from './search-view'
|
||||
import {FuzzyMatch} from '../lib/fuzzy-find'
|
||||
import {Theme, useTheme, withTheme} from './themes/theme'
|
||||
|
||||
interface SandwichViewProps {
|
||||
selectedFrame: Frame | null
|
||||
profileIndex: number
|
||||
theme: Theme
|
||||
activeProfileState: ActiveProfileState
|
||||
setSelectedFrame: (selectedFrame: Frame | null) => void
|
||||
glCanvas: HTMLCanvasElement
|
||||
@@ -43,6 +45,8 @@ class SandwichView extends StatelessComponent<SandwichViewProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = getStyle(this.props.theme)
|
||||
|
||||
const {selectedFrame} = this.props
|
||||
let flamegraphViews: JSX.Element | null = null
|
||||
|
||||
@@ -84,46 +88,48 @@ class SandwichView extends StatelessComponent<SandwichViewProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
tableView: {
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
},
|
||||
panZoomViewWraper: {
|
||||
flex: 1,
|
||||
},
|
||||
flamechartLabelParent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
fontSize: FontSize.TITLE,
|
||||
width: FontSize.TITLE * 1.2,
|
||||
borderRight: `1px solid ${Colors.LIGHT_GRAY}`,
|
||||
},
|
||||
flamechartLabelParentBottom: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
flamechartLabel: {
|
||||
transform: 'rotate(-90deg)',
|
||||
transformOrigin: '50% 50% 0',
|
||||
width: FontSize.TITLE * 1.2,
|
||||
flexShrink: 1,
|
||||
},
|
||||
flamechartLabelBottom: {
|
||||
transform: 'rotate(-90deg)',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
callersAndCallees: {
|
||||
flex: 1,
|
||||
borderLeft: `${Sizes.SEPARATOR_HEIGHT}px solid ${Colors.LIGHT_GRAY}`,
|
||||
},
|
||||
divider: {
|
||||
height: 2,
|
||||
background: Colors.LIGHT_GRAY,
|
||||
},
|
||||
})
|
||||
const getStyle = withTheme(theme =>
|
||||
StyleSheet.create({
|
||||
tableView: {
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
},
|
||||
panZoomViewWraper: {
|
||||
flex: 1,
|
||||
},
|
||||
flamechartLabelParent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
fontSize: FontSize.TITLE,
|
||||
width: FontSize.TITLE * 1.2,
|
||||
borderRight: `1px solid ${theme.fgSecondaryColor}`,
|
||||
},
|
||||
flamechartLabelParentBottom: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
flamechartLabel: {
|
||||
transform: 'rotate(-90deg)',
|
||||
transformOrigin: '50% 50% 0',
|
||||
width: FontSize.TITLE * 1.2,
|
||||
flexShrink: 1,
|
||||
},
|
||||
flamechartLabelBottom: {
|
||||
transform: 'rotate(-90deg)',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
callersAndCallees: {
|
||||
flex: 1,
|
||||
borderLeft: `${Sizes.SEPARATOR_HEIGHT}px solid ${theme.fgSecondaryColor}`,
|
||||
},
|
||||
divider: {
|
||||
height: 2,
|
||||
background: theme.fgSecondaryColor,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface SandwichViewContainerProps {
|
||||
activeProfileState: ActiveProfileState
|
||||
@@ -145,6 +151,7 @@ export const SandwichViewContainer = memo((ownProps: SandwichViewContainerProps)
|
||||
const {sandwichViewState, index} = activeProfileState
|
||||
const {callerCallee} = sandwichViewState
|
||||
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
const setSelectedFrame = useCallback(
|
||||
(selectedFrame: Frame | null) => {
|
||||
@@ -224,6 +231,7 @@ export const SandwichViewContainer = memo((ownProps: SandwichViewContainerProps)
|
||||
return (
|
||||
<SandwichViewContext.Provider value={contextData}>
|
||||
<SandwichView
|
||||
theme={theme}
|
||||
activeProfileState={activeProfileState}
|
||||
glCanvas={glCanvas}
|
||||
setSelectedFrame={setSelectedFrame}
|
||||
|
||||
+61
-56
@@ -2,12 +2,13 @@ import {StyleSheet, css} from 'aphrodite'
|
||||
import {h, createContext, ComponentChildren, Fragment} from 'preact'
|
||||
import {useCallback, useRef, useEffect, useMemo} from 'preact/hooks'
|
||||
import {memo} from 'preact/compat'
|
||||
import {Sizes, Colors, FontSize} from './style'
|
||||
import {Sizes, FontSize} from './style'
|
||||
import {ProfileSearchResults} from '../lib/profile-search'
|
||||
import {Profile} from '../lib/profile'
|
||||
import {useActiveProfileState, useAppSelector} from '../store'
|
||||
import {useActionCreator} from '../lib/preact-redux'
|
||||
import {actions} from '../store/actions'
|
||||
import {useTheme, withTheme} from './themes/theme'
|
||||
|
||||
function stopPropagation(ev: Event) {
|
||||
ev.stopPropagation()
|
||||
@@ -44,6 +45,8 @@ interface SearchViewProps {
|
||||
|
||||
export const SearchView = memo(
|
||||
({numResults, resultIndex, selectNext, selectPrev}: SearchViewProps) => {
|
||||
const theme = useTheme()
|
||||
const style = getStyle(theme)
|
||||
const searchQuery = useAppSelector(state => state.searchQuery, [])
|
||||
const searchIsActive = useAppSelector(state => state.searchIsActive, [])
|
||||
const setSearchQuery = useActionCreator(setSearchQueryAction, [])
|
||||
@@ -169,7 +172,7 @@ export const SearchView = memo(
|
||||
>
|
||||
<path
|
||||
d="M4.99999 4.16217L11.6427 10.8048M11.6427 4.16217L4.99999 10.8048"
|
||||
stroke="#BDBDBD"
|
||||
stroke={theme.altFgSecondaryColor}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -177,61 +180,63 @@ export const SearchView = memo(
|
||||
},
|
||||
)
|
||||
|
||||
const style = StyleSheet.create({
|
||||
searchView: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 10,
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
width: 16 * 13,
|
||||
borderWidth: 2,
|
||||
borderColor: Colors.BLACK,
|
||||
borderStyle: 'solid',
|
||||
fontSize: FontSize.LABEL,
|
||||
boxSizing: 'border-box',
|
||||
background: Colors.DARK_GRAY,
|
||||
color: Colors.WHITE,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
inputContainer: {
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
},
|
||||
input: {
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
fontSize: FontSize.LABEL,
|
||||
lineHeight: `${Sizes.TOOLBAR_HEIGHT}px`,
|
||||
color: Colors.WHITE,
|
||||
':focus': {
|
||||
const getStyle = withTheme(theme =>
|
||||
StyleSheet.create({
|
||||
searchView: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 10,
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
width: 16 * 13,
|
||||
borderWidth: 2,
|
||||
borderColor: theme.altFgPrimaryColor,
|
||||
borderStyle: 'solid',
|
||||
fontSize: FontSize.LABEL,
|
||||
boxSizing: 'border-box',
|
||||
background: theme.altBgSecondaryColor,
|
||||
color: theme.altFgPrimaryColor,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
inputContainer: {
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
},
|
||||
input: {
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
background: 'none',
|
||||
fontSize: FontSize.LABEL,
|
||||
lineHeight: `${Sizes.TOOLBAR_HEIGHT}px`,
|
||||
color: theme.altFgPrimaryColor,
|
||||
':focus': {
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
},
|
||||
'::selection': {
|
||||
color: theme.altFgPrimaryColor,
|
||||
background: theme.selectionPrimaryColor,
|
||||
},
|
||||
},
|
||||
'::selection': {
|
||||
color: Colors.WHITE,
|
||||
background: Colors.DARK_BLUE,
|
||||
resultCount: {
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
},
|
||||
resultCount: {
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
icon: {
|
||||
flexShrink: 0,
|
||||
verticalAlign: 'middle',
|
||||
height: '100%',
|
||||
margin: '0px 2px 0px 2px',
|
||||
fontSize: FontSize.LABEL,
|
||||
},
|
||||
button: {
|
||||
display: 'inline',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
icon: {
|
||||
flexShrink: 0,
|
||||
verticalAlign: 'middle',
|
||||
height: '100%',
|
||||
margin: '0px 2px 0px 2px',
|
||||
fontSize: FontSize.LABEL,
|
||||
},
|
||||
},
|
||||
})
|
||||
button: {
|
||||
display: 'inline',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -10,22 +10,6 @@ export enum FontSize {
|
||||
BIG_BUTTON = 36,
|
||||
}
|
||||
|
||||
export enum Colors {
|
||||
WHITE = '#FFFFFF',
|
||||
OFF_WHITE = '#F6F6F6',
|
||||
LIGHT_GRAY = '#BDBDBD',
|
||||
GRAY = '#666666',
|
||||
DARK_GRAY = '#222222',
|
||||
BLACK = '#000000',
|
||||
BRIGHT_BLUE = '#56CCF2',
|
||||
DARK_BLUE = '#2F80ED',
|
||||
PALE_DARK_BLUE = '#8EB7ED',
|
||||
GREEN = '#6FCF97',
|
||||
TRANSPARENT_GREEN = 'rgba(111, 207, 151, 0.2)',
|
||||
YELLOW = '#FEDC62',
|
||||
ORANGE = '#FFAC02',
|
||||
}
|
||||
|
||||
export enum Sizes {
|
||||
MINIMAP_HEIGHT = 100,
|
||||
DETAIL_VIEW_HEIGHT = 150,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import {Color} from '../../lib/color'
|
||||
import {triangle} from '../../lib/utils'
|
||||
import {Theme} from './theme'
|
||||
|
||||
// These colors are intentionally not exported from this file, because these
|
||||
// colors are theme specific, and we want all color values to come from the
|
||||
// active theme.
|
||||
enum Colors {
|
||||
LIGHTER_GRAY = '#D0D0D0',
|
||||
LIGHT_GRAY = '#BDBDBD',
|
||||
GRAY = '#666666',
|
||||
DARK_GRAY = '#222222',
|
||||
DARKER_GRAY = '#0C0C0C',
|
||||
OFF_BLACK = '#060606',
|
||||
BLACK = '#000000',
|
||||
BLUE = '#00769B',
|
||||
PALE_BLUE = '#004E75',
|
||||
GREEN = '#0F8A42',
|
||||
LIGHT_BROWN = '#D6AE24',
|
||||
BROWN = '#A66F1C',
|
||||
}
|
||||
|
||||
const C_0 = 0.2
|
||||
const C_d = 0.1
|
||||
const L_0 = 0.2
|
||||
const L_d = 0.1
|
||||
|
||||
const colorForBucket = (t: number) => {
|
||||
const x = triangle(30.0 * t)
|
||||
const H = 360.0 * (0.9 * t)
|
||||
const C = C_0 + C_d * x
|
||||
const L = L_0 - L_d * x
|
||||
return Color.fromLumaChromaHue(L, C, H)
|
||||
}
|
||||
const colorForBucketGLSL = `
|
||||
vec3 colorForBucket(float t) {
|
||||
float x = triangle(30.0 * t);
|
||||
float H = 360.0 * (0.9 * t);
|
||||
float C = ${C_0.toFixed(1)} + ${C_d.toFixed(1)} * x;
|
||||
float L = ${L_0.toFixed(1)} - ${L_d.toFixed(1)} * x;
|
||||
return hcl2rgb(H, C, L);
|
||||
}
|
||||
`
|
||||
|
||||
export const darkTheme: Theme = {
|
||||
fgPrimaryColor: Colors.LIGHTER_GRAY,
|
||||
fgSecondaryColor: Colors.GRAY,
|
||||
|
||||
bgPrimaryColor: Colors.OFF_BLACK,
|
||||
bgSecondaryColor: Colors.DARKER_GRAY,
|
||||
|
||||
altFgPrimaryColor: Colors.LIGHTER_GRAY,
|
||||
altFgSecondaryColor: Colors.GRAY,
|
||||
|
||||
altBgPrimaryColor: Colors.BLACK,
|
||||
altBgSecondaryColor: Colors.DARKER_GRAY,
|
||||
|
||||
selectionPrimaryColor: Colors.BLUE,
|
||||
selectionSecondaryColor: Colors.PALE_BLUE,
|
||||
|
||||
weightColor: Colors.GREEN,
|
||||
|
||||
searchMatchTextColor: Colors.DARKER_GRAY,
|
||||
searchMatchPrimaryColor: Colors.BROWN,
|
||||
searchMatchSecondaryColor: Colors.LIGHT_BROWN,
|
||||
|
||||
colorForBucket,
|
||||
colorForBucketGLSL,
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import {Color} from '../../lib/color'
|
||||
import {triangle} from '../../lib/utils'
|
||||
import {Theme} from './theme'
|
||||
|
||||
// These colors are intentionally not exported from this file, because these
|
||||
// colors are theme specific, and we want all color values to come from the
|
||||
// active theme.
|
||||
enum Colors {
|
||||
WHITE = '#FFFFFF',
|
||||
OFF_WHITE = '#F6F6F6',
|
||||
LIGHT_GRAY = '#BDBDBD',
|
||||
GRAY = '#666666',
|
||||
DARK_GRAY = '#222222',
|
||||
OFF_BLACK = '#111111',
|
||||
BLACK = '#000000',
|
||||
DARK_BLUE = '#2F80ED',
|
||||
PALE_DARK_BLUE = '#8EB7ED',
|
||||
GREEN = '#6FCF97',
|
||||
YELLOW = '#FEDC62',
|
||||
ORANGE = '#FFAC02',
|
||||
}
|
||||
|
||||
const C_0 = 0.25
|
||||
const C_d = 0.2
|
||||
const L_0 = 0.8
|
||||
const L_d = 0.15
|
||||
|
||||
const colorForBucket = (t: number) => {
|
||||
const x = triangle(30.0 * t)
|
||||
const H = 360.0 * (0.9 * t)
|
||||
const C = C_0 + C_d * x
|
||||
const L = L_0 - L_d * x
|
||||
return Color.fromLumaChromaHue(L, C, H)
|
||||
}
|
||||
const colorForBucketGLSL = `
|
||||
vec3 colorForBucket(float t) {
|
||||
float x = triangle(30.0 * t);
|
||||
float H = 360.0 * (0.9 * t);
|
||||
float C = ${C_0.toFixed(1)} + ${C_d.toFixed(1)} * x;
|
||||
float L = ${L_0.toFixed(1)} - ${L_d.toFixed(1)} * x;
|
||||
return hcl2rgb(H, C, L);
|
||||
}
|
||||
`
|
||||
|
||||
export const lightTheme: Theme = {
|
||||
fgPrimaryColor: Colors.BLACK,
|
||||
fgSecondaryColor: Colors.LIGHT_GRAY,
|
||||
|
||||
bgPrimaryColor: Colors.WHITE,
|
||||
bgSecondaryColor: Colors.OFF_WHITE,
|
||||
|
||||
altFgPrimaryColor: Colors.WHITE,
|
||||
altFgSecondaryColor: Colors.LIGHT_GRAY,
|
||||
|
||||
altBgPrimaryColor: Colors.BLACK,
|
||||
altBgSecondaryColor: Colors.DARK_GRAY,
|
||||
|
||||
selectionPrimaryColor: Colors.DARK_BLUE,
|
||||
selectionSecondaryColor: Colors.PALE_DARK_BLUE,
|
||||
|
||||
weightColor: Colors.GREEN,
|
||||
|
||||
searchMatchTextColor: Colors.BLACK,
|
||||
searchMatchPrimaryColor: Colors.ORANGE,
|
||||
searchMatchSecondaryColor: Colors.YELLOW,
|
||||
|
||||
colorForBucket,
|
||||
colorForBucketGLSL,
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import {h, ComponentChildren, createContext} from 'preact'
|
||||
import {useCallback, useContext, useEffect, useState} from 'preact/hooks'
|
||||
import {Color} from '../../lib/color'
|
||||
import {memoizeByReference} from '../../lib/utils'
|
||||
import {ColorScheme, useAppSelector} from '../../store'
|
||||
import {darkTheme} from './dark-theme'
|
||||
import {lightTheme} from './light-theme'
|
||||
|
||||
export interface Theme {
|
||||
fgPrimaryColor: string
|
||||
fgSecondaryColor: string
|
||||
bgPrimaryColor: string
|
||||
bgSecondaryColor: string
|
||||
|
||||
altFgPrimaryColor: string
|
||||
altFgSecondaryColor: string
|
||||
altBgPrimaryColor: string
|
||||
altBgSecondaryColor: string
|
||||
|
||||
selectionPrimaryColor: string
|
||||
selectionSecondaryColor: string
|
||||
|
||||
weightColor: string
|
||||
|
||||
searchMatchTextColor: string
|
||||
searchMatchPrimaryColor: string
|
||||
searchMatchSecondaryColor: string
|
||||
|
||||
colorForBucket: (t: number) => Color
|
||||
colorForBucketGLSL: string
|
||||
}
|
||||
|
||||
export const ThemeContext = createContext<Theme>(lightTheme)
|
||||
|
||||
export function useTheme(): Theme {
|
||||
return useContext(ThemeContext)
|
||||
}
|
||||
|
||||
export function withTheme<T>(cb: (theme: Theme) => T) {
|
||||
return memoizeByReference(cb)
|
||||
}
|
||||
|
||||
function matchMediaDarkColorScheme(): MediaQueryList {
|
||||
return matchMedia('(prefers-color-scheme: dark)')
|
||||
}
|
||||
|
||||
export function colorSchemeToString(scheme: ColorScheme): string {
|
||||
switch (scheme) {
|
||||
case ColorScheme.SYSTEM: {
|
||||
return 'System'
|
||||
}
|
||||
case ColorScheme.DARK: {
|
||||
return 'Dark'
|
||||
}
|
||||
case ColorScheme.LIGHT: {
|
||||
return 'Light'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function nextColorScheme(scheme: ColorScheme): ColorScheme {
|
||||
const systemPrefersDarkMode = matchMediaDarkColorScheme().matches
|
||||
|
||||
// We'll use a different cycling order for changing the color scheme depending
|
||||
// on what the *current* system preference is. This should guarantee that when
|
||||
// a user interacts with the color scheme toggle for the first time, it always
|
||||
// changes the color scheme.
|
||||
if (systemPrefersDarkMode) {
|
||||
switch (scheme) {
|
||||
case ColorScheme.SYSTEM: {
|
||||
return ColorScheme.LIGHT
|
||||
}
|
||||
case ColorScheme.LIGHT: {
|
||||
return ColorScheme.DARK
|
||||
}
|
||||
case ColorScheme.DARK: {
|
||||
return ColorScheme.SYSTEM
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (scheme) {
|
||||
case ColorScheme.SYSTEM: {
|
||||
return ColorScheme.DARK
|
||||
}
|
||||
case ColorScheme.DARK: {
|
||||
return ColorScheme.LIGHT
|
||||
}
|
||||
case ColorScheme.LIGHT: {
|
||||
return ColorScheme.SYSTEM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTheme(colorScheme: ColorScheme, systemPrefersDarkMode: boolean) {
|
||||
switch (colorScheme) {
|
||||
case ColorScheme.SYSTEM: {
|
||||
return systemPrefersDarkMode ? darkTheme : lightTheme
|
||||
}
|
||||
case ColorScheme.DARK: {
|
||||
return darkTheme
|
||||
}
|
||||
case ColorScheme.LIGHT: {
|
||||
return lightTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ThemeProvider(props: {children: ComponentChildren}) {
|
||||
const [systemPrefersDarkMode, setSystemPrefersDarkMode] = useState(
|
||||
() => matchMediaDarkColorScheme().matches,
|
||||
)
|
||||
|
||||
const matchMediaListener = useCallback(
|
||||
(event: MediaQueryListEvent) => {
|
||||
setSystemPrefersDarkMode(event.matches)
|
||||
},
|
||||
[setSystemPrefersDarkMode],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const media = matchMediaDarkColorScheme()
|
||||
media.addEventListener('change', matchMediaListener)
|
||||
return () => {
|
||||
media.removeEventListener('change', matchMediaListener)
|
||||
}
|
||||
}, [matchMediaListener])
|
||||
|
||||
const colorScheme = useAppSelector(s => s.colorScheme, [])
|
||||
const theme = getTheme(colorScheme, systemPrefersDarkMode)
|
||||
return <ThemeContext.Provider value={theme} children={props.children} />
|
||||
}
|
||||
+106
-73
@@ -1,13 +1,16 @@
|
||||
import {ApplicationProps} from './application'
|
||||
import {ViewMode} from '../store'
|
||||
import {useAppSelector, ViewMode} from '../store'
|
||||
import {h, JSX, Fragment} from 'preact'
|
||||
import {useCallback, useState, useEffect} from 'preact/hooks'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {Sizes, Colors, FontFamily, FontSize, Duration} from './style'
|
||||
import {Sizes, FontFamily, FontSize, Duration} from './style'
|
||||
import {ProfileSelect} from './profile-select'
|
||||
import {ProfileGroupState} from '../store/profiles-state'
|
||||
import {Profile} from '../lib/profile'
|
||||
import {objectsHaveShallowEquality} from '../lib/utils'
|
||||
import {colorSchemeToString, nextColorScheme, useTheme, withTheme} from './themes/theme'
|
||||
import {useActionCreator} from '../lib/preact-redux'
|
||||
import {actions} from '../store/actions'
|
||||
|
||||
interface ToolbarProps extends ApplicationProps {
|
||||
browseForFile(): void
|
||||
@@ -19,6 +22,7 @@ function useSetViewMode(setViewMode: (viewMode: ViewMode) => void, viewMode: Vie
|
||||
}
|
||||
|
||||
function ToolbarLeftContent(props: ToolbarProps) {
|
||||
const style = getStyle(useTheme())
|
||||
const setChronoFlameChart = useSetViewMode(props.setViewMode, ViewMode.CHRONO_FLAME_CHART)
|
||||
const setLeftHeavyFlameGraph = useSetViewMode(props.setViewMode, ViewMode.LEFT_HEAVY_FLAME_GRAPH)
|
||||
const setSandwichView = useSetViewMode(props.setViewMode, ViewMode.SANDWICH_VIEW)
|
||||
@@ -83,6 +87,8 @@ const getCachedProfileList = (() => {
|
||||
})()
|
||||
|
||||
function ToolbarCenterContent(props: ToolbarProps): JSX.Element {
|
||||
const style = getStyle(useTheme())
|
||||
|
||||
const {activeProfileState, profileGroup} = props
|
||||
const profiles = getCachedProfileList(profileGroup)
|
||||
const [profileSelectShown, setProfileSelectShown] = useState(false)
|
||||
@@ -150,11 +156,33 @@ function ToolbarCenterContent(props: ToolbarProps): JSX.Element {
|
||||
}
|
||||
|
||||
function ToolbarRightContent(props: ToolbarProps) {
|
||||
const style = getStyle(useTheme())
|
||||
const colorScheme = useAppSelector(s => s.colorScheme, [])
|
||||
|
||||
const exportFile = (
|
||||
<div className={css(style.toolbarTab)} onClick={props.saveFile}>
|
||||
<span className={css(style.emoji)}>⤴️</span>Export
|
||||
</div>
|
||||
)
|
||||
const importFile = (
|
||||
<div className={css(style.toolbarTab)} onClick={props.browseForFile}>
|
||||
<span className={css(style.emoji)}>⤵️</span>Import
|
||||
</div>
|
||||
)
|
||||
const toggleColorScheme = useActionCreator(
|
||||
() => actions.setColorScheme(nextColorScheme(colorScheme)),
|
||||
[colorScheme],
|
||||
)
|
||||
|
||||
const colorSchemeToggle = (
|
||||
<div className={css(style.toolbarTab)} onClick={toggleColorScheme}>
|
||||
<span className={css(style.emoji)}>🎨</span>
|
||||
<span className={css(style.toolbarTabColorSchemeToggle)}>
|
||||
{colorSchemeToString(colorScheme)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
const help = (
|
||||
<div className={css(style.toolbarTab)}>
|
||||
<a
|
||||
@@ -169,18 +197,16 @@ function ToolbarRightContent(props: ToolbarProps) {
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarRight)}>
|
||||
{props.activeProfileState && (
|
||||
<div className={css(style.toolbarTab)} onClick={props.saveFile}>
|
||||
<span className={css(style.emoji)}>⤴️</span>Export
|
||||
</div>
|
||||
)}
|
||||
{props.activeProfileState && exportFile}
|
||||
{importFile}
|
||||
{colorSchemeToggle}
|
||||
{help}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Toolbar(props: ToolbarProps) {
|
||||
const style = getStyle(useTheme())
|
||||
return (
|
||||
<div className={css(style.toolbar)}>
|
||||
<ToolbarLeftContent {...props} />
|
||||
@@ -190,71 +216,78 @@ export function Toolbar(props: ToolbarProps) {
|
||||
)
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
toolbar: {
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
flexShrink: 0,
|
||||
background: Colors.BLACK,
|
||||
color: Colors.WHITE,
|
||||
textAlign: 'center',
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
fontSize: FontSize.TITLE,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
userSelect: 'none',
|
||||
},
|
||||
toolbarLeft: {
|
||||
position: 'absolute',
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
top: 0,
|
||||
left: 0,
|
||||
marginRight: 2,
|
||||
textAlign: 'left',
|
||||
},
|
||||
toolbarCenter: {
|
||||
paddingTop: 1,
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
},
|
||||
toolbarRight: {
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
marginRight: 2,
|
||||
textAlign: 'right',
|
||||
},
|
||||
toolbarProfileIndex: {
|
||||
color: Colors.LIGHT_GRAY,
|
||||
},
|
||||
toolbarTab: {
|
||||
background: Colors.DARK_GRAY,
|
||||
marginTop: Sizes.SEPARATOR_HEIGHT,
|
||||
height: Sizes.TOOLBAR_TAB_HEIGHT,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
paddingLeft: 2,
|
||||
paddingRight: 8,
|
||||
display: 'inline-block',
|
||||
marginLeft: 2,
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
':hover': {
|
||||
background: Colors.GRAY,
|
||||
const getStyle = withTheme(theme =>
|
||||
StyleSheet.create({
|
||||
toolbar: {
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
flexShrink: 0,
|
||||
background: theme.altBgPrimaryColor,
|
||||
color: theme.altFgPrimaryColor,
|
||||
textAlign: 'center',
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
fontSize: FontSize.TITLE,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
userSelect: 'none',
|
||||
},
|
||||
},
|
||||
toolbarTabActive: {
|
||||
background: Colors.BRIGHT_BLUE,
|
||||
':hover': {
|
||||
background: Colors.BRIGHT_BLUE,
|
||||
toolbarLeft: {
|
||||
position: 'absolute',
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
top: 0,
|
||||
left: 0,
|
||||
marginRight: 2,
|
||||
textAlign: 'left',
|
||||
},
|
||||
},
|
||||
emoji: {
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
paddingTop: '0px',
|
||||
marginRight: '0.3em',
|
||||
},
|
||||
noLinkStyle: {
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
},
|
||||
})
|
||||
toolbarCenter: {
|
||||
paddingTop: 1,
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
},
|
||||
toolbarRight: {
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
marginRight: 2,
|
||||
textAlign: 'right',
|
||||
},
|
||||
toolbarProfileIndex: {
|
||||
color: theme.altFgSecondaryColor,
|
||||
},
|
||||
toolbarTab: {
|
||||
background: theme.altBgSecondaryColor,
|
||||
marginTop: Sizes.SEPARATOR_HEIGHT,
|
||||
height: Sizes.TOOLBAR_TAB_HEIGHT,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
paddingLeft: 2,
|
||||
paddingRight: 8,
|
||||
display: 'inline-block',
|
||||
marginLeft: 2,
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
':hover': {
|
||||
background: theme.selectionSecondaryColor,
|
||||
},
|
||||
},
|
||||
toolbarTabActive: {
|
||||
background: theme.selectionPrimaryColor,
|
||||
':hover': {
|
||||
background: theme.selectionPrimaryColor,
|
||||
},
|
||||
},
|
||||
toolbarTabColorSchemeToggle: {
|
||||
display: 'inline-block',
|
||||
textAlign: 'center',
|
||||
minWidth: '50px',
|
||||
},
|
||||
emoji: {
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
paddingTop: '0px',
|
||||
marginRight: '0.3em',
|
||||
},
|
||||
noLinkStyle: {
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user