├── .husky ├── .gitignore └── pre-commit ├── packages ├── v8-deopt-webapp │ ├── .gitignore │ ├── src │ │ ├── modules.d.ts │ │ ├── prism.scss │ │ ├── components │ │ │ ├── SummaryList.module.scss │ │ │ ├── V8DeoptInfoPanel │ │ │ │ ├── index.module.scss │ │ │ │ ├── DeoptTables.module.scss │ │ │ │ ├── MapExplorer.module.scss │ │ │ │ ├── index.jsx │ │ │ │ └── DeoptTables.jsx │ │ │ ├── FileViewer.module.scss │ │ │ ├── App.module.scss │ │ │ ├── SummaryTable.module.scss │ │ │ ├── CodeSettings.module.scss │ │ │ ├── App.jsx │ │ │ ├── Summary.jsx │ │ │ ├── CodePanel.module.scss │ │ │ ├── SummaryList.jsx │ │ │ ├── SummaryTable.jsx │ │ │ ├── appState.jsx │ │ │ ├── FileViewer.jsx │ │ │ ├── CodeSettings.jsx │ │ │ └── CodePanel.jsx │ │ ├── utils │ │ │ ├── mapUtils.js │ │ │ ├── deoptMarkers.module.scss │ │ │ ├── useHashLocation.js │ │ │ └── deoptMarkers.js │ │ ├── _variables.scss │ │ ├── index.d.ts │ │ ├── index.jsx │ │ ├── theme.module.scss │ │ ├── routes.js │ │ └── spectre.module.scss │ ├── jsconfig.json │ ├── vite.config.mjs │ ├── package.json │ ├── index.html │ ├── CHANGELOG.md │ ├── README.md │ └── test │ │ └── generateTestData.mjs ├── v8-deopt-parser │ ├── test │ │ ├── logs │ │ │ ├── adders.node16.v8.log.br │ │ │ ├── adders.node16_14.v8.log.br │ │ │ ├── adders.traceMaps.v8.log.br │ │ │ ├── v8-deopt-parser.v8.log.br │ │ │ └── brotli.js │ │ ├── index.test.js │ │ ├── parseLogs.js │ │ ├── snapshots │ │ │ ├── html-inline.traceMaps.mapTree.txt │ │ │ ├── adders.traceMaps.mapTree.txt │ │ │ └── html-external.traceMaps.mapTree.txt │ │ ├── utils.test.js │ │ ├── parseV8Log.traceMaps.test.js │ │ ├── groupBy.test.js │ │ ├── helpers.js │ │ ├── traceMapsHelpers.js │ │ └── constants.js │ ├── src │ │ ├── v8-tools-core │ │ │ ├── Readme.md │ │ │ ├── csvparser.js │ │ │ └── logreader.js │ │ ├── findEntry.js │ │ ├── sortEntries.js │ │ ├── groupBy.js │ │ ├── deoptParsers.js │ │ ├── optimizationStateParsers.js │ │ ├── index.js │ │ ├── utils.js │ │ ├── propertyICParsers.js │ │ └── index.d.ts │ ├── package.json │ ├── CHANGELOG.md │ └── README.md ├── v8-deopt-viewer │ ├── src │ │ ├── index.d.ts │ │ ├── template.html │ │ ├── determineCommonRoot.js │ │ └── index.js │ ├── scripts │ │ └── prepare.js │ ├── package.json │ ├── bin │ │ └── v8-deopt-viewer.js │ ├── CHANGELOG.md │ └── test │ │ └── determineCommonRoot.test.js └── v8-deopt-generate-log │ ├── package.json │ ├── src │ ├── index.d.ts │ └── index.js │ ├── CHANGELOG.md │ ├── README.md │ └── test │ └── index.test.js ├── .prettierignore ├── examples ├── v8-deopt-webapp.png ├── html-external │ ├── index.html │ ├── objects.js │ └── adders.js ├── two-modules │ ├── objects.js │ └── adders.js ├── simple │ └── adders.js └── html-inline │ └── adders.html ├── .editorconfig ├── .changeset ├── config.json └── README.md ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .vscode └── launch.json ├── package.json ├── LICENSE ├── CONTRIBUTING.md ├── codeql-analysis.yml ├── .gitignore └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/.gitignore: -------------------------------------------------------------------------------- 1 | stats.html 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/v8-deopt-parser/src/v8-tools-core -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.module.scss"; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint-staged 5 | -------------------------------------------------------------------------------- /examples/v8-deopt-webapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewiggins/v8-deopt-viewer/HEAD/examples/v8-deopt-webapp.png -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/prism.scss: -------------------------------------------------------------------------------- 1 | @import "prismjs/themes/prism.css"; 2 | @import "prismjs/plugins/line-numbers/prism-line-numbers.css"; 3 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/logs/adders.node16.v8.log.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewiggins/v8-deopt-viewer/HEAD/packages/v8-deopt-parser/test/logs/adders.node16.v8.log.br -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/logs/adders.node16_14.v8.log.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewiggins/v8-deopt-viewer/HEAD/packages/v8-deopt-parser/test/logs/adders.node16_14.v8.log.br -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/logs/adders.traceMaps.v8.log.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewiggins/v8-deopt-viewer/HEAD/packages/v8-deopt-parser/test/logs/adders.traceMaps.v8.log.br -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/logs/v8-deopt-parser.v8.log.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewiggins/v8-deopt-viewer/HEAD/packages/v8-deopt-parser/test/logs/v8-deopt-parser.v8.log.br -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "checkJs": true, 5 | "jsx": "react-jsx", 6 | "jsxImportSource": "preact", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [{package.json,.*rc,*.yml}] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.1.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "master", 8 | "updateInternalDependencies": "patch" 9 | } 10 | -------------------------------------------------------------------------------- /packages/v8-deopt-viewer/src/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Options { 2 | out: string; 3 | timeout: number; 4 | ["keep-internals"]: boolean; 5 | ["skip-maps"]: boolean; 6 | open: boolean; 7 | input: string; 8 | } 9 | 10 | export default async function run( 11 | srcFile: string, 12 | options: Options 13 | ): Promise; 14 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/SummaryList.module.scss: -------------------------------------------------------------------------------- 1 | ul.summaryList { 2 | list-style: none; 3 | margin: 0; 4 | } 5 | 6 | .severityTable { 7 | text-align: center; 8 | margin-top: 1rem; 9 | 10 | th { 11 | min-width: 100px; 12 | } 13 | } 14 | 15 | .codes { 16 | color: black; 17 | } 18 | 19 | .deopts { 20 | color: black; 21 | } 22 | 23 | .ics { 24 | color: black; 25 | } 26 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/V8DeoptInfoPanel/index.module.scss: -------------------------------------------------------------------------------- 1 | .v8deoptInfoPanel { 2 | border-top-left-radius: 0; 3 | overflow: auto; 4 | 5 | .panel_title { 6 | font-size: 1rem; 7 | font-weight: 500; 8 | text-align: center; 9 | margin-bottom: 0; 10 | } 11 | 12 | .tabLink { 13 | white-space: nowrap; 14 | } 15 | 16 | > nav { 17 | box-shadow: 0 0 0 black; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/html-external/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Adders (external) 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/src/v8-tools-core/Readme.md: -------------------------------------------------------------------------------- 1 | # v8-tools-core 2 | 3 | Core JavaScript modules supporting v8 tools. 4 | 5 | ## Origin 6 | 7 | Files pulled from the `./tools` folder of the [v8 repo](https://github.com/v8/v8) and modified to support inclusion in NodeJS and web browsers 8 | 9 | Last update from V8 was from [4/27/2020](https://github.com/v8/v8/tree/abfdb819ced84d76595083a5c278ad2561d3f516). 10 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/FileViewer.module.scss: -------------------------------------------------------------------------------- 1 | @import "../variables"; 2 | 3 | .fileViewer { 4 | display: flex; 5 | height: calc(100% - #{$headerHeight}); 6 | position: relative; 7 | 8 | @media (max-width: $headerViewportSm) { 9 | height: calc(100% - #{$headerHeightSm}); 10 | } 11 | } 12 | 13 | .fileViewer > * { 14 | flex: 1 0 50%; 15 | } 16 | 17 | .codeSettings { 18 | position: absolute; 19 | top: 0; 20 | right: 0; 21 | z-index: 1; 22 | margin: 0.25rem 0.5rem; 23 | } 24 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/utils/mapUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} mapId 3 | * @returns {string} 4 | */ 5 | export function formatMapId(mapId) { 6 | // return "0x" + mapId.padStart(12, "0"); 7 | return mapId; // Don't do any formatting for now 8 | } 9 | 10 | /** 11 | * @param {import('v8-deopt-parser').MapData} mapData 12 | */ 13 | export function hasMapData(mapData) { 14 | return ( 15 | Object.keys(mapData.nodes).length > 0 && 16 | Object.keys(mapData.edges).length > 0 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/v8-deopt-viewer/scripts/prepare.js: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { copyFile } from "fs/promises"; 4 | 5 | // @ts-ignore 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | const repoRoot = (...args) => path.join(__dirname, "..", "..", "..", ...args); 8 | 9 | async function prepare() { 10 | await copyFile( 11 | repoRoot("README.md"), 12 | repoRoot("packages/v8-deopt-viewer/README.md") 13 | ); 14 | } 15 | 16 | prepare(); 17 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/src/findEntry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {import('v8-deopt-parser').FileV8DeoptInfo} deoptInfo 3 | * @param {string} entryId 4 | * @returns {import('v8-deopt-parser').Entry} 5 | */ 6 | export function findEntry(deoptInfo, entryId) { 7 | if (!entryId) { 8 | return null; 9 | } 10 | 11 | /** @type {Array} */ 12 | const kinds = ["codes", "deopts", "ics"]; 13 | for (let kind of kinds) { 14 | for (let entry of deoptInfo[kind]) { 15 | if (entry.id == entryId) { 16 | return entry; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/src/sortEntries.js: -------------------------------------------------------------------------------- 1 | const typeOrder = ["code", "deopt", "ics"]; 2 | 3 | /** 4 | * @param {import('v8-deopt-parser').Entry[]} entries 5 | */ 6 | export function sortEntries(entries) { 7 | return entries.sort((entry1, entry2) => { 8 | if (entry1.line != entry2.line) { 9 | return entry1.line - entry2.line; 10 | } else if (entry1.column != entry2.column) { 11 | return entry1.column - entry2.column; 12 | } else if (entry1.type != entry2.type) { 13 | return typeOrder.indexOf(entry1.type) - typeOrder.indexOf(entry2.type); 14 | } else { 15 | return 0; 16 | } 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /packages/v8-deopt-generate-log/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v8-deopt-generate-log", 3 | "version": "0.2.3", 4 | "description": "Generate a V8 log given a JS or HTML file", 5 | "type": "module", 6 | "main": "src/index.js", 7 | "types": "src/index.d.ts", 8 | "repository": "https://github.com/andrewiggins/v8-deopt-viewer", 9 | "author": "Andre Wiggins", 10 | "license": "MIT", 11 | "files": [ 12 | "src" 13 | ], 14 | "scripts": { 15 | "test": "node test/index.test.js" 16 | }, 17 | "dependencies": { 18 | "chrome-launcher": "^0.15.1", 19 | "puppeteer-core": "^19.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/index.test.js: -------------------------------------------------------------------------------- 1 | import { readdir } from "fs/promises"; 2 | import { pathToFileURL } from "url"; 3 | import { pkgRoot } from "./helpers.js"; 4 | 5 | async function main() { 6 | const dirContents = await readdir(pkgRoot("test")); 7 | const testFiles = dirContents.filter( 8 | (name) => name.endsWith(".test.js") && name !== "index.test.js" 9 | ); 10 | 11 | for (let testFile of testFiles) { 12 | try { 13 | await import(pathToFileURL(pkgRoot("test", testFile)).toString()); 14 | } catch (e) { 15 | console.error(e); 16 | process.exit(1); 17 | } 18 | } 19 | } 20 | 21 | main(); 22 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/_variables.scss: -------------------------------------------------------------------------------- 1 | @function getHeaderMargin($headerFontSize) { 2 | @return calc($headerFontSize / 2); // From Spectre for all headers (.5em) 3 | } 4 | 5 | @function getHeaderHeight($headerFontSize) { 6 | @return ($headerFontSize * $headerLineHeight) + getHeaderMargin($headerFontSize); 7 | } 8 | 9 | $headerLineHeight: 1.2; // From Spectre for .h1 10 | 11 | $headerFontSize: 2rem; // From Spectre for .h1 12 | $headerFontSizeSm: 1.5rem; // Custom override 13 | 14 | $headerHeight: getHeaderHeight($headerFontSize); 15 | $headerHeightSm: getHeaderHeight($headerFontSizeSm); 16 | 17 | $headerViewportSm: 768px; // Custom override 18 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PerFileV8DeoptInfo, FileV8DeoptInfo } from "v8-deopt-parser"; 2 | 3 | export interface AppProps { 4 | deoptInfo: PerFileDeoptInfoWithSources; 5 | } 6 | 7 | export interface PerFileDeoptInfoWithSources extends PerFileV8DeoptInfo { 8 | files: Record; 9 | } 10 | 11 | export type FileV8DeoptInfoWithSources = FileV8DeoptInfo & { 12 | relativePath: string; 13 | srcPath: string; 14 | src?: string; 15 | srcError?: string; 16 | }; 17 | 18 | export interface Route { 19 | id: string; 20 | title?: string; 21 | route: string; 22 | getHref(...args: Args): string; 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@master 15 | with: 16 | fetch-depth: 0 17 | - name: Setup Node.js 16.x 18 | uses: actions/setup-node@master 19 | with: 20 | node-version: 16.x 21 | - name: Install 22 | run: npm ci 23 | - name: Create Release Pull Request 24 | uses: changesets/action@master 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v8-deopt-parser", 3 | "version": "0.4.3", 4 | "description": "Parse a v8.log file for deoptimizations", 5 | "type": "module", 6 | "main": "src/index.js", 7 | "types": "src/index.d.ts", 8 | "repository": "https://github.com/andrewiggins/v8-deopt-viewer", 9 | "author": "Andre Wiggins", 10 | "license": "MIT", 11 | "files": [ 12 | "src" 13 | ], 14 | "scripts": { 15 | "test": "node test/index.test.js", 16 | "deopts": "v8-deopt-viewer -i test/deopt-results/out-of-memory.v8.log -o test/deopt-results --open" 17 | }, 18 | "devDependencies": { 19 | "escape-string-regexp": "^5.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import preact from "@preact/preset-vite"; 2 | import visualizer from "rollup-plugin-visualizer"; 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | preact(), 8 | // @ts-expect-error Types are wrong 9 | visualizer.default({ 10 | filename: "stats.html", 11 | gzipSize: true, 12 | // brotliSize: true, 13 | }), 14 | ], 15 | css: { 16 | modules: { 17 | // @ts-expect-error Vite types are wrong 18 | localsConvention(name) { 19 | return name.replace(/-/g, "_"); 20 | }, 21 | }, 22 | 23 | }, 24 | build: { 25 | lib: { 26 | entry: "src/index.jsx", 27 | name: "V8DeoptViewer", 28 | }, 29 | }, 30 | json: { 31 | stringify: true, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/V8DeoptInfoPanel/DeoptTables.module.scss: -------------------------------------------------------------------------------- 1 | .entryTable { 2 | display: block; 3 | margin: 1rem auto; 4 | padding: 0.5rem; 5 | border: 0.05rem solid #dadee4; 6 | border-radius: 0.25rem; 7 | 8 | > table { 9 | width: auto; 10 | background: white; 11 | } 12 | 13 | > table > caption { 14 | text-align: left; 15 | padding-bottom: 0.5rem; 16 | } 17 | 18 | .entryId { 19 | margin-right: 0.5rem; 20 | } 21 | 22 | .entryLink { 23 | font-style: italic; 24 | } 25 | } 26 | 27 | .entryTable.selected { 28 | background-color: #fbf1a9; 29 | } 30 | 31 | td.sev1 { 32 | color: var(--sev1-color); 33 | } 34 | 35 | td.sev2 { 36 | color: var(--sev2-color); 37 | } 38 | 39 | td.sev3 { 40 | font-weight: 600; 41 | color: var(--sev3-color); 42 | } 43 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/src/groupBy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {import('.').V8DeoptInfo} rawDeoptInfo 3 | * @returns {import('.').PerFileV8DeoptInfo} 4 | */ 5 | export function groupByFile(rawDeoptInfo) { 6 | /** @type {Record} */ 7 | const files = Object.create(null); 8 | 9 | /** @type {Array<"codes" | "deopts" | "ics">} */ 10 | // @ts-ignore 11 | const kinds = ["codes", "deopts", "ics"]; 12 | for (const kind of kinds) { 13 | for (const entry of rawDeoptInfo[kind]) { 14 | if (!(entry.file in files)) { 15 | files[entry.file] = { id: entry.file, ics: [], deopts: [], codes: [] }; 16 | } 17 | 18 | // @ts-ignore 19 | files[entry.file][kind].push(entry); 20 | } 21 | } 22 | 23 | return { 24 | files, 25 | maps: rawDeoptInfo.maps, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { render } from "preact"; 2 | import "preact/devtools"; 3 | import { App } from "./components/App.jsx"; 4 | import "./theme.module.scss"; 5 | import { Packr } from "msgpackr"; 6 | // VSCode max file limits (https://git.io/JfAp3): 7 | // MODEL_SYNC_LIMIT = 50 * 1024 * 1024; // 50 MB 8 | // LARGE_FILE_SIZE_THRESHOLD = 20 * 1024 * 1024; // 20 MB; 9 | // LARGE_FILE_LINE_COUNT_THRESHOLD = 300 * 1000; // 300K lines 10 | 11 | /** 12 | * @param {Uint8Array} buffer 13 | * @param {Element} container 14 | */ 15 | export function renderIntoDom(buffer, container) { 16 | /** 17 | * @type {import('.').AppProps["deoptInfo"]} 18 | */ 19 | let deoptInfo = new Packr({ variableMapSize: true }).unpack(buffer); 20 | render(, container); 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | # Trigger the workflow on push or pull request, 5 | # but only for the master branch 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | - "!changeset-release/**" 13 | 14 | jobs: 15 | build: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest] 20 | node: [14, 16, 18] 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node }} 28 | cache: npm 29 | - name: npm install & test 30 | run: | 31 | npm ci 32 | npm test -ws 33 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/App.module.scss: -------------------------------------------------------------------------------- 1 | @import "../variables"; 2 | 3 | .pageHeader { 4 | display: flex; 5 | align-items: center; 6 | margin-bottom: getHeaderMargin($headerFontSize); 7 | padding-left: 0.5rem; 8 | 9 | transform: translate3d(calc(-34px - 0.7rem), 0, 0); 10 | transition: transform 0.25s ease-out; 11 | } 12 | 13 | .pageTitle { 14 | margin-bottom: 0; 15 | } 16 | 17 | .backButton { 18 | position: relative; 19 | top: 3px; 20 | margin-right: 0.6rem; 21 | border-radius: 50%; 22 | } 23 | 24 | .pageHeader.subRoute { 25 | transform: none; 26 | } 27 | 28 | @media (max-width: $headerViewportSm) { 29 | .pageTitle { 30 | font-size: $headerFontSizeSm; 31 | } 32 | 33 | .pageHeader { 34 | margin-bottom: getHeaderMargin($headerFontSizeSm); 35 | } 36 | 37 | .backButton { 38 | top: 1px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/v8-deopt-viewer/src/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | V8 Deopt Viewer 7 | 8 | 15 | 16 | 17 |
18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /packages/v8-deopt-generate-log/src/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Options { 2 | /** Path to store the V8 log file. Defaults to your OS temporary directory */ 3 | logFilePath?: string; 4 | 5 | /** 6 | * How long the keep the browser open to allow the webpage to run before 7 | * closing the browser 8 | */ 9 | browserTimeoutMs?: number; 10 | 11 | /** 12 | * Trace the creation of V8 object maps. Defaults to false. Greatly increases 13 | * the size of log files. 14 | */ 15 | traceMaps?: boolean; 16 | } 17 | 18 | /** 19 | * Generate a V8 log of optimizations and deoptimizations for the given JS or 20 | * HTML file 21 | * @param srcPath The path or URL to run 22 | * @param options Options to influence how the log is generated 23 | * @returns The path to the generated V8 log file 24 | */ 25 | export async function generateV8Log( 26 | srcPath: string, 27 | options?: Options 28 | ): Promise; 29 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Parser: parseLogs.js", 11 | "skipFiles": ["/**"], 12 | "program": "${workspaceFolder}/packages/v8-deopt-parser/test/parseLogs.js", 13 | "outFiles": ["${workspaceFolder}/packages/**/*.js", "!**/node_modules/**"] 14 | }, 15 | { 16 | "type": "node", 17 | "request": "launch", 18 | "name": "Parser: traceMaps tests", 19 | "skipFiles": ["/**"], 20 | "program": "${workspaceFolder}/packages/v8-deopt-parser/test/parseV8Log.traceMaps.test.js", 21 | "outFiles": ["${workspaceFolder}/packages/**/*.js", "!**/node_modules/**"] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/v8-deopt-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v8-deopt-viewer", 3 | "version": "0.3.0", 4 | "description": "Generate and view a log of deoptimizations in V8", 5 | "type": "module", 6 | "main": "src/index.js", 7 | "bin": { 8 | "v8-deopt-viewer": "./bin/v8-deopt-viewer.js" 9 | }, 10 | "repository": "https://github.com/andrewiggins/v8-deopt-viewer", 11 | "author": "Andre Wiggins", 12 | "license": "MIT", 13 | "files": [ 14 | "src", 15 | "bin" 16 | ], 17 | "scripts": { 18 | "prepare": "node ./scripts/prepare.js", 19 | "test": "node ./test/determineCommonRoot.test.js" 20 | }, 21 | "dependencies": { 22 | "httpie": "^1.1.2", 23 | "msgpackr": "^1.8.1", 24 | "open": "^8.4.0", 25 | "sade": "^1.8.1", 26 | "v8-deopt-generate-log": "^0.2.3", 27 | "v8-deopt-parser": "^0.4.3", 28 | "v8-deopt-webapp": "^0.5.0" 29 | }, 30 | "devDependencies": { 31 | "tape": "^5.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/utils/deoptMarkers.module.scss: -------------------------------------------------------------------------------- 1 | .deoptMarker { 2 | mark { 3 | border: 0.05rem solid transparent; 4 | border-bottom: 0.05rem solid #ffd367; 5 | } 6 | 7 | &:focus { 8 | box-shadow: none; 9 | } 10 | } 11 | 12 | @mixin severity($sevColor) { 13 | color: var($sevColor); 14 | 15 | mark { 16 | color: var($sevColor); 17 | } 18 | 19 | &:focus mark { 20 | border: 1px solid var($sevColor); 21 | } 22 | 23 | // &:target mark { 24 | // background-color: yellow; 25 | // border: 2px solid var($sevColor); 26 | // } 27 | 28 | &.active mark { 29 | background-color: yellow; 30 | border: 2px solid var($sevColor); 31 | } 32 | } 33 | 34 | .deoptMarker.sev1 { 35 | display: none; 36 | @include severity(--sev1-color); 37 | } 38 | 39 | .showLowSevs .deoptMarker.sev1 { 40 | display: inline; 41 | } 42 | 43 | .deoptMarker.sev2 { 44 | @include severity(--sev2-color); 45 | } 46 | 47 | .deoptMarker.sev3 { 48 | @include severity(--sev3-color); 49 | } 50 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/SummaryTable.module.scss: -------------------------------------------------------------------------------- 1 | .summaryTable { 2 | text-align: center; 3 | 4 | .headers { 5 | font-size: 1.1rem; 6 | // text-transform: uppercase; 7 | 8 | th { 9 | font-weight: normal; 10 | } 11 | } 12 | 13 | .subheaders { 14 | font-size: 0.7rem; 15 | } 16 | } 17 | 18 | .summaryTable.grid { 19 | display: grid; 20 | grid-template-columns: auto repeat(9, 1fr); 21 | 22 | > thead, 23 | > tbody, 24 | tr { 25 | display: contents; 26 | } 27 | 28 | td:first-child { 29 | grid-column: 1 / 2; 30 | } 31 | 32 | .headers { 33 | .codes { 34 | grid-column: 2 / 5; 35 | } 36 | 37 | .deopts { 38 | grid-column: 5 / 8; 39 | } 40 | 41 | .ics { 42 | grid-column: 8 / 12; 43 | } 44 | } 45 | } 46 | 47 | .fileName { 48 | text-align: left; 49 | } 50 | 51 | .sev1 { 52 | color: var(--sev1-color); 53 | } 54 | 55 | .sev2 { 56 | color: var(--sev2-color); 57 | } 58 | 59 | .sev3 { 60 | font-weight: 600; 61 | color: var(--sev3-color); 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v8-deopt-viewer", 3 | "version": "0.0.0", 4 | "description": "View deoptimizations of your JavaScript in V8", 5 | "repository": "https://github.com/andrewiggins/v8-deopt-viewer", 6 | "author": "Andre Wiggins", 7 | "license": "MIT", 8 | "scripts": { 9 | "changeset": "changeset", 10 | "lint-staged": "lint-staged", 11 | "prepare": "husky install" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/andrewiggins/v8-deopt-viewer/issues" 15 | }, 16 | "homepage": "https://github.com/andrewiggins/v8-deopt-viewer", 17 | "private": true, 18 | "workspaces": [ 19 | "packages/*" 20 | ], 21 | "lint-staged": { 22 | "**/*.{js,jsx,ts,tsx,html,yml,md}": [ 23 | "prettier --write" 24 | ] 25 | }, 26 | "devDependencies": { 27 | "@changesets/cli": "^2.25.2", 28 | "husky": "^8.0.2", 29 | "lint-staged": "^13.0.4", 30 | "prettier": "^2.8.0", 31 | "tape": "^5.6.1" 32 | }, 33 | "volta": { 34 | "node": "18.12.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/v8-deopt-generate-log/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v8-deopt-generate-log 2 | 3 | ## 0.2.3 4 | 5 | ### Patch Changes 6 | 7 | - 5692a95: Update dependencies 8 | - 3331e33: Add support for parsing a v8.log stream by adding new `parseV8LogStream` API (thanks @maximelkin) 9 | 10 | ## 0.2.2 11 | 12 | ### Patch Changes 13 | 14 | - 861659f: Fix "bad argument" with Node 16.x (PR #23, thanks @marvinhagemeister!) 15 | - 861659f: Update --trace-ic flag to new --log-ic flag 16 | - b444fb4: Use new V8 flags with Chromium 17 | 18 | ## 0.2.1 19 | 20 | ### Patch Changes 21 | 22 | - b55a8d1: Fix puppeteer integration 23 | 24 | ## 0.2.0 25 | 26 | ### Minor Changes 27 | 28 | - ee774e5: Fall back to chrome-launcher if puppeteer is not found 29 | - 174b57b: Add traceMaps option to v8-deopt-generate-log 30 | 31 | ## 0.1.1 32 | 33 | ### Patch Changes 34 | 35 | - Remove http restrictions and warnings about the "--no-sandbox" flag. See commit for details 36 | 37 | ## 0.1.0 38 | 39 | ### Minor Changes 40 | 41 | - 89817c5: Initial release 42 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v8-deopt-webapp", 3 | "version": "0.5.0", 4 | "description": "View the deoptimizations in a V8 log", 5 | "main": "dist/v8-deopt-webapp.umd.js", 6 | "module": "dist/v8-deopt-webapp.mjs", 7 | "repository": "https://github.com/andrewiggins/v8-deopt-viewer", 8 | "author": "Andre Wiggins", 9 | "license": "MIT", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "build": "vite build", 15 | "dev": "vite", 16 | "start": "vite", 17 | "serve": "vite preview", 18 | "test": "vite build && node test/generateTestData.mjs" 19 | }, 20 | "devDependencies": { 21 | "@preact/preset-vite": "^2.4.0", 22 | "preact": "^10.11.3", 23 | "prismjs": "^1.29.0", 24 | "rollup-plugin-visualizer": "^5.8.3", 25 | "sass": "^1.56.1", 26 | "spectre.css": "^0.5.9", 27 | "v8-deopt-parser": "^0.4.3", 28 | "vite": "^3.2.4", 29 | "wouter-preact": "^2.9.0" 30 | }, 31 | "dependencies": { 32 | "msgpackr": "^1.8.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/utils/useHashLocation.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from "preact/hooks"; 2 | 3 | // returns the current hash location in a normalized form 4 | // (excluding the leading '#' symbol) 5 | function currentLocation() { 6 | return window.location.hash.replace(/^#/, "") || "/"; 7 | } 8 | 9 | /** 10 | * @returns {[string, (to: string) => string]} 11 | */ 12 | export function useHashLocation() { 13 | const [loc, setLoc] = useState(currentLocation()); 14 | 15 | useEffect(() => { 16 | // this function is called whenever the hash changes 17 | const handler = () => setLoc(currentLocation()); 18 | 19 | // subscribe to hash changes 20 | window.addEventListener("hashchange", handler); 21 | return () => window.removeEventListener("hashchange", handler); 22 | }, []); 23 | 24 | // remember to wrap your function with `useCallback` hook 25 | // a tiny but important optimization 26 | const navigate = useCallback((to) => (window.location.hash = to), []); 27 | 28 | return [loc, navigate]; 29 | } 30 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/src/deoptParsers.js: -------------------------------------------------------------------------------- 1 | import { unquote, MIN_SEVERITY, parseSourcePosition } from "./utils.js"; 2 | 3 | const sourcePositionRx = /^<(.+?)>(?: inlined at <(.+?)>)?$/; 4 | 5 | function parseDeoptSourceLocation(sourcePositionText) { 6 | const match = sourcePositionRx.exec(sourcePositionText); 7 | if (match) { 8 | const source = parseSourcePosition(match[1]); 9 | if (match[2]) { 10 | source.inlinedAt = parseSourcePosition(match[2]); 11 | } 12 | return source; 13 | } 14 | return parseSourcePosition(sourcePositionText); 15 | } 16 | 17 | export function getOptimizationSeverity(bailoutType) { 18 | switch (bailoutType) { 19 | case "soft": 20 | return MIN_SEVERITY; 21 | case "lazy": 22 | return MIN_SEVERITY + 1; 23 | case "eager": 24 | return MIN_SEVERITY + 2; 25 | } 26 | } 27 | 28 | export const deoptFieldParsers = [ 29 | parseInt, // timestamp 30 | parseInt, // size 31 | parseInt, // code 32 | parseInt, // inliningId 33 | parseInt, // scriptOffset 34 | unquote, // bailoutType 35 | parseDeoptSourceLocation, // deopt source location 36 | unquote, // deoptReasonText 37 | ]; 38 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | V8 Deopt Webapp Test Page 10 | 11 | 18 | 19 | 20 | 21 |
22 | 23 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andre Wiggins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/v8-deopt-viewer/bin/v8-deopt-viewer.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as path from "path"; 4 | import sade from "sade"; 5 | import run from "../src/index.js"; 6 | 7 | sade("v8-deopt-viewer [file]", true) 8 | .describe( 9 | "Generate and view deoptimizations in JavaScript code running in V8" 10 | ) 11 | .example("examples/simple/adder.js") 12 | .example("examples/html-inline/adders.html -o /tmp/directory") 13 | .example("https://google.com") 14 | .example("-i v8.log") 15 | .example("-i v8.log -o /tmp/directory") 16 | .option("-i --input", "Path to an already generated v8.log file") 17 | .option( 18 | "-o --out", 19 | "The directory to output files too", 20 | path.join(process.cwd(), "v8-deopt-viewer") 21 | ) 22 | .option( 23 | "-t --timeout", 24 | "How long in milliseconds to keep the browser open while the webpage runs", 25 | 5e3 26 | ) 27 | .option( 28 | "--keep-internals", 29 | "Don't remove NodeJS internals from the log", 30 | false 31 | ) 32 | .option("--skip-maps", "Skip tracing internal maps of V8", false) 33 | .option("--open", "Open the resulting webapp in a web browser", false) 34 | .action(run) 35 | .parse(process.argv); 36 | -------------------------------------------------------------------------------- /examples/two-modules/objects.js: -------------------------------------------------------------------------------- 1 | class Object1 { 2 | constructor(x, y) { 3 | this.x = x; 4 | this.y = y; 5 | } 6 | } 7 | 8 | class Object2 { 9 | constructor(x, y) { 10 | this.y = y; 11 | this.x = x; 12 | } 13 | } 14 | 15 | class Object3 { 16 | constructor(x, y) { 17 | this.hello = "world"; 18 | this.x = x; 19 | this.y = y; 20 | } 21 | } 22 | 23 | class Object4 { 24 | constructor(x, y) { 25 | this.x = x; 26 | this.hello = "world"; 27 | this.y = y; 28 | } 29 | } 30 | 31 | class Object5 { 32 | constructor(x, y) { 33 | this.x = x; 34 | this.y = y; 35 | this.hello = "world"; 36 | } 37 | } 38 | 39 | class Object6 { 40 | constructor(x, y) { 41 | this.hola = "mundo"; 42 | this.x = x; 43 | this.y = y; 44 | this.hello = "world"; 45 | } 46 | } 47 | 48 | class Object7 { 49 | constructor(x, y) { 50 | this.x = x; 51 | this.hola = "mundo"; 52 | this.y = y; 53 | this.hello = "world"; 54 | } 55 | } 56 | 57 | class Object8 { 58 | constructor(x, y) { 59 | this.x = x; 60 | this.y = y; 61 | this.hola = "mundo"; 62 | this.hello = "world"; 63 | } 64 | } 65 | 66 | module.exports = { 67 | Object1, 68 | Object2, 69 | Object3, 70 | Object4, 71 | Object5, 72 | Object6, 73 | Object7, 74 | Object8, 75 | }; 76 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v8-deopt-parser 2 | 3 | ## 0.4.3 4 | 5 | ### Patch Changes 6 | 7 | - 5692a95: Fix typo "unintialized" -> "uninitialized" (thanks @victor-homyakov) 8 | 9 | ## 0.4.2 10 | 11 | ### Patch Changes 12 | 13 | - e0fea98: Fix parsing baseline tier symbol in newer v8 versions 14 | 15 | ## 0.4.1 16 | 17 | ### Patch Changes 18 | 19 | - a05fe6b: Add support for parsing v8 >= 8.6 IC format with ten fields (PR #25, thanks @marvinhagemeister) 20 | - 861659f: Update parser to handle more IC states 21 | - 648c759: Replace UNKNOWN IC State with NO_FEEDBACK IC State 22 | 23 | ## 0.4.0 24 | 25 | ### Minor Changes 26 | 27 | - 8dd3f03: Change map field in ICEntry from string to number 28 | - b227331: Handle and expose IC entries with unknown severity 29 | - 8dd3f03: Change PerFileV8DeoptInfo to put file data into files field 30 | 31 | ### Patch Changes 32 | 33 | - 70e4a2b: Add file ID to FileV8DeoptInfo 34 | 35 | ## 0.3.0 36 | 37 | ### Minor Changes 38 | 39 | - 42f4223: Handle and expose IC entries with unknown severity 40 | 41 | ## 0.2.0 42 | 43 | ### Minor Changes 44 | 45 | - 65358c9: Add ability to view all IC loads for a specific location 46 | 47 | ## 0.1.0 48 | 49 | ### Minor Changes 50 | 51 | - 89817c5: Initial release 52 | -------------------------------------------------------------------------------- /examples/html-external/objects.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class Object1 { 4 | constructor(x, y) { 5 | this.x = x; 6 | this.y = y; 7 | } 8 | } 9 | 10 | class Object2 { 11 | constructor(x, y) { 12 | this.y = y; 13 | this.x = x; 14 | } 15 | } 16 | 17 | class Object3 { 18 | constructor(x, y) { 19 | this.hello = "world"; 20 | this.x = x; 21 | this.y = y; 22 | } 23 | } 24 | 25 | class Object4 { 26 | constructor(x, y) { 27 | this.x = x; 28 | this.hello = "world"; 29 | this.y = y; 30 | } 31 | } 32 | 33 | class Object5 { 34 | constructor(x, y) { 35 | this.x = x; 36 | this.y = y; 37 | this.hello = "world"; 38 | } 39 | } 40 | 41 | class Object6 { 42 | constructor(x, y) { 43 | this.hola = "mundo"; 44 | this.x = x; 45 | this.y = y; 46 | this.hello = "world"; 47 | } 48 | } 49 | 50 | class Object7 { 51 | constructor(x, y) { 52 | this.x = x; 53 | this.hola = "mundo"; 54 | this.y = y; 55 | this.hello = "world"; 56 | } 57 | } 58 | 59 | class Object8 { 60 | constructor(x, y) { 61 | this.x = x; 62 | this.y = y; 63 | this.hola = "mundo"; 64 | this.hello = "world"; 65 | } 66 | } 67 | 68 | // export { 69 | // Object1, 70 | // Object2, 71 | // Object3, 72 | // Object4, 73 | // Object5, 74 | // Object6, 75 | // Object7, 76 | // Object8, 77 | // } 78 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/theme.module.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --sev1-color: #5ac878; 3 | --sev2-color: #4cb5ff; 4 | --sev3-color: #ff6040; 5 | } 6 | 7 | /* Color Theme Swatches in Hex */ 8 | .Glowing-Blue-1-hex { 9 | color: #230a59; 10 | } 11 | .Glowing-Blue-2-hex { 12 | color: #3e38f2; 13 | } 14 | .Glowing-Blue-3-hex { 15 | color: #0029fa; 16 | } 17 | .Glowing-Blue-4-hex { 18 | color: #5c73f2; 19 | } 20 | .Glowing-Blue-5-hex { 21 | color: #829fd9; 22 | } 23 | 24 | /* Color Theme Swatches in HSLA */ 25 | .Glowing-Blue-1-hsla { 26 | color: hsla(258, 79%, 19%, 1); 27 | } 28 | .Glowing-Blue-2-hsla { 29 | color: hsla(241, 87%, 58%, 1); 30 | } 31 | .Glowing-Blue-3-hsla { 32 | color: hsla(230, 100%, 49%, 1); 33 | } 34 | .Glowing-Blue-4-hsla { 35 | color: hsla(230, 85%, 65%, 1); 36 | } 37 | .Glowing-Blue-5-hsla { 38 | color: hsla(219, 53%, 68%, 1); 39 | } 40 | 41 | /* https://colorhunt.co/palette/167893 */ 42 | .dark-blue-1 { 43 | color: #1b262c; 44 | } 45 | .dark-blue-2 { 46 | color: #0f4c75; 47 | } 48 | .dark-blue-3 { 49 | color: #3282b8; 50 | } 51 | .dark-blue-4 { 52 | color: #bbe1fa; 53 | } 54 | 55 | /* https://colordrop.io/color/17949 */ 56 | .aqua-2 { 57 | color: #08ffc8; 58 | } 59 | .aqua-2 { 60 | color: #fff7f7; 61 | } 62 | .aqua-2 { 63 | color: #dadada; 64 | } 65 | .aqua-2 { 66 | color: #204969; 67 | } 68 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/parseLogs.js: -------------------------------------------------------------------------------- 1 | import { readdir } from "fs/promises"; 2 | import { pkgRoot, runParser, writeSnapshot } from "./helpers.js"; 3 | import { validateMapData, writeMapSnapshot } from "./traceMapsHelpers.js"; 4 | 5 | // This file is used to run v8-deopt-viewer on v8-deopt-parser itself :) 6 | 7 | const t = { 8 | /** 9 | * @param {any} actual 10 | * @param {any} expected 11 | * @param {string} [message] 12 | */ 13 | equal(actual, expected, message) { 14 | if (actual !== expected) { 15 | const errorMessage = `${message}: Actual (${actual}) does not equal expected (${expected}).`; 16 | console.error(errorMessage); 17 | // throw new Error(errorMessage); 18 | } 19 | }, 20 | }; 21 | 22 | async function main() { 23 | // const logFileNames = await readdir(pkgRoot("test/logs")); 24 | // for (let logFileName of logFileNames) { 25 | // await runParser(t, logFileName); 26 | // } 27 | 28 | // const logFileName = "html-inline.traceMaps.v8.log"; 29 | // const logFileName = "v8-deopt-parser.v8.log"; 30 | const logFileName = "adders.traceMaps.v8.log"; 31 | 32 | // @ts-ignore 33 | const results = await runParser(t, logFileName); 34 | await writeSnapshot(logFileName, results); 35 | await writeMapSnapshot(logFileName, results); 36 | validateMapData(t, results); 37 | } 38 | 39 | main(); 40 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/CodeSettings.module.scss: -------------------------------------------------------------------------------- 1 | :global { 2 | details > summary::-webkit-details-marker { 3 | display: none; 4 | } 5 | } 6 | 7 | .codeSettings > summary { 8 | position: relative; 9 | z-index: 400; // So it is above the dropdown menu 10 | 11 | width: 32px; 12 | height: 32px; 13 | list-style: none; 14 | 15 | background: radial-gradient(#fff, #0000); 16 | border-radius: 50%; 17 | 18 | cursor: pointer; 19 | 20 | svg { 21 | transition: transform 0.5s ease-out; 22 | fill: #ccc; 23 | } 24 | 25 | // .form-switch .form-icon { 26 | // box-sizing: border-box; // Not sure why this isn't inherited 27 | // } 28 | 29 | &:hover, 30 | &:focus { 31 | background-color: #f1f1fc; 32 | 33 | svg { 34 | fill: #5755d9; 35 | } 36 | } 37 | 38 | &:focus { 39 | outline: rgba(87, 85, 217, 0.2) solid 0.1rem; 40 | } 41 | 42 | &::-webkit-details-marker { 43 | display: none; 44 | } 45 | } 46 | 47 | .codeSettings[open] { 48 | summary svg { 49 | transform: rotate(270deg); 50 | fill: #5755d9; 51 | } 52 | 53 | > .menu { 54 | display: block; 55 | } 56 | } 57 | 58 | .codeSettings.dirty { 59 | summary svg { 60 | fill: #5755d9; 61 | } 62 | } 63 | 64 | .settingsBody { 65 | position: absolute; 66 | right: 0; 67 | } 68 | 69 | .settingsMenu { 70 | min-width: 224px; 71 | 72 | @media (max-width: 580px) { 73 | min-width: 130px; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/V8DeoptInfoPanel/MapExplorer.module.scss: -------------------------------------------------------------------------------- 1 | .map_selectors { 2 | display: flex; 3 | flex-wrap: wrap; 4 | column-gap: 1rem; 5 | // Explore more at: https://css-tricks.com/almanac/properties/b/box-shadow/ 6 | // box-shadow: 0 10px 6px -6px #ddd 7 | 8 | > .grouping { 9 | flex: 1 1 auto; 10 | } 11 | 12 | > .group_value { 13 | flex: 1 1 auto; 14 | } 15 | 16 | > .map_ids { 17 | flex: 1 1 auto; 18 | } 19 | } 20 | 21 | .map_details > summary { 22 | cursor: pointer; 23 | } 24 | 25 | .goto_loc_btn { 26 | padding: 0; 27 | float: right; 28 | height: auto; 29 | } 30 | 31 | .goto_loc_btn:disabled { 32 | pointer-events: auto; 33 | } 34 | 35 | .map_details > summary.map_title { 36 | &:hover, 37 | &:focus { 38 | color: #5755d9; 39 | text-decoration: underline; 40 | } 41 | 42 | &.selected { 43 | font-weight: bold; 44 | } 45 | } 46 | 47 | .icon_triangle_up::before { 48 | width: 0; 49 | height: 0; 50 | border-left: 6px solid transparent; 51 | border-right: 6px solid transparent; 52 | border-bottom: 12px solid white; 53 | } 54 | 55 | .icon_triangle_down::before { 56 | width: 0; 57 | height: 0; 58 | border-left: 6px solid transparent; 59 | border-right: 6px solid transparent; 60 | border-top: 12px solid white; 61 | } 62 | 63 | .icon_double_bars::before { 64 | width: 8px; 65 | height: 0.8rem; 66 | border-left: 2px solid #fff; 67 | border-right: 2px solid #fff; 68 | } 69 | -------------------------------------------------------------------------------- /packages/v8-deopt-generate-log/README.md: -------------------------------------------------------------------------------- 1 | # v8-deopt-generate-log 2 | 3 | Given a JavaScript file or URL, run the file or webpage and save a log of V8 optimizations and deoptimizations. 4 | 5 | ## Installation 6 | 7 | > Check out [`v8-deopt-viewer`](https://npmjs.com/package/v8-deopt-viewer) for a CLI that automates this for you! 8 | 9 | Requires [NodeJS](https://nodejs.org) 14.x or greater. 10 | 11 | ``` 12 | npm i v8-deopt-generate-log 13 | ``` 14 | 15 | Also install [`puppeteer`](https://github.com/GoogleChrome/puppeteer) if you plan to generate logs for URLs or HTML files: 16 | 17 | ```bash 18 | npm i puppeteer 19 | ``` 20 | 21 | ## Usage 22 | 23 | See [`index.d.ts`](src/index.d.ts) for the latest API. A snapshot is below. 24 | 25 | ```typescript 26 | interface Options { 27 | /** Path to store the V8 log file. Defaults to your OS temporary directory */ 28 | logFilePath?: string; 29 | 30 | /** 31 | * How long the keep the browser open to allow the webpage to run before 32 | * closing the browser 33 | */ 34 | browserTimeoutMs?: number; 35 | } 36 | 37 | /** 38 | * Generate a V8 log of optimizations and deoptimizations for the given JS or 39 | * HTML file 40 | * @param srcPath The path or URL to run 41 | * @param options Options to influence how the log is generated 42 | * @returns The path to the generated V8 log file 43 | */ 44 | export async function generateV8Log( 45 | srcPath: string, 46 | options?: Options 47 | ): Promise; 48 | ``` 49 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v8-deopt-webapp 2 | 3 | ## 0.5.0 4 | 5 | ### Minor Changes 6 | 7 | - d9b96e8: Fix log big size #62 (thanks @Nadya2002) 8 | 9 | ### Patch Changes 10 | 11 | - 5692a95: Update dependencies 12 | - d9b96e8: Fix the search for "v8-deopt-webapp" module #59 (thanks @Nadya2002) 13 | - 71d5625: Fix Prism.css styles 14 | - 5692a95: Fix endless rerenders in MapExplorer (thanks @victor-homyakov) 15 | - 5692a95: Build webapp using vite 16 | 17 | ## 0.4.3 18 | 19 | ### Patch Changes 20 | 21 | - 861659f: Fix highlight + annotation of .mjs files (PR #27, thanks @developit!) 22 | - 307360f: Handle file with no maps correctly 23 | 24 | ## 0.4.2 25 | 26 | ### Patch Changes 27 | 28 | - Fix infinite loop in MapExplorer 29 | 30 | ## 0.4.1 31 | 32 | ### Patch Changes 33 | 34 | - 7846cf9: Fix ICEntry MapExplorer links 35 | 36 | ## 0.4.0 37 | 38 | ### Minor Changes 39 | 40 | - 80b75d3: Add MapExplorer tab to v8-deopt-viewer 41 | - b227331: Handle and expose IC entries with unknown severity 42 | 43 | ## 0.3.0 44 | 45 | ### Minor Changes 46 | 47 | - 42f4223: Handle and expose IC entries with unknown severity 48 | 49 | ## 0.2.0 50 | 51 | ### Minor Changes 52 | 53 | - 65358c9: Add ability to view all IC loads for a specific location 54 | 55 | ### Patch Changes 56 | 57 | - 701d23c: Fix hiding line numbers 58 | - c946b7a: Add selected filename to FileView 59 | 60 | ## 0.1.0 61 | 62 | ### Minor Changes 63 | 64 | - 89817c5: Initial release 65 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/snapshots/html-inline.traceMaps.mapTree.txt: -------------------------------------------------------------------------------- 1 | └─InitialMap Object1 [0xea08283f29] 2 | └─+x [0xea08283fa1] 3 | └─+y [0xea08283fc9] 4 | 5 | └─InitialMap Object2 [0xea08283ff1] 6 | └─+y [0xea08284069] 7 | └─+x [0xea08284091] 8 | 9 | └─InitialMap Object3 [0xea082840b9] 10 | └─+hello [0xea08284131] 11 | └─+x [0xea08284159] 12 | └─+y [0xea08284181] 13 | 14 | └─InitialMap Object4 [0xea082841a9] 15 | └─+x [0xea08284221] 16 | └─+hello [0xea08284249] 17 | └─+y [0xea08284271] 18 | 19 | └─InitialMap Object5 [0xea08284299] 20 | └─+x [0xea08284311] 21 | └─+y [0xea08284339] 22 | └─+hello [0xea08284361] 23 | 24 | └─InitialMap Object6 [0xea08284389] 25 | └─+hola [0xea08284401] 26 | └─+x [0xea08284429] 27 | └─+y [0xea08284451] 28 | └─+hello [0xea08284479] 29 | 30 | └─InitialMap Object7 [0xea082844a1] 31 | └─+x [0xea08284519] 32 | └─+hola [0xea08284541] 33 | └─+y [0xea08284569] 34 | └─+hello [0xea08284591] 35 | 36 | └─InitialMap Object8 [0xea082845b9] 37 | └─+x [0xea08284631] 38 | └─+y [0xea08284659] 39 | └─+hola [0xea08284681] 40 | └─+hello [0xea082846a9] 41 | 42 | └─0xea08280329 43 | └─ReplaceDescriptors MapCreate [0xea08283ed9] 44 | └─+flag [0xea08283f01] 45 | 46 | └─0xea08281139 47 | 48 | └─0xea08281049 49 | 50 | 51 | 52 | Total Map Count : 38 53 | Total Edge Count: 35 54 | 55 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/snapshots/adders.traceMaps.mapTree.txt: -------------------------------------------------------------------------------- 1 | └─InitialMap Object1 [0x3533d3ba069] 2 | └─+x [0x3533d3ba141] 3 | └─+y [0x3533d3ba189] 4 | 5 | └─InitialMap Object2 [0x3533d3ba1d1] 6 | └─+y [0x3533d3ba2a9] 7 | └─+x [0x3533d3ba2f1] 8 | 9 | └─InitialMap Object3 [0x3533d3ba339] 10 | └─+hello [0x3533d3ba411] 11 | └─+x [0x3533d3ba459] 12 | └─+y [0x3533d3ba4a1] 13 | 14 | └─InitialMap Object4 [0x3533d3ba4e9] 15 | └─+x [0x3533d3ba5c1] 16 | └─+hello [0x3533d3ba609] 17 | └─+y [0x3533d3ba651] 18 | 19 | └─InitialMap Object5 [0x3533d3ba699] 20 | └─+x [0x3533d3ba771] 21 | └─+y [0x3533d3ba7b9] 22 | └─+hello [0x3533d3ba801] 23 | 24 | └─InitialMap Object6 [0x3533d3ba849] 25 | └─+hola [0x3533d3ba921] 26 | └─+x [0x3533d3ba969] 27 | └─+y [0x3533d3ba9b1] 28 | └─+hello [0x3533d3ba9f9] 29 | 30 | └─InitialMap Object7 [0x3533d3baa41] 31 | └─+x [0x3533d3bab19] 32 | └─+hola [0x3533d3bab61] 33 | └─+y [0x3533d3baba9] 34 | └─+hello [0x3533d3babf1] 35 | 36 | └─InitialMap Object8 [0x3533d3bac39] 37 | └─+x [0x3533d3bad11] 38 | └─+y [0x3533d3bad59] 39 | └─+hola [0x3533d3bada1] 40 | └─+hello [0x3533d3bade9] 41 | 42 | └─0x3533d380439 43 | └─ReplaceDescriptors MapCreate [0x3533d3a7019] 44 | └─+flag [0x3533d3b9601] 45 | 46 | └─0x3533d382e69 47 | 48 | └─0x3533d382d01 49 | 50 | 51 | 52 | Total Map Count : 38 53 | Total Edge Count: 35 54 | 55 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/snapshots/html-external.traceMaps.mapTree.txt: -------------------------------------------------------------------------------- 1 | └─InitialMap Object1 [0x2ec008287511] 2 | └─+x [0x2ec008287589] 3 | └─+y [0x2ec0082875b1] 4 | 5 | └─InitialMap Object2 [0x2ec0082875d9] 6 | └─+y [0x2ec008287651] 7 | └─+x [0x2ec008287679] 8 | 9 | └─InitialMap Object3 [0x2ec0082876a1] 10 | └─+hello [0x2ec008287719] 11 | └─+x [0x2ec008287741] 12 | └─+y [0x2ec008287769] 13 | 14 | └─0x2ec008280329 15 | └─ReplaceDescriptors MapCreate [0x2ec008287499] 16 | └─+flag [0x2ec0082874c1] 17 | 18 | └─InitialMap Object4 [0x2ec008287791] 19 | └─+x [0x2ec008287809] 20 | └─+hello [0x2ec008287831] 21 | └─+y [0x2ec008287859] 22 | 23 | └─InitialMap Object5 [0x2ec008287881] 24 | └─+x [0x2ec0082878f9] 25 | └─+y [0x2ec008287921] 26 | └─+hello [0x2ec008287949] 27 | 28 | └─InitialMap Object6 [0x2ec008287971] 29 | └─+hola [0x2ec0082879e9] 30 | └─+x [0x2ec008287a11] 31 | └─+y [0x2ec008287a39] 32 | └─+hello [0x2ec008287a61] 33 | 34 | └─InitialMap Object7 [0x2ec008287a89] 35 | └─+x [0x2ec008287b01] 36 | └─+hola [0x2ec008287b29] 37 | └─+y [0x2ec008287b51] 38 | └─+hello [0x2ec008287b79] 39 | 40 | └─InitialMap Object8 [0x2ec008287ba1] 41 | └─+x [0x2ec008287c19] 42 | └─+y [0x2ec008287c41] 43 | └─+hola [0x2ec008287c69] 44 | └─+hello [0x2ec008287c91] 45 | 46 | └─0x2ec008281139 47 | 48 | └─0x2ec008281049 49 | 50 | 51 | 52 | Total Map Count : 38 53 | Total Edge Count: 35 54 | 55 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "preact"; 2 | import { Router, Route, useRoute } from "wouter-preact"; 3 | import { Summary } from "./Summary"; 4 | import { useHashLocation } from "../utils/useHashLocation"; 5 | import { fileRoute, summaryRoute } from "../routes"; 6 | import { FileViewer } from "./FileViewer"; 7 | import { btn, icon, icon_back } from "../spectre.module.scss"; 8 | import { pageHeader, backButton, subRoute, pageTitle } from "./App.module.scss"; 9 | 10 | /** 11 | * @param {import('..').AppProps} props 12 | */ 13 | export function App({ deoptInfo }) { 14 | const files = Object.keys(deoptInfo.files); 15 | 16 | return ( 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | {(params) => ( 25 | 33 | )} 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | function Header() { 41 | const [isRootRoute] = useRoute("/"); 42 | 43 | return ( 44 |
45 | 46 | 47 | 48 |

V8 Deopt Viewer

49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/V8DeoptInfoPanel/index.jsx: -------------------------------------------------------------------------------- 1 | import { useRoute } from "wouter-preact"; 2 | import { codeRoute, deoptsRoute, icsRoute, mapsRoute } from "../../routes.js"; 3 | import { 4 | panel, 5 | panel_header, 6 | panel_nav, 7 | tab, 8 | tab_block, 9 | tab_item, 10 | active, 11 | panel_body, 12 | } from "../../spectre.module.scss"; 13 | import { v8deoptInfoPanel, panel_title, tabLink } from "./index.module.scss"; 14 | 15 | const routes = [codeRoute, deoptsRoute, icsRoute, mapsRoute]; 16 | 17 | /** 18 | * @typedef {{ title: string; fileId: number; children: import('preact').JSX.Element; }} V8DeoptInfoPanelProps 19 | * @param {V8DeoptInfoPanelProps} props 20 | */ 21 | export function V8DeoptInfoPanel({ title, fileId, children }) { 22 | return ( 23 |
24 |
25 |

{title}

26 |
27 | 34 |
{children}
35 |
36 | ); 37 | } 38 | 39 | /** 40 | * @param {{ fileId: number; route: import('../..').Route; }} props 41 | */ 42 | function TabLink({ fileId, route }) { 43 | const href = route.getHref(fileId); 44 | const [isActive] = useRoute(route.route); 45 | const liClass = [tab_item, isActive ? active : null].join(" "); 46 | 47 | return ( 48 |
  • 49 | 50 | {route.title} 51 | 52 |
  • 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/src/optimizationStateParsers.js: -------------------------------------------------------------------------------- 1 | import { Profile } from "./v8-tools-core/profile.js"; 2 | import { MIN_SEVERITY, UNKNOWN_SEVERITY } from "./utils.js"; 3 | 4 | export const UNKNOWN_OPT_STATE = -1; 5 | 6 | export function parseOptimizationState(rawState) { 7 | switch (rawState) { 8 | case "": 9 | return Profile.CodeState.COMPILED; 10 | case "~": 11 | return Profile.CodeState.OPTIMIZABLE; 12 | case "*": 13 | return Profile.CodeState.OPTIMIZED; 14 | case "^": 15 | return Profile.CodeState.BASELINE; 16 | default: 17 | throw new Error("unknown code state: " + rawState); 18 | } 19 | } 20 | 21 | /** 22 | * @param {number} state 23 | * @returns {import('./index').CodeState} 24 | */ 25 | export function nameOptimizationState(state) { 26 | switch (state) { 27 | case Profile.CodeState.COMPILED: 28 | return "compiled"; 29 | case Profile.CodeState.OPTIMIZABLE: 30 | return "optimizable"; 31 | case Profile.CodeState.OPTIMIZED: 32 | return "optimized"; 33 | case Profile.CodeState.BASELINE: 34 | return "baseline"; 35 | case UNKNOWN_OPT_STATE: 36 | return "unknown"; 37 | default: 38 | throw new Error("unknown code state: " + state); 39 | } 40 | } 41 | 42 | export function severityOfOptimizationState(state) { 43 | switch (state) { 44 | case Profile.CodeState.COMPILED: 45 | return MIN_SEVERITY + 2; 46 | case Profile.CodeState.OPTIMIZABLE: 47 | return MIN_SEVERITY + 1; 48 | case Profile.CodeState.OPTIMIZED: 49 | return MIN_SEVERITY; 50 | case Profile.CodeState.BASELINE: 51 | case UNKNOWN_OPT_STATE: 52 | return UNKNOWN_SEVERITY; 53 | default: 54 | throw new Error("unknown code state: " + state); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/Summary.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "preact"; 2 | import { useMemo } from "preact/hooks"; 3 | // import { SummaryList } from "./SummaryList.jsx"; 4 | import { SummaryTable } from "./SummaryTable.jsx"; 5 | 6 | /** 7 | * @typedef {[number, number, number]} SeveritySummary 8 | * @typedef {{ codes: SeveritySummary; deopts: SeveritySummary; ics: SeveritySummary }} FileSeverities 9 | * @typedef {Record} PerFileStats 10 | * @param {import('..').AppProps["deoptInfo"]} deoptInfo 11 | * @returns {PerFileStats} 12 | */ 13 | function getPerFileStats(deoptInfo) { 14 | /** @type {PerFileStats} */ 15 | const results = {}; 16 | 17 | const files = Object.keys(deoptInfo.files); 18 | for (let fileName of files) { 19 | const fileDepotInfo = deoptInfo.files[fileName]; 20 | results[fileName] = { 21 | codes: [0, 0, 0], 22 | deopts: [0, 0, 0], 23 | ics: [0, 0, 0], 24 | }; 25 | 26 | for (let kind of ["codes", "deopts", "ics"]) { 27 | const entries = fileDepotInfo[kind]; 28 | for (let entry of entries) { 29 | results[fileName][kind][entry.severity - 1]++; 30 | } 31 | } 32 | } 33 | 34 | return results; 35 | } 36 | 37 | /** 38 | * @typedef {{ deoptInfo: import('..').PerFileDeoptInfoWithSources; perFileStats: PerFileStats }} SummaryProps 39 | * @param {import('..').AppProps} props 40 | */ 41 | export function Summary({ deoptInfo }) { 42 | const perFileStats = useMemo(() => getPerFileStats(deoptInfo), [deoptInfo]); 43 | 44 | return ( 45 | 46 | {/* */} 47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Basic process for contributing: 4 | 5 | 1. Fork repo and create local branch 6 | 2. Make and commit changes 7 | 3. If you think a change you make should have a changelog entry (can run multiple times for multiple entries), run `npm run changeset` and answer the questions about the changes you are making 8 | 4. Open a pull request with your changes 9 | 10 | ## Organization 11 | 12 | ### Packages 13 | 14 | This project is a monorepo. Check each sub-repo's `Readme.md` for details about contributing to each project, but for convenience, below is a quick description of each sub-repo. 15 | 16 | - `v8-deopt-generate-log`: Given a JS or HTML file, generate a log file of V8's deoptimizations. Uses NodeJS or puppeteer to generate the log 17 | - `v8-deopt-parser`: Parses a V8 log into a data structure containing relevant deoptimization info 18 | - `v8-deopt-viewer`: Command-line tool to automate generating, parsing, and displaying V8's deoptimizations 19 | - `v8-deopt-webapp`: Webapp to display parsed V8 logs 20 | 21 | Quick thoughts: 22 | 23 | - `v8-deopt-parser` package should work in the browser and nodeJS and should correctly parse Linux, Windows, file: protocol, and http(s): protocol paths 24 | - `v8-deopt-parser` uses `tape` for testing because it easily works with NodeJS ES Modules and runs easily runs natively in the browser (?does it?) 25 | 26 | ## Releasing 27 | 28 | 1. Run `npm run changeset -- version` 29 | 2. Commit changes and push to master 30 | 3. Run `npm run changeset -- publish` to publish changes 31 | Make sure no commits exist between the commit in step 2 and the publish command 32 | 4. Run `git push --follow-tags` to publish the new tags 33 | 5. Create a GitHub release from the new tag 34 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/logs/brotli.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import zlib from "zlib"; 3 | import sade from "sade"; 4 | 5 | /** 6 | * @param {string} inputPath 7 | * @param {{ quality?: number }} opts 8 | */ 9 | async function run(inputPath, opts) { 10 | let output, brotli; 11 | if (inputPath.endsWith(".br")) { 12 | output = fs.createWriteStream(inputPath.replace(/.br$/, "")); 13 | brotli = zlib.createBrotliDecompress(); 14 | } else { 15 | let quality; 16 | if (opts.quality < zlib.constants.BROTLI_MIN_QUALITY) { 17 | throw new Error( 18 | `Passed in quality value (${opts.quality}) is less than the min brotli quality allowed (${zlib.constants.BROTLI_MIN_QUALITY})` 19 | ); 20 | } else if (opts.quality > zlib.constants.BROTLI_MAX_QUALITY) { 21 | throw new Error( 22 | `Passed in quality value (${opts.quality}) is grater than the max brotli quality allowed (${zlib.constants.BROTLI_MAX_QUALITY})` 23 | ); 24 | } else { 25 | quality = opts.quality; 26 | } 27 | 28 | output = fs.createWriteStream(inputPath + ".br"); 29 | brotli = zlib.createBrotliCompress({ 30 | params: { 31 | [zlib.constants.BROTLI_PARAM_QUALITY]: quality, 32 | }, 33 | }); 34 | } 35 | 36 | fs.createReadStream(inputPath).pipe(brotli).pipe(output); 37 | } 38 | 39 | sade("brotli ", true) 40 | .describe("Compress a text file or decompress a brotli (.br) file") 41 | .option( 42 | "-q --quality", 43 | "Quality of compression. Must be between NodeJS's zlib.constants.BROTLI_MIN_QUALITY and zlib.constants.BROTLI_MAX_QUALITY. Default is MAX_QUALITY. (default 11)", 44 | zlib.constants.BROTLI_MAX_QUALITY 45 | ) 46 | .example("compressed.txt.br") 47 | .example("plain_text.txt -q 9") 48 | .action(run) 49 | .parse(process.argv); 50 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/CodePanel.module.scss: -------------------------------------------------------------------------------- 1 | :global { 2 | //******************************* 3 | // Prism overrides 4 | //******************************* 5 | pre[class*="language-"] { 6 | overflow: visible; // Allow CodePanel styles to handle overflow 7 | } 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | tab-size: 2; // Override Prism default tab-size 12 | } 13 | 14 | code[class*="language-"] { 15 | padding: 0; // Override spectre styles to not conflict with Prism 16 | padding-top: 0.1rem; 17 | } 18 | 19 | .line-numbers-rows { 20 | display: none; // By default don't show the rows unless the parent "line-numbers" class is presents 21 | } 22 | 23 | // Custom overrides for line-numbers to work with our SSR rendering 24 | .line-numbers .line-numbers-rows { 25 | display: block; 26 | top: 1em; 27 | left: 0; 28 | } 29 | 30 | // Hide line-numbers in small screens 31 | // @media (max-width: 600px) { 32 | // pre[class*="language-"].line-numbers { 33 | // padding-left: 1em; 34 | // } 35 | // 36 | // .line-numbers .line-numbers-rows { 37 | // display: none; 38 | // } 39 | // } 40 | 41 | .line-numbers .line-numbers-rows .active { 42 | background-color: yellow; 43 | } 44 | } 45 | 46 | .codePanel { 47 | overflow: auto; 48 | border: 0.05rem solid #dadee4; 49 | border-right: none; 50 | border-radius: 0.1rem; 51 | border-top-right-radius: 0; 52 | // scroll-padding-top: 50%; // Using scrollIntoView + block:center option instead 53 | 54 | background-color: #f5f2f0; // Make sure background is consistent on very small and very large screens. 55 | } 56 | 57 | .codePanel > pre { 58 | margin: 0; 59 | } 60 | 61 | .codePanel.error { 62 | padding: 0.5rem; 63 | padding-right: 60px; 64 | } 65 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/src/index.js: -------------------------------------------------------------------------------- 1 | import { DeoptLogReader } from "./DeoptLogReader.js"; 2 | 3 | /** 4 | * @param {string} v8LogContent 5 | * @param {import('.').Options} [options] 6 | * @returns {Promise} 7 | */ 8 | export async function parseV8Log(v8LogContent, options = {}) { 9 | v8LogContent = v8LogContent.replace(/\r\n/g, "\n"); 10 | 11 | const logReader = new DeoptLogReader(options); 12 | logReader.processLogChunk(v8LogContent); 13 | return logReader.toJSON(); 14 | } 15 | 16 | /** 17 | * @param {Generator>} v8LogStream 18 | * @param {import('.').Options} [options] 19 | * @returns {Promise} 20 | */ 21 | export async function parseV8LogStream(v8LogStream, options = {}) { 22 | const logReader = new DeoptLogReader(options); 23 | 24 | // we receive chunks of strings, but chunks split at random places, not \n 25 | // so, lets keep leftovers from previous steps and concat them with current block 26 | let leftOver = ''; 27 | for await (const chunk of v8LogStream) { 28 | const actualChunk = (leftOver + chunk).replace(/\r\n/g, "\n"); 29 | 30 | const lastLineBreak = actualChunk.lastIndexOf('\n'); 31 | if (lastLineBreak !== -1) { 32 | logReader.processLogChunk(actualChunk.slice(0, lastLineBreak)); 33 | leftOver = actualChunk.slice(lastLineBreak + 1); // skip \n 34 | } else { 35 | leftOver = actualChunk; // nothing processed at this step, save for later processing 36 | } 37 | } 38 | 39 | if (leftOver.length > 0) { 40 | logReader.processLogChunk(leftOver); 41 | } 42 | return logReader.toJSON(); 43 | } 44 | 45 | // TODO: Consider rewriting v8-tools-core to be tree-shakeable 46 | export { groupByFile } from "./groupBy.js"; 47 | export { findEntry } from "./findEntry.js"; 48 | export { sortEntries } from "./sortEntries.js"; 49 | export { severityIcState } from "./propertyICParsers.js"; 50 | export { MIN_SEVERITY, UNKNOWN_SEVERITY } from "./utils.js"; 51 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/routes.js: -------------------------------------------------------------------------------- 1 | /** @type {import('./').Route<[]>} */ 2 | export const summaryRoute = { 3 | id: "summary", 4 | route: "/", 5 | getHref() { 6 | return "#" + this.route; 7 | }, 8 | }; 9 | 10 | /** @type {import('./').Route<[number]>} */ 11 | export const fileRoute = { 12 | id: "file", 13 | route: "/file/:fileId/:tabId?/:path*", 14 | getHref(fileId = null, tabId = null) { 15 | let url = "#/file/"; 16 | if (fileId) { 17 | url += fileId + "/"; 18 | 19 | if (tabId) { 20 | url += tabId + "/"; 21 | } 22 | } 23 | 24 | return url; 25 | }, 26 | }; 27 | 28 | /** @type {import('./').Route<[number, string?]>} */ 29 | export const codeRoute = { 30 | id: "codes", 31 | title: "Optimizations", 32 | route: "/file/:fileId/codes/:entryId?", 33 | getHref(fileId, entryId = "") { 34 | return `#/file/${fileId}/codes/${entryId}`; 35 | }, 36 | }; 37 | 38 | /** @type {import('./').Route<[number, string?]>} */ 39 | export const deoptsRoute = { 40 | id: "deopts", 41 | title: "Deoptimizations", 42 | route: "/file/:fileId/deopts/:entryId?", 43 | getHref(fileId, entryId = "") { 44 | return `#/file/${fileId}/deopts/${entryId}`; 45 | }, 46 | }; 47 | 48 | /** @type {import('./').Route<[number, string?]>} */ 49 | export const icsRoute = { 50 | id: "ics", 51 | title: "Inline Caches", 52 | route: "/file/:fileId/ics/:entryId?", 53 | getHref(fileId, entryId = "") { 54 | return `#/file/${fileId}/ics/${entryId}`; 55 | }, 56 | }; 57 | 58 | /** @type {import('./').Route<[number, string?, string?, string?]>} */ 59 | export const mapsRoute = { 60 | id: "maps", 61 | title: "Map Explorer", 62 | route: "/file/:fileId/maps/:grouping?/:groupValue?/:mapId?", 63 | getHref(fileId, grouping = null, groupValue = null, mapId = null) { 64 | let url = `#/file/${fileId}/maps/`; 65 | // Only add subsequent paths if parent path is provided 66 | if (grouping) { 67 | url += encodeURIComponent(grouping) + "/"; 68 | 69 | if (groupValue) { 70 | url += encodeURIComponent(groupValue) + "/"; 71 | 72 | if (mapId) { 73 | url += encodeURIComponent(mapId); 74 | } 75 | } 76 | } 77 | 78 | return url; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /packages/v8-deopt-viewer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v8-deopt-viewer 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - d9b96e8: Fix log big size #62 (thanks @Nadya2002) 8 | 9 | ### Patch Changes 10 | 11 | - 5692a95: Update dependencies 12 | - d9b96e8: Fix the search for "v8-deopt-webapp" module #59 (thanks @Nadya2002) 13 | - Updated dependencies [5692a95] 14 | - Updated dependencies [d9b96e8] 15 | - Updated dependencies [5692a95] 16 | - Updated dependencies [3331e33] 17 | - Updated dependencies [71d5625] 18 | - Updated dependencies [5692a95] 19 | - Updated dependencies [d9b96e8] 20 | - Updated dependencies [5692a95] 21 | - v8-deopt-generate-log@0.2.3 22 | - v8-deopt-webapp@0.5.0 23 | - v8-deopt-parser@0.4.3 24 | 25 | ## 0.2.1 26 | 27 | ### Patch Changes 28 | 29 | - Ensure output directory exists before writing to it 30 | 31 | ## 0.2.0 32 | 33 | ### Minor Changes 34 | 35 | - 80b75d3: Add MapExplorer tab to v8-deopt-viewer 36 | 37 | ### Patch Changes 38 | 39 | - Updated dependencies [80b75d3] 40 | - Updated dependencies [8dd3f03] 41 | - Updated dependencies [b227331] 42 | - Updated dependencies [8dd3f03] 43 | - Updated dependencies [70e4a2b] 44 | - v8-deopt-webapp@0.4.0 45 | - v8-deopt-parser@0.4.0 46 | 47 | ## 0.1.4 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies [42f4223] 52 | - v8-deopt-webapp@0.3.0 53 | - v8-deopt-parser@0.3.0 54 | 55 | ## 0.1.3 56 | 57 | ### Patch Changes 58 | 59 | - Updated dependencies [ee774e5] 60 | - Updated dependencies [701d23c] 61 | - Updated dependencies [174b57b] 62 | - Updated dependencies [c946b7a] 63 | - Updated dependencies [65358c9] 64 | - v8-deopt-generate-log@0.2.0 65 | - v8-deopt-webapp@0.2.0 66 | - v8-deopt-parser@0.2.0 67 | 68 | ## 0.1.2 69 | 70 | ### Patch Changes 71 | 72 | - Remove http restrictions and warnings about the "--no-sandbox" flag. See commit for details 73 | - Updated dependencies [undefined] 74 | - v8-deopt-generate-log@0.1.1 75 | 76 | ## 0.1.1 77 | 78 | ### Patch Changes 79 | 80 | - Fix v8-deopt-viewer bin field 81 | 82 | ## 0.1.0 83 | 84 | ### Minor Changes 85 | 86 | - 89817c5: Initial release 87 | 88 | ### Patch Changes 89 | 90 | - Updated dependencies [89817c5] 91 | - v8-deopt-generate-log@0.1.0 92 | - v8-deopt-parser@0.1.0 93 | - v8-deopt-webapp@0.1.0 94 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/SummaryList.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "preact"; 2 | import { 3 | CodeTableHeaders, 4 | SeverityTableHeaders, 5 | SeverityTableSummary, 6 | } from "./SummaryTable.jsx"; 7 | import { table } from "../spectre.module.scss"; 8 | import { 9 | globalHeaders, 10 | summaryList, 11 | codes, 12 | deopts, 13 | ics, 14 | severityTable, 15 | } from "./SummaryList.module.scss"; 16 | 17 | // TODO: 18 | // - Consider putting each file into a Spectre Panel. 19 | // - Consider using Panel tabs for each of the classifications. Maybe make the list a two column "field: value" list 20 | // but current header text is too big for Panel tabs 21 | 22 | /** 23 | * @param {import('./Summary').SummaryProps} props 24 | */ 25 | export function SummaryList({ deoptInfo, perFileStats }) { 26 | return ( 27 | 28 |
    29 |
      30 | {Object.keys(perFileStats).map((fileName, i) => { 31 | const summaryInfo = perFileStats[fileName]; 32 | 33 | return ( 34 |
    • 35 | 38 | } 43 | /> 44 | } 49 | /> 50 | } 55 | /> 56 |
    • 57 | ); 58 | })} 59 |
    60 |
    61 | ); 62 | } 63 | 64 | function InlineSeverityTable(props) { 65 | return ( 66 | 67 | 68 | 69 | {props.header} 70 | 71 | 72 | 73 | 74 | 75 | 76 |
    {props.caption}
    77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/utils.test.js: -------------------------------------------------------------------------------- 1 | import test from "tape"; 2 | import { isAbsolutePath, parseSourcePosition } from "../src/utils.js"; 3 | 4 | test("parseSourcePosition", (t) => { 5 | const validSourcePositions = [ 6 | ["/path/to/file", 11, 22], 7 | ["C:\\path\\to\\file", 11, 22], 8 | ["file:///path/to/file", 11, 22], 9 | ["file:///C:/path/to/file", 11, 22], 10 | ["http://a.com/path/to/file", 11, 22], 11 | ["https://a.com/path/to/file", 11, 22], 12 | ["", 11, 22], 13 | ]; 14 | validSourcePositions.forEach((inputs) => { 15 | const position = inputs.join(":"); 16 | const result = parseSourcePosition(position); 17 | 18 | const expected = { file: inputs[0], line: inputs[1], column: inputs[2] }; 19 | t.deepEqual(result, expected, "valid: " + position); 20 | }); 21 | 22 | const invalidSourcePositions = [ 23 | ["/path/to/file", 11], 24 | ["/path/to/file", 11, ""], 25 | ["/path/to/file", "", 22], 26 | ["/path/to/file", "", ""], 27 | ["/path/to/file", "a", ""], 28 | ["/path/to/file", "", "a"], 29 | ["/path/to/file", "a", "a"], 30 | [11, 22], 31 | ]; 32 | invalidSourcePositions.forEach((inputs) => { 33 | const position = inputs.join(":"); 34 | 35 | let didCatch = false; 36 | try { 37 | parseSourcePosition(position); 38 | } catch (e) { 39 | didCatch = true; 40 | } 41 | 42 | t.equal(didCatch, true, "invalid: " + position); 43 | }); 44 | 45 | t.end(); 46 | }); 47 | 48 | test("isAbsolutePath", (t) => { 49 | const areAbsolute = [ 50 | "/", 51 | "/tmp", 52 | "/tmp/sub/path", 53 | "C:\\", 54 | "A:\\", 55 | "C:\\Temp", 56 | "C:\\Temp\\With Spaces", 57 | "\\", 58 | "\\Temp", 59 | "\\Temp\\With Spaces", 60 | "file:///tmp/sub/path", 61 | "file:///C:/Windows/File/URL", 62 | "http://a.com/path", 63 | "https://a.com/path", 64 | ]; 65 | areAbsolute.forEach((path) => { 66 | t.equal(isAbsolutePath(path), true, `Absolute path: ${path}`); 67 | }); 68 | 69 | const notAbsolute = [ 70 | null, 71 | undefined, 72 | "", 73 | "internal/fs", 74 | "tmp", 75 | "tmp/sub/path", 76 | "Temp", 77 | "Temp\\With Spaces", 78 | "1:", 79 | "C:", 80 | "A", 81 | "2", 82 | ]; 83 | notAbsolute.forEach((path) => { 84 | t.equal(isAbsolutePath(path), false, `Relative path: ${path}`); 85 | }); 86 | 87 | t.end(); 88 | }); 89 | -------------------------------------------------------------------------------- /examples/html-external/adders.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // import { 4 | // Object1, 5 | // Object2, 6 | // Object3, 7 | // Object4, 8 | // Object5, 9 | // Object6, 10 | // Object7, 11 | // Object8, 12 | // } from './objects' 13 | 14 | // We access this object in all the functions as otherwise 15 | // v8 will just inline them since they are so short 16 | const preventInlining = { 17 | flag: false, 18 | }; 19 | 20 | function addSmis(a, b) { 21 | if (preventInlining.flag) return a - b; 22 | return a + b; 23 | } 24 | 25 | function addNumbers(a, b) { 26 | if (preventInlining.flag) return a - b; 27 | return a + b; 28 | } 29 | 30 | function addStrings(a, b) { 31 | if (preventInlining.flag) return a - b; 32 | return a + b; 33 | } 34 | 35 | function addAny(a, b) { 36 | if (preventInlining.flag) return a - b; 37 | // passed one object? 38 | if (b == null) return a.x + a.y; 39 | return a + b; 40 | } 41 | 42 | const ITER = 1e3; 43 | 44 | var results = []; 45 | 46 | function processResult(r) { 47 | // will never happen 48 | if (r === -1) preventInlining.flag = true; 49 | results.push(r); 50 | // prevent exhausting memory 51 | if (results.length > 1e5) results = []; 52 | } 53 | 54 | for (let i = 0; i < ITER; i++) { 55 | for (let j = ITER; j > 0; j--) { 56 | processResult(addSmis(i, j)); 57 | processResult(addNumbers(i, j)); 58 | processResult(addNumbers(i * 0.2, j * 0.2)); 59 | processResult(addStrings(`${i}`, `${j}`)); 60 | // Just passing Smis for now 61 | processResult(addAny(i, j)); 62 | } 63 | } 64 | 65 | for (let i = 0; i < ITER; i++) { 66 | for (let j = ITER; j > 0; j--) { 67 | // Adding Doubles 68 | processResult(addAny(i * 0.2, j * 0.2)); 69 | } 70 | } 71 | 72 | for (let i = 0; i < ITER; i++) { 73 | for (let j = ITER; j > 0; j--) { 74 | // Adding Strings 75 | processResult(addAny(`${i}`, `${j}`)); 76 | } 77 | } 78 | 79 | function addObjects(SomeObject) { 80 | for (let i = 0; i < ITER; i++) { 81 | for (let j = ITER; j > 0; j--) { 82 | processResult(addAny(new SomeObject(i, j))); 83 | } 84 | } 85 | } 86 | addObjects(Object1); 87 | addObjects(Object2); 88 | addObjects(Object3); 89 | addObjects(Object4); 90 | addObjects(Object5); 91 | addObjects(Object6); 92 | addObjects(Object7); 93 | addObjects(Object8); 94 | 95 | console.log(results.length); 96 | -------------------------------------------------------------------------------- /examples/two-modules/adders.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { 4 | Object1, 5 | Object2, 6 | Object3, 7 | Object4, 8 | Object5, 9 | Object6, 10 | Object7, 11 | Object8, 12 | } = require("./objects"); 13 | 14 | // We access this object in all the functions as otherwise 15 | // v8 will just inline them since they are so short 16 | const preventInlining = { 17 | flag: false, 18 | }; 19 | 20 | function addSmis(a, b) { 21 | if (preventInlining.flag) return a - b; 22 | return a + b; 23 | } 24 | 25 | function addNumbers(a, b) { 26 | if (preventInlining.flag) return a - b; 27 | return a + b; 28 | } 29 | 30 | function addStrings(a, b) { 31 | if (preventInlining.flag) return a - b; 32 | return a + b; 33 | } 34 | 35 | function addAny(a, b) { 36 | if (preventInlining.flag) return a - b; 37 | // passed one object? 38 | if (b == null) return a.x + a.y; 39 | return a + b; 40 | } 41 | 42 | const ITER = 1e3; 43 | 44 | var results = []; 45 | 46 | function processResult(r) { 47 | // will never happen 48 | if (r === -1) preventInlining.flag = true; 49 | results.push(r); 50 | // prevent exhausting memory 51 | if (results.length > 1e5) results = []; 52 | } 53 | 54 | for (let i = 0; i < ITER; i++) { 55 | for (let j = ITER; j > 0; j--) { 56 | processResult(addSmis(i, j)); 57 | processResult(addNumbers(i, j)); 58 | processResult(addNumbers(i * 0.2, j * 0.2)); 59 | processResult(addStrings(`${i}`, `${j}`)); 60 | // Just passing Smis for now 61 | processResult(addAny(i, j)); 62 | } 63 | } 64 | 65 | for (let i = 0; i < ITER; i++) { 66 | for (let j = ITER; j > 0; j--) { 67 | // Adding Doubles 68 | processResult(addAny(i * 0.2, j * 0.2)); 69 | } 70 | } 71 | 72 | for (let i = 0; i < ITER; i++) { 73 | for (let j = ITER; j > 0; j--) { 74 | // Adding Strings 75 | processResult(addAny(`${i}`, `${j}`)); 76 | } 77 | } 78 | 79 | function addObjects(SomeObject) { 80 | for (let i = 0; i < ITER; i++) { 81 | for (let j = ITER; j > 0; j--) { 82 | processResult(addAny(new SomeObject(i, j))); 83 | } 84 | } 85 | } 86 | addObjects(Object1); 87 | addObjects(Object2); 88 | addObjects(Object3); 89 | addObjects(Object4); 90 | addObjects(Object5); 91 | addObjects(Object6); 92 | addObjects(Object7); 93 | addObjects(Object8); 94 | 95 | function log() { 96 | console.log.apply(console, arguments); 97 | } 98 | 99 | log(results.length); 100 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/src/utils.js: -------------------------------------------------------------------------------- 1 | export const MIN_SEVERITY = 1; 2 | export const UNKNOWN_SEVERITY = -1; 3 | 4 | export function unquote(s) { 5 | // for some reason Node.js double quotes some the strings in the log, i.e. ""eager"" 6 | return s.replace(/^"/, "").replace(/"$/, ""); 7 | } 8 | 9 | // allow DOS disk paths (i.e. 'C:\path\to\file') 10 | const lineColumnRx = /:(\d+):(\d+)$/; 11 | 12 | function safeToInt(x) { 13 | if (x == null) return 0; 14 | return parseInt(x); 15 | } 16 | 17 | /** 18 | * @param {string} sourcePosition 19 | */ 20 | export function parseSourcePosition(sourcePosition) { 21 | const match = lineColumnRx.exec(sourcePosition); 22 | if (match) { 23 | return { 24 | file: sourcePosition.slice(0, match.index), 25 | line: safeToInt(match[1]), 26 | column: safeToInt(match[2]), 27 | }; 28 | } 29 | 30 | throw new Error("Could not parse source position: " + sourcePosition); 31 | } 32 | 33 | // Inspired by Node.JS isAbsolute algorithm. Copied here to be compatible with URLs 34 | // https://github.com/nodejs/node/blob/bcdbd57134558e3bea730f8963881e8865040f6f/lib/path.js#L352 35 | 36 | const CHAR_UPPERCASE_A = 65; /* A */ 37 | const CHAR_LOWERCASE_A = 97; /* a */ 38 | const CHAR_UPPERCASE_Z = 90; /* Z */ 39 | const CHAR_LOWERCASE_Z = 122; /* z */ 40 | const CHAR_FORWARD_SLASH = 47; /* / */ 41 | const CHAR_BACKWARD_SLASH = 92; /* \ */ 42 | const CHAR_COLON = 58; /* : */ 43 | 44 | function isPathSeparator(code) { 45 | return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; 46 | } 47 | 48 | function isWindowsDeviceRoot(code) { 49 | return ( 50 | (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) || 51 | (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) 52 | ); 53 | } 54 | 55 | /** 56 | * @param {string} path 57 | */ 58 | export function isAbsolutePath(path) { 59 | const length = path && path.length; 60 | if (path == null || length == 0) { 61 | return false; 62 | } 63 | 64 | const firstChar = path.charCodeAt(0); 65 | if (isPathSeparator(firstChar)) { 66 | return true; 67 | } else if ( 68 | length > 2 && 69 | isWindowsDeviceRoot(firstChar) && 70 | path.charCodeAt(1) === CHAR_COLON && 71 | isPathSeparator(path.charCodeAt(2)) 72 | ) { 73 | return true; 74 | } else if ( 75 | path.startsWith("file:///") || 76 | path.startsWith("http://") || 77 | path.startsWith("https://") 78 | ) { 79 | return true; 80 | } else { 81 | return false; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/README.md: -------------------------------------------------------------------------------- 1 | # v8-deopt-parser 2 | 3 | Parse a V8 log of optimizations and deoptimizations into an JavaScript object. 4 | 5 | ## Installation 6 | 7 | > Check out [`v8-deopt-viewer`](https://npmjs.com/package/v8-deopt-viewer) for a CLI that automates this for you! 8 | 9 | Requires [NodeJS](https://nodejs.org) 14.x or greater. 10 | 11 | ```bash 12 | npm i v8-deopt-parser 13 | ``` 14 | 15 | ## Usage 16 | 17 | The main export of this package is `parseV8Log`. Given the contents of a v8.log file, it returns a JavaScript object that contains the relevant optimization and deoptimization information. 18 | 19 | This package also contains some helper methods for using the resulting `V8DeoptInfo` object. See [`index.d.ts`](src/index.d.ts) for the latest API and definition of the `V8DeoptInfo` object. 20 | 21 | ```typescript 22 | /** 23 | * Parse the deoptimizations from a v8.log file 24 | * @param v8LogContent The contents of a v8.log file 25 | * @param options Options to influence the parsing of the V8 log 26 | */ 27 | export function parseV8Log( 28 | v8LogContent: string, 29 | options?: Options 30 | ): Promise; 31 | 32 | /** 33 | * Group the V8 deopt information into an object mapping files to the relevant 34 | * data 35 | * @param rawDeoptInfo A V8DeoptInfo object from `parseV8Log` 36 | */ 37 | export function groupByFile(rawDeoptInfo: V8DeoptInfo): PerFileV8DeoptInfo; 38 | 39 | /** 40 | * Find an entry in a V8DeoptInfo object 41 | * @param deoptInfo A V8DeoptInfo object from `parseV8Log` 42 | * @param entryId The ID of the entry to find 43 | */ 44 | export function findEntry( 45 | deoptInfo: V8DeoptInfo, 46 | entryId: string 47 | ): Entry | null; 48 | 49 | /** 50 | * Sort V8 Deopt entries by line, number, and type. Modifies the original array. 51 | * @param entries A list of V8 Deopt Entries 52 | * @returns The sorted entries 53 | */ 54 | export function sortEntries(entries: Entry[]): Entry[]; 55 | 56 | /** 57 | * Get the severity of an Inline Cache state 58 | * @param state An Inline Cache state 59 | */ 60 | export function severityIcState(state: ICState): number; 61 | 62 | /** The minimum severity an update or entry can be. */ 63 | export const MIN_SEVERITY = 1; 64 | ``` 65 | 66 | ## Prior work 67 | 68 | - [thlorenz/deoptigate](https://github.com/thlorenz/deoptigate) 69 | 70 | This project started out as a fork of the awesome `deoptigate` but as the scope of what I wanted to accomplish grew, I figured it was time to start my own project that I could re-architect to meet my requirements 71 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/README.md: -------------------------------------------------------------------------------- 1 | # v8-deopt-webapp 2 | 3 | Display the V8 optimizations and deoptimizations of a JavaScript file 4 | 5 | ## Installation 6 | 7 | > Check out [`v8-deopt-viewer`](https://npmjs.com/package/v8-deopt-viewer) for a CLI that automates this for you! 8 | 9 | ```bash 10 | npm i v8-deopt-webapp 11 | ``` 12 | 13 | ## Usage 14 | 15 | 1. Generate a `PerFileDeoptInfoWithSources` object: 16 | 17 | 1. Use [`v8-deopt-generate-log`](https://npmjs.com/package/v8-deopt-generate-log) and [`v8-deopt-parser`](https://npmjs.com/package/v8-deopt-parser) to generate a `V8DeoptInfo` object. 18 | 2. Use the `groupByFile` utility from `v8-deopt-parser` to group the results by file. 19 | 3. Extend the resulting object with the source of each file listed, and the shortened relative path to that file (can be defined relative to whatever you like) 20 | 21 | ```javascript 22 | import { parseV8Log, groupByFile } from "v8-deopt-parser"; 23 | 24 | const rawDeoptInfo = await parseV8Log(logContents); 25 | const groupDeoptInfo = groupByFile(rawDeoptInfo); 26 | const deoptInfo = { 27 | ...groupDeoptInfo, 28 | // Define some addSources function that adds the `src` and `relativePath` property 29 | // to each file object in groupDeoptInfo.files 30 | files: await addSources(groupDeoptInfo.files), 31 | }; 32 | ``` 33 | 34 | 2. Include the object in an HTML file 35 | 3. Include `dist/v8-deopt-webapp.css` and `dist/v8-deopt-webapp.js` in the HTML file 36 | 4. Call `V8DeoptViewer.renderIntoDom(object, container)` with the object and the DOM Node you want the app to render into 37 | 38 | Currently, we only produce a UMD build, but would consider publishing an ESM build if it's useful to people 39 | 40 | ## Contributing 41 | 42 | See the `Contributing.md` guide at the root of the repo for general guidance you should follow. 43 | 44 | To run the webapp locally: 45 | 46 | 1. `cd` into this package's directory (e.g. `packages/v8-deopt-webapp`) 47 | 1. Run `node test/generateTestData.mjs` 48 | 1. Open `test/index.html` in your favorite web browser 49 | 50 | By default, the test page (`test/index.html`) loads the data from `test/deoptInfo.js`. If you want to simulate a log that doesn't have sources, add `?error` to the URL to load `deoptInfoError.js`. 51 | 52 | If you make changes to `v8-deopt-generate-log` or `v8-deopt-parser`, you'll need to rerun `generateTestData.mjs` to re-generate `deoptInfo.js` & `deoptInfoError.js`. 53 | 54 | ``` 55 | 56 | ``` 57 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/parseV8Log.traceMaps.test.js: -------------------------------------------------------------------------------- 1 | import test from "tape"; 2 | import { runParserNonStream, runParserStream, writeSnapshot } from "./helpers.js"; 3 | import { validateMapData, writeMapSnapshot } from "./traceMapsHelpers.js"; 4 | 5 | for (const runParser of [runParserNonStream, runParserStream]) { 6 | test("runParser(html-inline.traceMaps.v8.log)", async (t) => { 7 | const logFileName = "html-inline.traceMaps.v8.log"; 8 | const result = await runParser(t, logFileName); 9 | 10 | t.equal(result.codes.length, 15, "Number of codes"); 11 | t.equal(result.deopts.length, 6, "Number of deopts"); 12 | t.equal(result.ics.length, 33, "Number of ics"); 13 | 14 | const mapEntryIds = Object.keys(result.maps.nodes); 15 | t.equal(mapEntryIds.length, 38, "Number of map entries"); 16 | 17 | const mapEdgeIds = Object.keys(result.maps.edges); 18 | t.equal(mapEdgeIds.length, 35, "Number of map edges"); 19 | 20 | await writeSnapshot(logFileName, result); 21 | await writeMapSnapshot(logFileName, result); 22 | validateMapData(t, result); 23 | }); 24 | 25 | test("runParser(html-external.traceMaps.v8.log)", async (t) => { 26 | const logFileName = "html-external.traceMaps.v8.log"; 27 | const result = await runParser(t, logFileName); 28 | 29 | t.equal(result.codes.length, 16, "Number of codes"); 30 | t.equal(result.deopts.length, 6, "Number of deopts"); 31 | t.equal(result.ics.length, 33, "Number of ics"); 32 | 33 | const mapEntryIds = Object.keys(result.maps.nodes); 34 | t.equal(mapEntryIds.length, 38, "Number of map entries"); 35 | 36 | const mapEdgeIds = Object.keys(result.maps.edges); 37 | t.equal(mapEdgeIds.length, 35, "Number of map edges"); 38 | 39 | await writeSnapshot(logFileName, result); 40 | await writeMapSnapshot(logFileName, result); 41 | validateMapData(t, result); 42 | }); 43 | 44 | test("runParser(adders.traceMaps.v8.log)", async (t) => { 45 | const logFileName = "adders.traceMaps.v8.log"; 46 | const result = await runParser(t, logFileName); 47 | 48 | t.equal(result.codes.length, 16, "Number of codes"); 49 | t.equal(result.deopts.length, 7, "Number of deopts"); 50 | t.equal(result.ics.length, 34, "Number of ics"); 51 | 52 | const mapEntryIds = Object.keys(result.maps.nodes); 53 | t.equal(mapEntryIds.length, 38, "Number of map entries"); 54 | 55 | const mapEdgeIds = Object.keys(result.maps.edges); 56 | t.equal(mapEdgeIds.length, 35, "Number of map edges"); 57 | 58 | await writeSnapshot(logFileName, result); 59 | await writeMapSnapshot(logFileName, result); 60 | validateMapData(t, result); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: "36 19 * * 3" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["javascript"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /packages/v8-deopt-viewer/src/determineCommonRoot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string[]} files 3 | */ 4 | export function determineCommonRoot(files) { 5 | if (files.length === 0) { 6 | return null; 7 | } 8 | 9 | let containsURLs, containsWin32Paths, containsUnixPaths; 10 | const parsed = files.map((f) => { 11 | // Remove trailing slashes 12 | // f = f.replace(/\/$/, "").replace(/\\$/, ""); 13 | 14 | if (f.startsWith("http:") || f.startsWith("https:")) { 15 | containsURLs = true; 16 | return new URL(f); 17 | } else if (f.startsWith("file://")) { 18 | containsURLs = true; 19 | return new URL(f); 20 | } else if (f.includes("\\")) { 21 | containsWin32Paths = true; 22 | return f.replace(/\\/g, "/"); 23 | } else { 24 | containsUnixPaths = true; 25 | return f; 26 | } 27 | }); 28 | 29 | if ( 30 | (containsUnixPaths && containsWin32Paths) || 31 | (containsURLs && (containsUnixPaths || containsWin32Paths)) 32 | ) { 33 | return null; 34 | } 35 | 36 | if (containsURLs) { 37 | // @ts-ignore 38 | return determineCommonURL(parsed); 39 | } else if (containsWin32Paths) { 40 | // @ts-ignore 41 | const root = determineCommonPath(parsed); 42 | return root && root.replace(/\//g, "\\"); 43 | } else { 44 | // @ts-ignore 45 | return determineCommonPath(parsed); 46 | } 47 | } 48 | 49 | /** 50 | * @param {URL[]} urls 51 | */ 52 | function determineCommonURL(urls) { 53 | if (urls.length == 1 && urls[0].pathname == "/") { 54 | return urls[0].protocol + "//"; 55 | } 56 | 57 | const host = urls[0].host; 58 | const paths = []; 59 | for (let url of urls) { 60 | if (url.host !== host) { 61 | return null; 62 | } 63 | 64 | paths.push(url.pathname); 65 | } 66 | 67 | const commonPath = determineCommonPath(paths); 68 | return new URL(commonPath, urls[0]).toString(); 69 | } 70 | 71 | /** 72 | * @param {string[]} paths 73 | */ 74 | function determineCommonPath(paths) { 75 | let commonPathParts = paths[0].split("/"); 76 | if (paths.length == 1) { 77 | return commonPathParts.slice(0, -1).join("/") + "/"; 78 | } 79 | 80 | for (const path of paths) { 81 | const parts = path.split("/"); 82 | for (let i = 1; i < parts.length; i++) { 83 | if (i == parts.length - 1 && parts[i] == commonPathParts[i]) { 84 | // This path is a strict subset of the root path, so make the root path 85 | // one part less than it currently is so root doesn't include the basename 86 | // of this path 87 | commonPathParts = commonPathParts.slice(0, i); 88 | } else if (parts[i] != commonPathParts[i]) { 89 | commonPathParts = commonPathParts.slice(0, i); 90 | break; 91 | } 92 | } 93 | } 94 | 95 | return commonPathParts.length > 0 ? commonPathParts.join("/") + "/" : null; 96 | } 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ########################################################### 2 | # 3 | # Project .gitignore 4 | # 5 | ########################################################### 6 | 7 | packages/v8-deopt-viewer/README.md 8 | packages/v8-deopt-parser/test/deoptResults 9 | packages/v8-deopt-webapp/test/deoptInfo.js 10 | packages/v8-deopt-webapp/test/deoptInfoNoMaps.js 11 | packages/v8-deopt-webapp/test/deoptInfoError.js 12 | 13 | 14 | ########################################################### 15 | # 16 | # Node .gitignore 17 | # 18 | ########################################################### 19 | 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | lerna-debug.log* 27 | 28 | # Diagnostic reports (https://nodejs.org/api/report.html) 29 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 30 | 31 | # Runtime data 32 | pids 33 | *.pid 34 | *.seed 35 | *.pid.lock 36 | 37 | # Directory for instrumented libs generated by jscoverage/JSCover 38 | lib-cov 39 | 40 | # Coverage directory used by tools like istanbul 41 | coverage 42 | *.lcov 43 | 44 | # nyc test coverage 45 | .nyc_output 46 | 47 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 48 | .grunt 49 | 50 | # Bower dependency directory (https://bower.io/) 51 | bower_components 52 | 53 | # node-waf configuration 54 | .lock-wscript 55 | 56 | # Compiled binary addons (https://nodejs.org/api/addons.html) 57 | build/Release 58 | 59 | # Dependency directories 60 | node_modules/ 61 | jspm_packages/ 62 | 63 | # TypeScript v1 declaration files 64 | typings/ 65 | 66 | # TypeScript cache 67 | *.tsbuildinfo 68 | 69 | # Optional npm cache directory 70 | .npm 71 | 72 | # Optional eslint cache 73 | .eslintcache 74 | 75 | # Microbundle cache 76 | .rpt2_cache/ 77 | .rts2_cache_cjs/ 78 | .rts2_cache_es/ 79 | .rts2_cache_umd/ 80 | 81 | # Optional REPL history 82 | .node_repl_history 83 | 84 | # Output of 'npm pack' 85 | *.tgz 86 | 87 | # Yarn Integrity file 88 | .yarn-integrity 89 | 90 | # dotenv environment variables file 91 | .env 92 | .env.test 93 | 94 | # parcel-bundler cache (https://parceljs.org/) 95 | .cache 96 | 97 | # Next.js build output 98 | .next 99 | 100 | # Nuxt.js build / generate output 101 | .nuxt 102 | dist 103 | 104 | # Gatsby files 105 | .cache/ 106 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 107 | # https://nextjs.org/blog/next-9-1#public-directory-support 108 | # public 109 | 110 | # vuepress build output 111 | .vuepress/dist 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | -------------------------------------------------------------------------------- /packages/v8-deopt-viewer/test/determineCommonRoot.test.js: -------------------------------------------------------------------------------- 1 | import test from "tape"; 2 | import { determineCommonRoot } from "../src/determineCommonRoot.js"; 3 | 4 | test("determineCommonRoot(absolute paths)", (t) => { 5 | // Windows paths 6 | let result = determineCommonRoot([ 7 | "C:\\a\\b\\c2\\d\\e", 8 | "C:\\a\\b\\c2\\f\\g", 9 | "C:\\a\\b\\c", 10 | "C:\\a\\b\\c\\", 11 | ]); 12 | t.equal(result, "C:\\a\\b\\", "Windows paths"); 13 | 14 | // Single path 15 | result = determineCommonRoot(["C:\\a\\b\\c\\d\\e"]); 16 | t.equal(result, "C:\\a\\b\\c\\d\\", "Single path"); 17 | 18 | // Linux paths with ending '/' 19 | result = determineCommonRoot(["/a/b/c2/d/e/", "/a/b/c/", "/a/b/c2/f/g/"]); 20 | t.equal(result, "/a/b/", "Linux paths with ending '/'"); 21 | 22 | // URLs with mixed endings 23 | result = determineCommonRoot([ 24 | "https://a.com/a/b/c/d/e", 25 | "https://a.com/a/b/c", 26 | "https://a.com/a/b/c/f/g", 27 | ]); 28 | t.equal(result, "https://a.com/a/b/", "URLs with mixed endings"); 29 | 30 | // Single URL 31 | result = determineCommonRoot(["https://a.com/a/b/c/d/e"]); 32 | t.equal(result, "https://a.com/a/b/c/d/", "Single URL"); 33 | 34 | // Single URL with no path 35 | result = determineCommonRoot(["https://a.com/"]); 36 | t.equal(result, "https://", "Single URL with no path"); 37 | 38 | // Different domains 39 | result = determineCommonRoot([ 40 | "https://a.com/a/b/c/d", 41 | "https://b.com/a/b/c/e", 42 | ]); 43 | t.equal(result, null, "Different domains"); 44 | 45 | t.end(); 46 | }); 47 | 48 | test("determineCommonRoot(mixed paths and URLs)", (t) => { 49 | // Windows & Linux 50 | let result = determineCommonRoot([ 51 | "/a/b/c/d/e/", 52 | "/a/b/c/", 53 | "C:\\a\\b\\c", 54 | "C:\\a\\b\\c\\f\\g", 55 | ]); 56 | t.equal(result, null, "Windows & Linux"); 57 | 58 | // Windows & URLs 59 | result = determineCommonRoot(["C:\\a\\b\\c", "https://a.com/b/c/d/"]); 60 | t.equal(result, null, "Windows & URLs"); 61 | 62 | // Linux & URLs 63 | result = determineCommonRoot(["https://a.com/b/c/d/", "/a/b/c"]); 64 | t.equal(result, null, "Linux & URLs"); 65 | 66 | // Windows & Linux & URLs 67 | result = determineCommonRoot([ 68 | "C:\\a\\b\\c", 69 | "/a/b/c", 70 | "https://a.com/b/c/d", 71 | ]); 72 | t.equal(result, null, "Windows & Linux & URLs"); 73 | 74 | t.end(); 75 | }); 76 | 77 | test("determineCommonRoot(relative paths)", (t) => { 78 | // Relative Windows paths 79 | let result = determineCommonRoot([ 80 | "a\\b\\c2\\d\\e", 81 | "a\\b\\c\\", 82 | "a\\b\\c2\\d\\f\\g", 83 | ]); 84 | t.equal(result, "a\\b\\", "Relative Windows paths"); 85 | 86 | // Relative Linux paths 87 | result = determineCommonRoot(["a/b/c", "a/b/c2/d/e/", "a/b/c2/d/f/g/"]); 88 | t.equal(result, "a/b/", "Relative Linux paths"); 89 | 90 | t.end(); 91 | }); 92 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/groupBy.test.js: -------------------------------------------------------------------------------- 1 | import test from "tape"; 2 | import { groupByFile } from "../src/index.js"; 3 | import { runParserNonStream as runParser, repoRoot, repoFileURL } from "./helpers.js"; 4 | 5 | test("groupByFile(adders.v8.log)", async (t) => { 6 | const rawData = await runParser(t, "adders.v8.log"); 7 | const result = groupByFile(rawData); 8 | 9 | const files = Object.keys(result.files); 10 | t.equal(files.length, 1, "Number of files"); 11 | 12 | const fileData = result.files[files[0]]; 13 | t.equal(fileData.codes.length, 16, "number of codes"); 14 | t.equal(fileData.deopts.length, 7, "number of deopts"); 15 | t.equal(fileData.ics.length, 33, "number of ics"); 16 | }); 17 | 18 | test("groupByFile(two-modules.v8.log)", async (t) => { 19 | const rawData = await runParser(t, "two-modules.v8.log"); 20 | const result = groupByFile(rawData); 21 | 22 | const files = Object.keys(result.files); 23 | t.equal(files.length, 2, "Number of files"); 24 | 25 | let fileData = result.files[repoRoot("examples/two-modules/adders.js")]; 26 | t.equal(fileData.codes.length, 8, "File 1: number of codes"); 27 | t.equal(fileData.deopts.length, 7, "File 1: number of deopts"); 28 | t.equal(fileData.ics.length, 8, "File 1: number of ics"); 29 | 30 | fileData = result.files[repoRoot("examples/two-modules/objects.js")]; 31 | t.equal(fileData.codes.length, 8, "File 2: number of codes"); 32 | t.equal(fileData.deopts.length, 0, "File 2: number of deopts"); 33 | t.equal(fileData.ics.length, 25, "File 2: number of ics"); 34 | }); 35 | 36 | test("groupByFile(html-inline.v8.log)", async (t) => { 37 | const rawData = await runParser(t, "html-inline.v8.log"); 38 | const result = groupByFile(rawData); 39 | 40 | const files = Object.keys(result.files); 41 | t.equal(files.length, 1, "Number of files"); 42 | 43 | const fileData = result.files[files[0]]; 44 | t.equal(fileData.codes.length, 15, "number of codes"); 45 | t.equal(fileData.deopts.length, 6, "number of deopts"); 46 | t.equal(fileData.ics.length, 33, "number of ics"); 47 | }); 48 | 49 | test("groupByFile(html-external.v8.log)", async (t) => { 50 | const rawData = await runParser(t, "html-external.v8.log"); 51 | const result = groupByFile(rawData); 52 | 53 | const files = Object.keys(result.files); 54 | t.equal(files.length, 2, "Number of files"); 55 | 56 | let fileData = result.files[repoFileURL("examples/html-external/adders.js")]; 57 | t.equal(fileData.codes.length, 7, "File 1: number of codes"); 58 | t.equal(fileData.deopts.length, 6, "File 1: number of deopts"); 59 | t.equal(fileData.ics.length, 8, "File 1: number of ics"); 60 | 61 | fileData = result.files[repoFileURL("examples/html-external/objects.js")]; 62 | t.equal(fileData.codes.length, 9, "File 2: number of codes"); 63 | t.equal(fileData.deopts.length, 0, "File 2: number of deopts"); 64 | t.equal(fileData.ics.length, 25, "File 2: number of ics"); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/test/generateTestData.mjs: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | import * as path from "path"; 3 | import { readFile, writeFile, copyFile, mkdir } from "fs/promises"; 4 | import { fileURLToPath } from "url"; 5 | import { readLogFile } from "../../v8-deopt-parser/test/helpers.js"; 6 | 7 | // const logFileName = "adders.v8.log"; 8 | 9 | const logFileName = "html-external.v8.log"; 10 | const logWithMapsFileName = "html-external.traceMaps.v8.log"; 11 | 12 | // const logFileName = "html-inline.v8.log"; 13 | // const logWithMapsFileName = "html-inline.traceMaps.v8.log"; 14 | 15 | // @ts-ignore 16 | const __dirname = path.join(fileURLToPath(import.meta.url)); 17 | const pkgRoot = (...args) => path.join(__dirname, "..", "..", ...args); 18 | const repoRoot = (...args) => pkgRoot("..", "..", ...args); 19 | const outDir = (...args) => pkgRoot("test/logs", ...args); 20 | 21 | export async function generateTestData() { 22 | await mkdir(outDir(), { recursive: true }); 23 | 24 | const logPath = (filename) => 25 | repoRoot(`packages/v8-deopt-parser/test/logs/${filename}`); 26 | 27 | // Generate log using raw log which has invalid paths (/tmp/v8-deopt-viewer) 28 | // to simulate error output 29 | console.log("Generating output without sources..."); 30 | spawnSync( 31 | process.execPath, 32 | [ 33 | repoRoot("packages/v8-deopt-viewer/bin/v8-deopt-viewer.js"), 34 | "-i", 35 | logPath(logFileName), 36 | "-o", 37 | outDir(), 38 | ], 39 | { stdio: "inherit" } 40 | ); 41 | 42 | await copyFile(outDir("v8-data.bin"), pkgRoot("test/deoptInfoError.bin")); 43 | 44 | // Now generate log using modified log with correct src paths 45 | console.log("Generating output with sources..."); 46 | const newNoMapContents = await readLogFile(logFileName, logPath(logFileName)); 47 | 48 | const updatedNoMapLogPath = outDir(logFileName); 49 | await writeFile(updatedNoMapLogPath, newNoMapContents, "utf8"); 50 | 51 | spawnSync( 52 | process.execPath, 53 | [ 54 | repoRoot("packages/v8-deopt-viewer/bin/v8-deopt-viewer.js"), 55 | "-i", 56 | updatedNoMapLogPath, 57 | "-o", 58 | outDir(), 59 | ], 60 | { stdio: "inherit" } 61 | ); 62 | 63 | await copyFile(outDir("v8-data.bin"), pkgRoot("test/deoptInfoNoMaps.bin")); 64 | 65 | // Generate logs with maps 66 | console.log("Generating output with maps & sources..."); 67 | const newContents = await readLogFile( 68 | logWithMapsFileName, 69 | logPath(logWithMapsFileName) 70 | ); 71 | 72 | const updatedLogPath = outDir(logWithMapsFileName); 73 | await writeFile(updatedLogPath, newContents, "utf8"); 74 | 75 | spawnSync( 76 | process.execPath, 77 | [ 78 | repoRoot("packages/v8-deopt-viewer/bin/v8-deopt-viewer.js"), 79 | "-i", 80 | updatedLogPath, 81 | "-o", 82 | outDir(), 83 | ], 84 | { stdio: "inherit" } 85 | ); 86 | 87 | await copyFile(outDir("v8-data.bin"), pkgRoot("test/deoptInfo.bin")); 88 | } 89 | 90 | generateTestData(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # v8-deopt-viewer 2 | 3 | View deoptimizations of your JavaScript in V8 4 | 5 | ![Sample image of the results of running v8-deopt-viewer](examples/v8-deopt-webapp.png) 6 | 7 | Also consider checking out [Deopt Explorer](https://devblogs.microsoft.com/typescript/introducing-deopt-explorer/) from the TypeScript team! 8 | 9 | ## You may not need this tool... 10 | 11 | V8 only optimizes code that runs repeatedly. Often for websites this code is your framework's code and not your app code. If you are looking to improve your website's performance, first check out tools like [Lighthouse](https://developers.google.com/web/tools/lighthouse/) or [webhint](https://webhint.io/), and follow other general website performance guidance. 12 | 13 | ## Usage 14 | 15 | Install [NodeJS](https://nodejs.org) 14.x or greater. 16 | 17 | ```bash 18 | npx v8-deopt-viewer program.js --open 19 | ``` 20 | 21 | If you want run this against URLs, also install [`puppeteer`](https://github.com/GoogleChrome/puppeteer): 22 | 23 | ```bash 24 | npm i -g puppeteer 25 | ``` 26 | 27 | The main usage of this repo is through the CLI. Download the `v8-deopt-viewer` package through [NPM](https://npmjs.com) or use [`npx`](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b) to run the CLI. Options for the CLI can be found using the `--help`. 28 | 29 | ``` 30 | $ npx v8-deopt-viewer --help 31 | 32 | Description 33 | Generate and view deoptimizations in JavaScript code running in V8 34 | 35 | Usage 36 | $ v8-deopt-viewer [file] [options] 37 | 38 | Options 39 | -i, --input Path to an already generated v8.log file 40 | -o, --out The directory to output files too (default current working directory) 41 | -t, --timeout How long in milliseconds to keep the browser open while the webpage runs (default 5000) 42 | --keep-internals Don't remove NodeJS internals from the log 43 | --skip-maps Skip tracing internal maps of V8 44 | --open Open the resulting webapp in a web browser 45 | -v, --version Displays current version 46 | -h, --help Displays this message 47 | 48 | Examples 49 | $ v8-deopt-viewer examples/simple/adder.js 50 | $ v8-deopt-viewer examples/html-inline/adders.html -o /tmp/directory 51 | $ v8-deopt-viewer https://google.com 52 | $ v8-deopt-viewer -i v8.log 53 | $ v8-deopt-viewer -i v8.log -o /tmp/directory 54 | ``` 55 | 56 | Running the CLI will run the script or webpage provided with V8 flags to output a log of optimizations and deoptimizations. That log is saved as `v8.log`. We'll then parse that log into a JSON object filtering out the useful log lines and extending the information with such as the severity of the deopts. This data is saved in a JavaScript file (`v8-data.js`). We copy over the files from the webapp for viewing the data (`index.html`, `v8-deopt-webapp.js`, `v8-deopt-webapp.css`). Finally, open the `index.html` file in a modern browser to view the results of the run. 57 | 58 | ## Prior work 59 | 60 | - [thlorenz/deoptigate](https://github.com/thlorenz/deoptigate) 61 | 62 | This project started out as a fork of the awesome `deoptigate` but as the scope of what I wanted to accomplish grew, I figured it was time to start my own project that I could re-architect to meet my requirements 63 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/SummaryTable.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "preact"; 2 | import { 3 | table, 4 | table_scroll, 5 | table_striped, 6 | table_hover, 7 | } from "../spectre.module.scss"; 8 | import { 9 | summaryTable, 10 | grid, 11 | headers, 12 | codes, 13 | deopts, 14 | ics, 15 | subheaders, 16 | fileName as fileNameClass, 17 | sev1, 18 | sev2, 19 | sev3, 20 | } from "./SummaryTable.module.scss"; 21 | 22 | /** 23 | * @param {import('./Summary').SummaryProps} props 24 | */ 25 | export function SummaryTable({ deoptInfo, perFileStats }) { 26 | return ( 27 | 37 | 38 | 39 | 40 | 43 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {Object.keys(perFileStats).map((fileName, i) => { 59 | const summaryInfo = perFileStats[fileName]; 60 | 61 | return ( 62 | 63 | 68 | 72 | 76 | 77 | 78 | ); 79 | })} 80 | 81 |
    File 41 | Optimizations 42 | 44 | Deoptimizations 45 | 47 | Inline Caches 48 |
    64 | 65 | {deoptInfo.files[fileName].relativePath} 66 | 67 |
    82 | ); 83 | } 84 | 85 | export function CodeTableHeaders(props) { 86 | return ( 87 | 88 | Optimized 89 | Optimizable 90 | Sev 3 91 | 92 | ); 93 | } 94 | 95 | export function SeverityTableHeaders(props) { 96 | return ( 97 | 98 | Sev 1 99 | Sev 2 100 | Sev 3 101 | 102 | ); 103 | } 104 | 105 | export function SeverityTableSummary(props) { 106 | return ( 107 | 108 | {props.severities.map((severityCount, i) => { 109 | return ( 110 | 0 ? severityClass(i + 1) : null, 114 | ].join(" ")} 115 | > 116 | {severityCount} 117 | 118 | ); 119 | })} 120 | 121 | ); 122 | } 123 | 124 | function severityClass(severity) { 125 | if (severity < 1) { 126 | return null; 127 | } else if (severity == 1) { 128 | return sev1; 129 | } else if (severity == 2) { 130 | return sev2; 131 | } else { 132 | return sev3; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/appState.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "preact"; 2 | import { useReducer, useMemo, useContext } from "preact/hooks"; 3 | 4 | /** 5 | * @typedef {import('v8-deopt-parser').FilePosition} FilePosition 6 | * 7 | * @typedef AppDispatchContextValue 8 | * @property {(newPos: FilePosition) => void} setSelectedPosition 9 | * @property {(newEntry: import('v8-deopt-parser').Entry) => void} setSelectedEntry 10 | */ 11 | 12 | /** @type {import('preact').PreactContext} */ 13 | const AppStateContext = createContext(null); 14 | 15 | /** @type {import('preact').PreactContext} */ 16 | const AppDispatchContext = createContext(null); 17 | 18 | /** 19 | * @typedef {import('v8-deopt-parser').Entry} Entry 20 | * // State 21 | * @typedef AppContextState 22 | * @property {Entry} prevSelectedEntry 23 | * @property {Entry} selectedEntry 24 | * @property {FilePosition} prevPosition 25 | * @property {FilePosition} selectedPosition 26 | * // Actions 27 | * @typedef {{ type: "SET_SELECTED_POSITION"; newPosition: FilePosition; }} SetSelectedPosition 28 | * @typedef {{ type: "SET_SELECTED_ENTRY"; entry: Entry; }} SetSelectedEntry 29 | * @typedef {SetSelectedPosition | SetSelectedEntry} AppContextAction 30 | * // Reducer 31 | * @param {AppContextState} state 32 | * @param {AppContextAction} action 33 | * @returns {AppContextState} 34 | */ 35 | function appContextReducer(state, action) { 36 | switch (action.type) { 37 | case "SET_SELECTED_POSITION": 38 | return { 39 | prevPosition: state.selectedPosition, 40 | prevSelectedEntry: state.selectedEntry, 41 | selectedPosition: action.newPosition, 42 | selectedEntry: null, 43 | }; 44 | case "SET_SELECTED_ENTRY": 45 | const entry = action.entry; 46 | return { 47 | prevPosition: state.selectedPosition, 48 | prevSelectedEntry: state.selectedEntry, 49 | selectedPosition: entry && { 50 | functionName: entry.functionName, 51 | file: entry.file, 52 | line: entry.line, 53 | column: entry.column, 54 | }, 55 | selectedEntry: entry, 56 | }; 57 | default: 58 | return state; 59 | } 60 | } 61 | 62 | /** @type {(props: any) => AppContextState} */ 63 | const initialState = (props) => ({ 64 | prevPosition: null, 65 | prevSelectedEntry: null, 66 | selectedPosition: null, 67 | selectedEntry: null, 68 | }); 69 | 70 | /** 71 | * @typedef AppProviderProps 72 | * @property {import('preact').JSX.Element | import('preact').JSX.Element[]} children 73 | * @param {AppProviderProps} props 74 | */ 75 | export function AppProvider(props) { 76 | const [state, dispatch] = useReducer(appContextReducer, props, initialState); 77 | const dispatchers = useMemo( 78 | () => ({ 79 | setSelectedPosition(newPosition) { 80 | dispatch({ type: "SET_SELECTED_POSITION", newPosition }); 81 | }, 82 | setSelectedEntry(entry) { 83 | dispatch({ type: "SET_SELECTED_ENTRY", entry }); 84 | }, 85 | }), 86 | [dispatch] 87 | ); 88 | 89 | return ( 90 | 91 | 92 | {props.children} 93 | 94 | 95 | ); 96 | } 97 | 98 | export const useAppState = () => useContext(AppStateContext); 99 | export const useAppDispatch = () => useContext(AppDispatchContext); 100 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/src/propertyICParsers.js: -------------------------------------------------------------------------------- 1 | import { parseString } from "./v8-tools-core/logreader.js"; 2 | import { MIN_SEVERITY, UNKNOWN_SEVERITY } from "./utils.js"; 3 | 4 | // Comments from: https://github.com/v8/v8/blob/23dace88f658c44b5346eb0858fdc2c6b52e9089/src/common/globals.h#L852 5 | 6 | /** Has never been executed */ 7 | const UNINITIALIZED = "uninitialized"; 8 | const PREMONOMORPHIC = "premonomorphic"; 9 | /** Has been executed and only on receiver has been seen */ 10 | const MONOMORPHIC = "monomorphic"; 11 | /** Check failed due to prototype (or map deprecation) */ 12 | const RECOMPUTE_HANDLER = "recompute_handler"; 13 | /** Multiple receiver types have been seen */ 14 | const POLYMORPHIC = "polymorphic"; 15 | /** Many receiver types have been seen */ 16 | const MEGAMORPHIC = "megamorphic"; 17 | /** Many DOM receiver types have been seen for the same accessor */ 18 | const MEGADOM = "megadom"; 19 | /** A generic handler is installed and no extra typefeedback is recorded */ 20 | const GENERIC = "generic"; 21 | /** No feedback will be collected */ 22 | export const NO_FEEDBACK = "no_feedback"; 23 | 24 | /** 25 | * @param {string} rawState Raw Inline Cache state from V8 26 | * @returns {import('./index').ICState} 27 | */ 28 | function parseIcState(rawState) { 29 | // ICState mapping in V8: https://github.com/v8/v8/blob/99c17a8bd0ff4c1f4873d491e1176f6c474985f0/src/ic/ic.cc#L53 30 | // Meanings: https://github.com/v8/v8/blob/99c17a8bd0ff4c1f4873d491e1176f6c474985f0/src/common/globals.h#L934 31 | switch (rawState) { 32 | case "0": 33 | return UNINITIALIZED; 34 | case ".": 35 | return PREMONOMORPHIC; 36 | case "1": 37 | return MONOMORPHIC; 38 | case "^": 39 | return RECOMPUTE_HANDLER; 40 | case "P": 41 | return POLYMORPHIC; 42 | case "N": 43 | return MEGAMORPHIC; 44 | case "D": 45 | return MEGADOM; 46 | case "G": 47 | return GENERIC; 48 | case "X": 49 | return NO_FEEDBACK; 50 | default: 51 | throw new Error("parse: unknown ic code state: " + rawState); 52 | } 53 | } 54 | 55 | /** 56 | * @param {import('./index').ICState} state 57 | * @returns {number} 58 | */ 59 | export function severityIcState(state) { 60 | switch (state) { 61 | case UNINITIALIZED: 62 | case PREMONOMORPHIC: 63 | case MONOMORPHIC: 64 | case RECOMPUTE_HANDLER: 65 | return MIN_SEVERITY; 66 | case POLYMORPHIC: 67 | case MEGADOM: 68 | return MIN_SEVERITY + 1; 69 | case MEGAMORPHIC: 70 | case GENERIC: 71 | return MIN_SEVERITY + 2; 72 | case NO_FEEDBACK: 73 | return UNKNOWN_SEVERITY; 74 | default: 75 | throw new Error("severity: unknown ic code state : " + state); 76 | } 77 | } 78 | 79 | // From https://github.com/v8/v8/blob/4773be80d9d716baeb99407ff8766158a2ae33b5/src/logging/log.cc#L1778 80 | export const propertyICFieldParsers = [ 81 | parseInt, // profile code 82 | parseInt, // line 83 | parseInt, // column 84 | parseIcState, // old_state 85 | parseIcState, // new_state 86 | parseString, // map ID 87 | parseString, // propertyKey 88 | parseString, // modifier 89 | parseString, // slow_reason 90 | ]; 91 | export const propertyIcFieldParsersNew = [ 92 | parseInt, // profile code 93 | parseInt, // time 94 | parseInt, // line 95 | parseInt, // column 96 | parseIcState, // old_state 97 | parseIcState, // new_state 98 | parseString, // map ID 99 | parseString, // propertyKey 100 | parseString, // modifier 101 | parseString, // slow_reason 102 | ]; 103 | -------------------------------------------------------------------------------- /examples/simple/adders.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global print */ 4 | 5 | class Object1 { 6 | constructor(x, y) { 7 | this.x = x; 8 | this.y = y; 9 | } 10 | } 11 | 12 | class Object2 { 13 | constructor(x, y) { 14 | this.y = y; 15 | this.x = x; 16 | } 17 | } 18 | 19 | class Object3 { 20 | constructor(x, y) { 21 | this.hello = "world"; 22 | this.x = x; 23 | this.y = y; 24 | } 25 | } 26 | 27 | class Object4 { 28 | constructor(x, y) { 29 | this.x = x; 30 | this.hello = "world"; 31 | this.y = y; 32 | } 33 | } 34 | 35 | class Object5 { 36 | constructor(x, y) { 37 | this.x = x; 38 | this.y = y; 39 | this.hello = "world"; 40 | } 41 | } 42 | 43 | class Object6 { 44 | constructor(x, y) { 45 | this.hola = "mundo"; 46 | this.x = x; 47 | this.y = y; 48 | this.hello = "world"; 49 | } 50 | } 51 | 52 | class Object7 { 53 | constructor(x, y) { 54 | this.x = x; 55 | this.hola = "mundo"; 56 | this.y = y; 57 | this.hello = "world"; 58 | } 59 | } 60 | 61 | class Object8 { 62 | constructor(x, y) { 63 | this.x = x; 64 | this.y = y; 65 | this.hola = "mundo"; 66 | this.hello = "world"; 67 | } 68 | } 69 | // We access this object in all the functions as otherwise 70 | // v8 will just inline them since they are so short 71 | const preventInlining = { 72 | flag: false, 73 | }; 74 | 75 | function addSmis(a, b) { 76 | if (preventInlining.flag) return a - b; 77 | return a + b; 78 | } 79 | 80 | function addNumbers(a, b) { 81 | if (preventInlining.flag) return a - b; 82 | return a + b; 83 | } 84 | 85 | function addStrings(a, b) { 86 | if (preventInlining.flag) return a - b; 87 | return a + b; 88 | } 89 | 90 | function addAny(a, b) { 91 | if (preventInlining.flag) return a - b; 92 | // passed one object? 93 | if (b == null) return a.x + a.y; 94 | return a + b; 95 | } 96 | 97 | const ITER = 1e3; 98 | 99 | var results = []; 100 | 101 | function processResult(r) { 102 | // will never happen 103 | if (r === -1) preventInlining.flag = true; 104 | results.push(r); 105 | // prevent exhausting memory 106 | if (results.length > 1e5) results = []; 107 | } 108 | 109 | for (let i = 0; i < ITER; i++) { 110 | for (let j = ITER; j > 0; j--) { 111 | processResult(addSmis(i, j)); 112 | processResult(addNumbers(i, j)); 113 | processResult(addNumbers(i * 0.2, j * 0.2)); 114 | processResult(addStrings(`${i}`, `${j}`)); 115 | // Just passing Smis for now 116 | processResult(addAny(i, j)); 117 | } 118 | } 119 | 120 | for (let i = 0; i < ITER; i++) { 121 | for (let j = ITER; j > 0; j--) { 122 | // Adding Doubles 123 | processResult(addAny(i * 0.2, j * 0.2)); 124 | } 125 | } 126 | 127 | for (let i = 0; i < ITER; i++) { 128 | for (let j = ITER; j > 0; j--) { 129 | // Adding Strings 130 | processResult(addAny(`${i}`, `${j}`)); 131 | } 132 | } 133 | 134 | function addObjects(SomeObject) { 135 | for (let i = 0; i < ITER; i++) { 136 | for (let j = ITER; j > 0; j--) { 137 | processResult(addAny(new SomeObject(i, j))); 138 | } 139 | } 140 | } 141 | addObjects(Object1); 142 | addObjects(Object2); 143 | addObjects(Object3); 144 | addObjects(Object4); 145 | addObjects(Object5); 146 | addObjects(Object6); 147 | addObjects(Object7); 148 | addObjects(Object8); 149 | 150 | // make this work with d8 and Node.js 151 | function log() { 152 | if (typeof print === "function") { 153 | print.apply(this, arguments); 154 | } else { 155 | console.log.apply(console, arguments); 156 | } 157 | } 158 | 159 | log(results.length); 160 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/FileViewer.jsx: -------------------------------------------------------------------------------- 1 | import { Route, Switch } from "wouter-preact"; 2 | import { V8DeoptInfoPanel } from "./V8DeoptInfoPanel/index.jsx"; 3 | import { CodePanel } from "./CodePanel.jsx"; 4 | import { CodeSettings, useCodeSettingsState } from "./CodeSettings.jsx"; 5 | import { 6 | fileViewer, 7 | codeSettings as codeSettingsClass, 8 | } from "./FileViewer.module.scss"; 9 | import { MapExplorer } from "./V8DeoptInfoPanel/MapExplorer.jsx"; 10 | import { DeoptTables } from "./V8DeoptInfoPanel/DeoptTables.jsx"; 11 | import { hasMapData } from "../utils/mapUtils.js"; 12 | import { codeRoute, deoptsRoute, icsRoute, mapsRoute } from "../routes.js"; 13 | import { AppProvider } from "./appState.jsx"; 14 | 15 | /** 16 | * @typedef {keyof import('v8-deopt-parser').V8DeoptInfo} EntryKind 17 | * @typedef {{ fileId: number; tabId?: string }} RouteParams 18 | * @typedef {{ routeParams: RouteParams; deoptInfo: import('..').PerFileDeoptInfoWithSources; files: string[]; }} FileViewerProps 19 | * @param {FileViewerProps} props 20 | */ 21 | export function FileViewer({ files, deoptInfo, routeParams }) { 22 | const { fileId, tabId } = routeParams; 23 | // TODO: COnsider using local state for tab navigation 24 | // const selectedTab = useState(tabId); 25 | const fileDeoptInfo = deoptInfo.files[files[fileId]]; 26 | 27 | const [codeSettings, toggleSetting] = useCodeSettingsState(); 28 | const toggleShowLowSevs = () => toggleSetting("showLowSevs"); 29 | 30 | const hasMaps = hasMapData(deoptInfo.maps); 31 | 32 | return ( 33 |
    34 | 35 | 40 | 45 | 46 | 47 | 48 | {(params) => ( 49 | 58 | )} 59 | 60 | 61 | {(params) => ( 62 | 71 | )} 72 | 73 | 74 | {(params) => ( 75 | 84 | )} 85 | 86 | 87 | {(params) => ( 88 | 95 | )} 96 | 97 | 98 | 99 | 100 |
    101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/spectre.module.scss: -------------------------------------------------------------------------------- 1 | /*! Spectre.css | MIT License | github.com/picturepan2/spectre */ 2 | @import "spectre.css/src/variables"; 3 | @import "spectre.css/src/mixins"; 4 | 5 | // Reset and dependencies 6 | @import "spectre.css/src/normalize"; 7 | @import "spectre.css/src/base"; 8 | 9 | // Elements 10 | @import "spectre.css/src/typography"; 11 | // @import "spectre.css/src/asian"; 12 | @import "spectre.css/src/tables"; 13 | @import "spectre.css/src/buttons"; 14 | @import "spectre.css/src/forms"; 15 | // @import "spectre.css/src/labels"; 16 | // @import "spectre.css/src/codes"; 17 | // @import "spectre.css/src/media"; 18 | 19 | // Layout 20 | // @import "spectre.css/src/layout"; 21 | // @import "spectre.css/src/hero"; 22 | // @import "spectre.css/src/navbar"; 23 | 24 | // Components 25 | @import "spectre.css/src/accordions"; 26 | // @import "spectre.css/src/avatars"; 27 | // @import "spectre.css/src/badges"; 28 | // @import "spectre.css/src/breadcrumbs"; 29 | // @import "spectre.css/src/bars"; 30 | // @import "spectre.css/src/cards"; 31 | // @import "spectre.css/src/chips"; 32 | // @import "spectre.css/src/dropdowns"; 33 | // @import "spectre.css/src/empty"; 34 | @import "spectre.css/src/menus"; 35 | // @import "spectre.css/src/modals"; 36 | // @import "spectre.css/src/navs"; 37 | // @import "spectre.css/src/pagination"; 38 | @import "spectre.css/src/panels"; 39 | // @import "spectre.css/src/popovers"; 40 | // @import "spectre.css/src/steps"; 41 | @import "spectre.css/src/tabs"; 42 | // @import "spectre.css/src/tiles"; 43 | // @import "spectre.css/src/toasts"; 44 | // @import "spectre.css/src/tooltips"; 45 | 46 | // Utility classes 47 | // @import "spectre.css/src/animations"; 48 | // @import "spectre.css/src/utilities"; 49 | 50 | /*! Spectre.css Icons | MIT License | github.com/picturepan2/spectre */ 51 | // Icons 52 | @import "spectre.css/src/icons/icons-core"; 53 | @import "spectre.css/src/icons/icons-navigation"; 54 | @import "spectre.css/src/icons/icons-action"; // TODO: This includes a LOT of icons tha may not be used... 55 | // @import "spectre.css/src/icons/icons-object"; 56 | 57 | /*! Spectre.css Experimentals | MIT License | github.com/picturepan2/spectre */ 58 | // Experimentals 59 | // @import "spectre.css/src/autocomplete"; 60 | // @import "spectre.css/src/calendars"; 61 | // @import "spectre.css/src/carousels"; 62 | // @import "spectre.css/src/comparison-sliders"; 63 | // @import "spectre.css/src/filters"; 64 | // @import "spectre.css/src/meters"; 65 | // @import "spectre.css/src/off-canvas"; 66 | // @import "spectre.css/src/parallax"; 67 | // @import "spectre.css/src/progress"; 68 | // @import "spectre.css/src/sliders"; 69 | @import "spectre.css/src/timelines"; 70 | // @import "spectre.css/src/viewer-360"; 71 | 72 | // ========================== 73 | // Spectre fixes 74 | // ========================== 75 | 76 | .form-switch .form-icon { 77 | box-sizing: border-box; // Not sure why this isn't always inherited properly 78 | } 79 | 80 | summary.accordion-header { 81 | cursor: pointer; 82 | } 83 | 84 | // ========================== 85 | // Spectre Overrides 86 | // ========================== 87 | 88 | body { 89 | overflow-x: initial !important; 90 | } 91 | 92 | a:visited { 93 | color: #5755d9; 94 | } 95 | 96 | // ========================== 97 | // Spectre Extensions 98 | // ========================== 99 | 100 | .btn_inline { 101 | display: inline; 102 | padding: 0; 103 | height: auto; 104 | padding: 0; 105 | vertical-align: inherit; 106 | 107 | color: #5755d9; 108 | background: transparent; 109 | border-color: transparent; 110 | 111 | &:focus, 112 | &:hover, 113 | &:active { 114 | text-decoration: underline; 115 | background: transparent; 116 | border-color: transparent; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /examples/html-inline/adders.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Adders (inline) 7 | 8 | 9 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/src/v8-tools-core/csvparser.js: -------------------------------------------------------------------------------- 1 | // Copyright 2009 the V8 project authors. All rights reserved. 2 | // Redistribution and use in source and binary forms, with or without 3 | // modification, are permitted provided that the following conditions are 4 | // met: 5 | // 6 | // * Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // * Redistributions in binary form must reproduce the above 9 | // copyright notice, this list of conditions and the following 10 | // disclaimer in the documentation and/or other materials provided 11 | // with the distribution. 12 | // * Neither the name of Google Inc. nor the names of its 13 | // contributors may be used to endorse or promote products derived 14 | // from this software without specific prior written permission. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | 29 | /** 30 | * Creates a CSV lines parser. 31 | */ 32 | export default class CsvParser { 33 | /** 34 | * Converts \x00 and \u0000 escape sequences in the given string. 35 | * 36 | * @param {string} input field. 37 | **/ 38 | escapeField(string) { 39 | let nextPos = string.indexOf("\\"); 40 | if (nextPos === -1) return string; 41 | 42 | let result = string.substring(0, nextPos); 43 | // Escape sequences of the form \x00 and \u0000; 44 | let endPos = string.length; 45 | let pos = 0; 46 | while (nextPos !== -1) { 47 | let escapeIdentifier = string.charAt(nextPos + 1); 48 | pos = nextPos + 2; 49 | if (escapeIdentifier === 'n') { 50 | result += '\n'; 51 | nextPos = pos; 52 | } else if (escapeIdentifier === '\\') { 53 | result += '\\'; 54 | nextPos = pos; 55 | } else { 56 | if (escapeIdentifier === 'x') { 57 | // \x00 ascii range escapes consume 2 chars. 58 | nextPos = pos + 2; 59 | } else { 60 | // \u0000 unicode range escapes consume 4 chars. 61 | nextPos = pos + 4; 62 | } 63 | // Convert the selected escape sequence to a single character. 64 | let escapeChars = string.substring(pos, nextPos); 65 | result += String.fromCharCode(parseInt(escapeChars, 16)); 66 | } 67 | 68 | // Continue looking for the next escape sequence. 69 | pos = nextPos; 70 | nextPos = string.indexOf("\\", pos); 71 | // If there are no more escape sequences consume the rest of the string. 72 | if (nextPos === -1) { 73 | result += string.substr(pos); 74 | } else if (pos !== nextPos) { 75 | result += string.substring(pos, nextPos); 76 | } 77 | } 78 | return result; 79 | } 80 | 81 | /** 82 | * Parses a line of CSV-encoded values. Returns an array of fields. 83 | * 84 | * @param {string} line Input line. 85 | */ 86 | parseLine(line) { 87 | var pos = 0; 88 | var endPos = line.length; 89 | var fields = []; 90 | if (endPos == 0) return fields; 91 | let nextPos = 0; 92 | while(nextPos !== -1) { 93 | nextPos = line.indexOf(',', pos); 94 | let field; 95 | if (nextPos === -1) { 96 | field = line.substr(pos); 97 | } else { 98 | field = line.substring(pos, nextPos); 99 | } 100 | fields.push(this.escapeField(field)); 101 | pos = nextPos + 1; 102 | }; 103 | return fields 104 | } 105 | } -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/CodeSettings.jsx: -------------------------------------------------------------------------------- 1 | import { useReducer } from "preact/hooks"; 2 | import { 3 | menu, 4 | menu_item, 5 | form_icon, 6 | form_switch, 7 | } from "../spectre.module.scss"; 8 | import { 9 | codeSettings, 10 | dirty as dirtyClass, 11 | settingsBody, 12 | settingsMenu, 13 | } from "./CodeSettings.module.scss"; 14 | 15 | /** 16 | * @typedef CodeSettingsState 17 | * @property {boolean} showLowSevs 18 | * @property {boolean} hideLineNums 19 | * @property {boolean} showAllICs 20 | */ 21 | 22 | /** 23 | * @type {CodeSettingsState} 24 | */ 25 | const initialState = { 26 | // TODO: Consider replacing showLowSevs with a toggle 27 | // to show each severity (e.g. to filter out optimizable) 28 | showLowSevs: false, 29 | hideLineNums: false, 30 | showAllICs: false, 31 | }; 32 | 33 | /** 34 | * @returns {[CodeSettingsState, (setting: keyof CodeSettingsState) => void]} 35 | */ 36 | export function useCodeSettingsState() { 37 | return useReducer((state, settingToToggle) => { 38 | return { 39 | ...state, 40 | [settingToToggle]: !state[settingToToggle], 41 | }; 42 | }, initialState); 43 | } 44 | 45 | /** 46 | * @typedef {{ class?: string; state: CodeSettingsState; toggle: (setting: keyof CodeSettingsState) => void; }} CodeSettingsProps 47 | * @param {CodeSettingsProps} props 48 | */ 49 | export function CodeSettings({ class: className, state, toggle }) { 50 | const dirty = 51 | state.showLowSevs !== initialState.showLowSevs || 52 | state.hideLineNums !== initialState.hideLineNums || 53 | state.showAllICs !== initialState.showAllICs; 54 | 55 | const settings = [ 56 | { 57 | key: "showLowSevs", 58 | label: "Display Low Severities", 59 | checked: state.showLowSevs, 60 | onInput: () => toggle("showLowSevs"), 61 | }, 62 | { 63 | key: "hideLineNums", 64 | label: "Hide Line Numbers", 65 | checked: state.hideLineNums, 66 | onInput: () => toggle("hideLineNums"), 67 | }, 68 | { 69 | key: "showAllICs", 70 | label: "Show All Inline Cache Entries", 71 | checked: state.showAllICs, 72 | onInput: () => toggle("showAllICs"), 73 | }, 74 | ]; 75 | 76 | const rootClass = [ 77 | codeSettings, 78 | className, 79 | (dirty && dirtyClass) || null, 80 | ].join(" "); 81 | 82 | // TODO: Consider replacing
    with a custom alternative that closes on 83 | // Esc and clicking outside of it 84 | return ( 85 |
    86 | 87 | 88 | 89 |
    90 |
      91 | {settings.map((setting) => ( 92 |
    • 93 | 101 |
    • 102 | ))} 103 |
    104 |
    105 |
    106 | ); 107 | } 108 | 109 | function SettingsCog(props) { 110 | return ( 111 | 116 | 117 | 118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /packages/v8-deopt-viewer/src/index.js: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { 3 | open as openFile, 4 | readFile, 5 | writeFile, 6 | copyFile, 7 | mkdir, 8 | } from "fs/promises"; 9 | import { createReadStream } from "fs"; 10 | import { Packr } from "msgpackr"; 11 | import { fileURLToPath, pathToFileURL } from "url"; 12 | import open from "open"; 13 | import { get } from "httpie/dist/httpie.mjs"; 14 | import { generateV8Log } from "v8-deopt-generate-log"; 15 | import { parseV8LogStream, groupByFile } from "v8-deopt-parser"; 16 | import { determineCommonRoot } from "./determineCommonRoot.js"; 17 | 18 | // TODO: Replace with import.meta.resolve when stable 19 | import { createRequire } from "module"; 20 | 21 | // @ts-ignore 22 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 23 | const templatePath = path.join(__dirname, "template.html"); 24 | 25 | /** 26 | * @param {import('v8-deopt-parser').PerFileV8DeoptInfo["files"]} deoptInfo 27 | * @returns {Promise>} 28 | */ 29 | async function addSources(deoptInfo) { 30 | const files = Object.keys(deoptInfo); 31 | const root = determineCommonRoot(files); 32 | 33 | /** @type {Record} */ 34 | const result = Object.create(null); 35 | for (let file of files) { 36 | let srcPath; 37 | 38 | let src, srcError; 39 | if (file.startsWith("https://") || file.startsWith("http://")) { 40 | try { 41 | srcPath = file; 42 | const { data } = await get(file); 43 | src = data; 44 | } catch (e) { 45 | srcError = e; 46 | } 47 | } else { 48 | let filePath = file; 49 | if (file.startsWith("file://")) { 50 | // Convert Linux-like file URLs for Windows and assume C: root. Useful for testing 51 | if ( 52 | process.platform == "win32" && 53 | !file.match(/^file:\/\/\/[a-zA-z]:/) 54 | ) { 55 | filePath = fileURLToPath(file.replace(/^file:\/\/\//, "file:///C:/")); 56 | } else { 57 | filePath = fileURLToPath(file); 58 | } 59 | } 60 | 61 | if (path.isAbsolute(filePath)) { 62 | try { 63 | srcPath = filePath; 64 | src = await readFile(filePath, "utf8"); 65 | } catch (e) { 66 | srcError = e; 67 | } 68 | } else { 69 | srcError = new Error("File path is not absolute"); 70 | } 71 | } 72 | 73 | const relativePath = root ? file.slice(root.length) : file; 74 | if (srcError) { 75 | result[file] = { 76 | ...deoptInfo[file], 77 | relativePath, 78 | srcPath, 79 | srcError: srcError.toString(), 80 | }; 81 | } else { 82 | result[file] = { 83 | ...deoptInfo[file], 84 | relativePath, 85 | srcPath, 86 | src, 87 | }; 88 | } 89 | } 90 | 91 | return result; 92 | } 93 | 94 | /** 95 | * @param {string} srcFile 96 | * @param {import('.').Options} options 97 | */ 98 | export default async function run(srcFile, options) { 99 | let logFilePath; 100 | if (srcFile) { 101 | console.log("Running and generating log..."); 102 | logFilePath = await generateV8Log(srcFile, { 103 | logFilePath: path.join(options.out, "v8.log"), 104 | browserTimeoutMs: options.timeout, 105 | traceMaps: !options["skip-maps"], 106 | }); 107 | } else if (options.input) { 108 | logFilePath = path.isAbsolute(options.input) 109 | ? options.input 110 | : path.join(process.cwd(), options.input); 111 | } else { 112 | throw new Error( 113 | 'Either a file/url to generate a log or the "--input" flag pointing to a v8.log must be provided' 114 | ); 115 | } 116 | 117 | // Ensure output directory exists 118 | await mkdir(options.out, { recursive: true }); 119 | 120 | console.log("Parsing log..."); 121 | 122 | // using 16mb highWaterMark instead of default 64kb, it's not saving what much, like 1 second or less, 123 | // but why not 124 | // Also not setting big values because of default max-old-space=512mb 125 | const logContentsStream = await createReadStream(logFilePath, { 126 | encoding: "utf8", 127 | highWaterMark: 16 * 1024 * 1024, 128 | }); 129 | const rawDeoptInfo = await parseV8LogStream(logContentsStream, { 130 | keepInternals: options["keep-internals"], 131 | }); 132 | 133 | console.log("Adding sources..."); 134 | 135 | // Group DeoptInfo by files and extend the files data with sources 136 | const groupDeoptInfo = groupByFile(rawDeoptInfo); 137 | const deoptInfo = { 138 | ...groupDeoptInfo, 139 | files: await addSources(groupDeoptInfo.files), 140 | }; 141 | 142 | const deoptInfoString = new Packr({ variableMapSize: true }).encode( 143 | deoptInfo 144 | ); 145 | await writeFile( 146 | path.join(options.out, "v8-data.bin"), 147 | deoptInfoString, 148 | "utf8" 149 | ); 150 | 151 | console.log("Generating webapp..."); 152 | const template = await readFile(templatePath, "utf8"); 153 | const indexPath = path.join(options.out, "index.html"); 154 | await writeFile(indexPath, template, "utf8"); 155 | 156 | // @ts-ignore 157 | const require = createRequire(import.meta.url); 158 | const webAppIndexPath = require.resolve("v8-deopt-webapp"); 159 | const webAppStylesPath = webAppIndexPath.replace( 160 | path.basename(webAppIndexPath), 161 | "style.css" 162 | ); 163 | await copyFile(webAppIndexPath, path.join(options.out, "v8-deopt-webapp.js")); 164 | await copyFile( 165 | webAppStylesPath, 166 | path.join(options.out, "v8-deopt-webapp.css") 167 | ); 168 | 169 | if (options.open) { 170 | await open(pathToFileURL(indexPath).toString(), { url: true }); 171 | console.log( 172 | `Done! Opening ${path.join(options.out, "index.html")} in your browser...` 173 | ); 174 | } else { 175 | console.log( 176 | `Done! Open ${path.join(options.out, "index.html")} in your browser.` 177 | ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /packages/v8-deopt-generate-log/src/index.js: -------------------------------------------------------------------------------- 1 | import { tmpdir } from "os"; 2 | import { mkdir } from "fs/promises"; 3 | import * as path from "path"; 4 | import { execFile } from "child_process"; 5 | import { promisify } from "util"; 6 | import { pathToFileURL } from "url"; 7 | 8 | const execFileAsync = promisify(execFile); 9 | const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 10 | const makeAbsolute = (filePath) => 11 | path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath); 12 | 13 | /** 14 | * @typedef {(options: import('puppeteer-core').LaunchOptions) => Promise} Launcher 15 | * @type {Launcher} 16 | */ 17 | let launcher = null; 18 | 19 | /** 20 | * @returns {Promise} 21 | */ 22 | async function getLauncher() { 23 | if (!launcher) { 24 | // 1. Try puppeteer 25 | try { 26 | const puppeteer = (await import("puppeteer")).default; 27 | launcher = (options) => puppeteer.launch(options); 28 | } catch (error) { 29 | if (error.code !== "ERR_MODULE_NOT_FOUND") { 30 | // console.error(error); 31 | } 32 | } 33 | 34 | // 2. Try chrome-launcher 35 | if (!launcher) { 36 | const [chromeLauncher, puppeteer] = await Promise.all([ 37 | import("chrome-launcher").then((m) => m.default), 38 | import("puppeteer-core").then((m) => m.default), 39 | ]); 40 | 41 | const chromePath = chromeLauncher.Launcher.getFirstInstallation(); 42 | if (!chromePath) { 43 | console.error( 44 | 'Could not find the "puppeteer" package or a local chrome installation. Try installing Chrome or Chromium locally to run v8-deopt-viewer' 45 | ); 46 | process.exit(1); 47 | } 48 | 49 | // console.log("Using Chrome installed at:", chromePath); 50 | launcher = (options) => 51 | puppeteer.launch({ 52 | ...options, 53 | executablePath: chromePath, 54 | }); 55 | } 56 | } 57 | 58 | return launcher; 59 | } 60 | 61 | /** 62 | * @param {import('puppeteer-core').LaunchOptions} options 63 | * @returns {Promise} 64 | */ 65 | async function launchBrowser(options) { 66 | return (await getLauncher())(options); 67 | } 68 | 69 | /** 70 | * @param {string} logFilePath 71 | * @param {boolean} [hasNewCliArgs] 72 | * @param {boolean} [traceMaps] 73 | * @returns {string[]} 74 | */ 75 | function getV8Flags(logFilePath, hasNewCliArgs = false, traceMaps = false) { 76 | const flags = [ 77 | hasNewCliArgs ? "--log-ic" : "--trace-ic", 78 | // Could pipe log to stdout ("-" value) but doesn't work very well with 79 | // Chromium. Chromium won't pipe v8 logs to a non-TTY pipe it seems :( 80 | `--logfile=${logFilePath}`, 81 | "--no-logfile-per-isolate", 82 | ]; 83 | 84 | if (traceMaps) { 85 | // --trace-maps-details doesn't seem to change output so leaving it out 86 | // Note: Newer versions of V8 renamed flags from `--log-maps` to 87 | // `--trace-maps`. Same for `--trace-maps-details` vs 88 | // `--log-maps-details` 89 | flags.push(hasNewCliArgs ? "--log-maps" : "--trace-maps"); 90 | } 91 | 92 | return flags; 93 | } 94 | 95 | /** 96 | * @param {string} srcUrl 97 | * @param {import('../').Options} options 98 | */ 99 | async function runPuppeteer(srcUrl, options) { 100 | const logFilePath = options.logFilePath; 101 | 102 | // Our GitHub actions started failing after the release of Chrome 90 so let's 103 | // assume Chrome 90 contains the new V8 version that requires the new flags. 104 | // Consider in the future trying to detect the version of chrome being used, 105 | // but for now let's just always use the new flags since most people 106 | // auto-update Chrome to the latest. 107 | const hasNewCliArgs = true; 108 | const v8Flags = getV8Flags(logFilePath, hasNewCliArgs, options.traceMaps); 109 | const args = [ 110 | "--disable-extensions", 111 | `--js-flags=${v8Flags.join(" ")}`, 112 | `--no-sandbox`, 113 | srcUrl, 114 | ]; 115 | 116 | let browser; 117 | try { 118 | browser = await launchBrowser({ 119 | ignoreDefaultArgs: ["about:blank"], 120 | args, 121 | }); 122 | 123 | await browser.pages(); 124 | 125 | // Wait 5s to allow page to load 126 | await delay(options.browserTimeoutMs); 127 | } finally { 128 | if (browser) { 129 | await browser.close(); 130 | // Give the browser 1s to release v8.log 131 | await delay(100); 132 | } 133 | } 134 | 135 | return logFilePath; 136 | } 137 | 138 | async function generateForRemoteURL(srcUrl, options) { 139 | return runPuppeteer(srcUrl, options); 140 | } 141 | 142 | async function generateForLocalHTML(srcPath, options) { 143 | const srcUrl = pathToFileURL(makeAbsolute(srcPath)).toString(); 144 | return runPuppeteer(srcUrl, options); 145 | } 146 | 147 | /** 148 | * @param {string} srcPath 149 | * @param {import('.').Options} options 150 | */ 151 | async function generateForNodeJS(srcPath, options) { 152 | const logFilePath = options.logFilePath; 153 | const hasNewCliArgs = +process.versions.node.match(/^(\d+)/)[0] >= 16; 154 | const args = [ 155 | ...getV8Flags(logFilePath, hasNewCliArgs, options.traceMaps), 156 | srcPath, 157 | ]; 158 | 159 | await execFileAsync(process.execPath, args, {}); 160 | 161 | return logFilePath; 162 | } 163 | 164 | /** @type {import('.').Options} */ 165 | const defaultOptions = { 166 | logFilePath: `${tmpdir()}/v8-deopt-generate-log/v8.log`, 167 | browserTimeoutMs: 5000, 168 | }; 169 | 170 | /** 171 | * @param {string} srcPath 172 | * @param {import('.').Options} options 173 | * @returns {Promise} 174 | */ 175 | export async function generateV8Log(srcPath, options = {}) { 176 | options = Object.assign({}, defaultOptions, options); 177 | options.logFilePath = makeAbsolute(options.logFilePath); 178 | 179 | await mkdir(path.dirname(options.logFilePath), { recursive: true }); 180 | 181 | if (srcPath.startsWith("https://") || srcPath.startsWith("http://")) { 182 | return generateForRemoteURL(srcPath, options); 183 | } else if (srcPath.endsWith(".html")) { 184 | return generateForLocalHTML(srcPath, options); 185 | } else { 186 | return generateForNodeJS(srcPath, options); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /packages/v8-deopt-webapp/src/components/CodePanel.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | useState, 3 | useMemo, 4 | useRef, 5 | useLayoutEffect, 6 | useEffect, 7 | } from "preact/hooks"; 8 | import { memo, forwardRef } from "preact/compat"; 9 | import Prism from "prismjs"; 10 | import { addDeoptMarkers, getMarkerId } from "../utils/deoptMarkers"; 11 | import { useAppDispatch, useAppState } from "./appState"; 12 | 13 | // Styles - order matters. prism.scss must come first so its styles can be 14 | // overridden by other files 15 | import "../prism.scss"; 16 | import { codePanel, error as errorClass } from "./CodePanel.module.scss"; 17 | import { 18 | showLowSevs as showLowSevsClass, 19 | active, 20 | } from "../utils/deoptMarkers.module.scss"; 21 | 22 | // Turn on auto highlighting by Prism 23 | Prism.manual = true; 24 | 25 | /** 26 | * @param {string} path 27 | */ 28 | function determineLanguage(path) { 29 | if (path.endsWith(".html")) { 30 | return "html"; 31 | } else if ( 32 | (path.startsWith("http:") || path.startsWith("https:")) && 33 | !path.match(/\.[mc]?jsx?$/) 34 | ) { 35 | // Assume URLs without .js extensions are HTML pages 36 | return "html"; 37 | } else { 38 | return "javascript"; 39 | } 40 | } 41 | 42 | /** 43 | * @param {import('v8-deopt-parser').Entry} entry 44 | * @param {boolean} shouldHighlight 45 | */ 46 | export function useHighlightEntry(entry, shouldHighlight) { 47 | const { setSelectedEntry } = useAppDispatch(); 48 | useEffect(() => { 49 | if (shouldHighlight) { 50 | setSelectedEntry(entry); 51 | } 52 | }, [shouldHighlight]); 53 | } 54 | 55 | /** 56 | * @typedef CodePanelProps 57 | * @property {import("..").FileV8DeoptInfoWithSources} fileDeoptInfo 58 | * @property {number} fileId 59 | * @property {import('./CodeSettings').CodeSettingsState} settings 60 | * @param {CodePanelProps} props 61 | */ 62 | export function CodePanel({ fileDeoptInfo, fileId, settings }) { 63 | if (fileDeoptInfo.srcError) { 64 | return ; 65 | } else if (!fileDeoptInfo.src) { 66 | return ; 67 | } 68 | 69 | const lang = determineLanguage(fileDeoptInfo.srcPath); 70 | 71 | const state = useAppState(); 72 | const selectedLine = state.selectedPosition?.line; 73 | 74 | /** 75 | * @typedef {Map} MarkerMap 76 | * @type {[MarkerMap, import('preact/hooks').StateUpdater]} 77 | */ 78 | const [markers, setMarkers] = useState(null); 79 | 80 | /** @type {import('preact').RefObject} */ 81 | const codeRef = useRef(null); 82 | useLayoutEffect(() => { 83 | // Saved the new markers so we can select them when CodePanelContext changes 84 | const markers = addDeoptMarkers(codeRef.current, fileId, fileDeoptInfo); 85 | setMarkers(new Map(markers.map((marker) => [marker.id, marker]))); 86 | }, [fileId, fileDeoptInfo]); 87 | 88 | useEffect(() => { 89 | if (state.prevSelectedEntry) { 90 | markers 91 | .get(getMarkerId(state.prevSelectedEntry)) 92 | ?.classList.remove(active); 93 | } 94 | 95 | /** @type {ScrollIntoViewOptions} */ 96 | const scrollIntoViewOpts = { block: "center", behavior: "smooth" }; 97 | if (state.selectedEntry) { 98 | const target = markers.get(getMarkerId(state.selectedEntry)); 99 | target.classList.add(active); 100 | // TODO: Why doesn't the smooth behavior always work? It seems that only 101 | // the first or last call to scrollIntoView with behavior smooth works? 102 | target.scrollIntoView(scrollIntoViewOpts); 103 | } else if (state.selectedPosition) { 104 | const lineSelector = `.line-numbers-rows > span:nth-child(${state.selectedPosition.line})`; 105 | document.querySelector(lineSelector)?.scrollIntoView(scrollIntoViewOpts); 106 | } 107 | 108 | // TODO: Figure out how to scroll line number into view when 109 | // selectedPosition is set but selectedMarkerId is not 110 | }, [state]); 111 | 112 | return ( 113 |
    119 | 125 | 126 | 127 |
    128 | ); 129 | } 130 | 131 | /** 132 | * @typedef {{ lang: string; src: string; class?: string; children?: any }} PrismCodeProps 133 | * @type {import('preact').FunctionComponent} 134 | */ 135 | const PrismCode = forwardRef(function PrismCode(props, ref) { 136 | const className = [`language-${props.lang}`, props.class].join(" "); 137 | 138 | // TODO: File route changes will unmount and delete this cache. May be useful 139 | // to cache across files so switching back and forth between files doesn't 140 | // re-highlight the file each time 141 | const __html = useMemo( 142 | () => Prism.highlight(props.src, Prism.languages[props.lang], props.lang), 143 | [props.src, props.lang] 144 | ); 145 | 146 | return ( 147 |
    148 | 			
    149 | 			{props.children}
    150 | 		
    151 | ); 152 | }); 153 | 154 | const NEW_LINE_EXP = /\n(?!$)/g; 155 | 156 | /** 157 | * @param {{ selectedLine: number; contents: string }} props 158 | */ 159 | const LineNumbers = memo(function LineNumbers({ selectedLine, contents }) { 160 | // TODO: Do we want to cache these results beyond renders and for all 161 | // combinations? memo will only remember the last combination. 162 | const lines = useMemo(() => contents.split(NEW_LINE_EXP), [contents]); 163 | return ( 164 |