Support remapping profiles using source maps (#317)
This PR adds the ability to remap an already-loaded profile using a JavaScript source map. This is useful for e.g. recording minified profiles in production, and then remapping their symbols when the source map isn't made directly available to the browser in production. This is a bit of a hidden feature. The way it works is to drop a profile into speedscope, then drop the sourcemap file on top of it. To test this, I used a small project @cricklet made (https://gist.github.com/cricklet/0deaaa7dd63657adb6818f0a52362651), and also tested against speedscope itself. To test against speedscope itself, I profiled loading a file in speedscope in Chrome, then dropped the resulting Chrome timeline profile into speedscope, and dropped speedscope's own sourcemap on top. Before dropping the source map, the symbols look like this:  After dropping the source map, they look like this:  I also added automated tests using a small JS bundle constructed with various different JS bundlers to make sure it was doing a sensible thing in each case. # Background Remapping symbols in profiles using source-maps proved to be more complex than I originally thought because of an idiosyncrasy of which line & column are referenced for stack frames in browsers. Rather than the line & column referencing the first character of the symbol, they instead reference the opening paren for the function definition. Here's an example file where it's not immediately apparent which line & column is going to be referenced by each stack frame: ``` class Kludge { constructor() { alpha() } zap() { alpha() } } function alpha() { for (let i = 0; i < 1000; i++) { beta() delta() } } function beta() { for (let i = 0; i < 10; i++) { gamma() } } const delta = function () { for (let i = 0; i < 10; i++) { gamma() } } const gamma = () => { let prod = 1 for (let i = 1; i < 1000; i++) { prod *= i } return prod } const k = new Kludge() k.zap() ``` The resulting profile looks like this:  The relevant line & column for each function are... ``` // Kludge: line 2, column 14 class Kludge { constructor() { ^ ... // zap: line 6, column 6 zap() { ^ ... // alpha: line 11, column 15 function alpha() { ^ ... // delta: line 24, column 24 const delta = function () { ^ ... // gamma: line 31, column 1 const gamma = () => { ^ ``` If we look up the source map entry that corresponds to the opening paren, we'll nearly always get nothing. Instead, we'll look at the entry *preceding* the one which contains the opening paren, and hope that has our symbol name. It seems this works at least some of the time. Another complication is that some, but not all source maps include the original names of functions. For ones that don't, but do include the original source-code, we try to deduce it ourselves with varying amounts of success. Supersedes #306 Fixes #139
This commit is contained in:
22
sample/programs/javascript/source-maps/README.md
Normal file
22
sample/programs/javascript/source-maps/README.md
Normal file
@@ -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.
|
||||
11
sample/programs/javascript/source-maps/alpha.ts
Normal file
11
sample/programs/javascript/source-maps/alpha.ts
Normal file
@@ -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()
|
||||
}
|
||||
})()
|
||||
}
|
||||
7
sample/programs/javascript/source-maps/beta.ts
Normal file
7
sample/programs/javascript/source-maps/beta.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {gamma} from './gamma'
|
||||
|
||||
export function beta() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
gamma()
|
||||
}
|
||||
}
|
||||
7
sample/programs/javascript/source-maps/delta.ts
Normal file
7
sample/programs/javascript/source-maps/delta.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {gamma} from './gamma'
|
||||
|
||||
export const delta = function () {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
gamma()
|
||||
}
|
||||
}
|
||||
7
sample/programs/javascript/source-maps/gamma.ts
Normal file
7
sample/programs/javascript/source-maps/gamma.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const gamma = () => {
|
||||
let prod = 1
|
||||
for (let i = 1; i < 1000; i++) {
|
||||
prod *= i
|
||||
}
|
||||
return prod
|
||||
}
|
||||
3
sample/programs/javascript/source-maps/html/esbuild.html
Normal file
3
sample/programs/javascript/source-maps/html/esbuild.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<title>ESbuild</title>
|
||||
<script src="../dist/esbuild/typescript-source-map-test.js"></script>
|
||||
<h1>ESbuild Source Map Test</h1>
|
||||
3
sample/programs/javascript/source-maps/html/parcel.html
Normal file
3
sample/programs/javascript/source-maps/html/parcel.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<title>Parcel</title>
|
||||
<script src="../dist/parcel/typescript-source-map-test.js"></script>
|
||||
<h1>Parcel Source Map Test</h1>
|
||||
3
sample/programs/javascript/source-maps/html/webpack.html
Normal file
3
sample/programs/javascript/source-maps/html/webpack.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<title>Webpack</title>
|
||||
<script src="../dist/webpack/typescript-source-map-test.js"></script>
|
||||
<h1>Webpack Source Map Test</h1>
|
||||
17
sample/programs/javascript/source-maps/kludge.ts
Normal file
17
sample/programs/javascript/source-maps/kludge.ts
Normal file
@@ -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
sample/programs/javascript/source-maps/package-lock.json
generated
Normal file
10497
sample/programs/javascript/source-maps/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
sample/programs/javascript/source-maps/package.json
Normal file
23
sample/programs/javascript/source-maps/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
10
sample/programs/javascript/source-maps/tsconfig.json
Normal file
10
sample/programs/javascript/source-maps/tsconfig.json
Normal file
@@ -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()
|
||||
22
sample/programs/javascript/source-maps/webpack.config.js
Normal file
22
sample/programs/javascript/source-maps/webpack.config.js
Normal file
@@ -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'),
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user