├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .vscode └── launch.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── codeql-analysis.yml ├── examples ├── html-external │ ├── adders.js │ ├── index.html │ └── objects.js ├── html-inline │ └── adders.html ├── simple │ └── adders.js ├── two-modules │ ├── adders.js │ └── objects.js └── v8-deopt-webapp.png ├── package-lock.json ├── package.json └── packages ├── v8-deopt-generate-log ├── CHANGELOG.md ├── README.md ├── package.json ├── src │ ├── index.d.ts │ └── index.js └── test │ └── index.test.js ├── v8-deopt-parser ├── CHANGELOG.md ├── README.md ├── package.json ├── src │ ├── DeoptLogReader.js │ ├── deoptParsers.js │ ├── findEntry.js │ ├── groupBy.js │ ├── index.d.ts │ ├── index.js │ ├── optimizationStateParsers.js │ ├── propertyICParsers.js │ ├── sortEntries.js │ ├── utils.js │ └── v8-tools-core │ │ ├── Readme.md │ │ ├── codemap.js │ │ ├── csvparser.js │ │ ├── logreader.js │ │ ├── profile.js │ │ └── splaytree.js └── test │ ├── constants.js │ ├── groupBy.test.js │ ├── helpers.js │ ├── index.test.js │ ├── logs │ ├── adders.node16.v8.log.br │ ├── adders.node16_14.v8.log.br │ ├── adders.traceMaps.v8.log.br │ ├── adders.v8.log │ ├── brotli.js │ ├── html-external.traceMaps.v8.log │ ├── html-external.v8.log │ ├── html-inline.traceMaps.v8.log │ ├── html-inline.v8.log │ ├── two-modules.v8.log │ └── v8-deopt-parser.v8.log.br │ ├── parseLogs.js │ ├── parseV8Log.test.js │ ├── parseV8Log.traceMaps.test.js │ ├── snapshots │ ├── adders.json │ ├── adders.traceMaps.json │ ├── adders.traceMaps.mapTree.txt │ ├── html-external.json │ ├── html-external.traceMaps.json │ ├── html-external.traceMaps.mapTree.txt │ ├── html-inline.json │ ├── html-inline.traceMaps.json │ ├── html-inline.traceMaps.mapTree.txt │ └── two-modules.json │ ├── traceMapsHelpers.js │ └── utils.test.js ├── v8-deopt-viewer ├── CHANGELOG.md ├── bin │ └── v8-deopt-viewer.js ├── package.json ├── scripts │ └── prepare.js ├── src │ ├── determineCommonRoot.js │ ├── index.d.ts │ ├── index.js │ └── template.html └── test │ └── determineCommonRoot.test.js └── v8-deopt-webapp ├── .gitignore ├── CHANGELOG.md ├── README.md ├── index.html ├── jsconfig.json ├── package.json ├── src ├── _variables.scss ├── components │ ├── App.jsx │ ├── App.module.scss │ ├── CodePanel.jsx │ ├── CodePanel.module.scss │ ├── CodeSettings.jsx │ ├── CodeSettings.module.scss │ ├── FileViewer.jsx │ ├── FileViewer.module.scss │ ├── Summary.jsx │ ├── SummaryList.jsx │ ├── SummaryList.module.scss │ ├── SummaryTable.jsx │ ├── SummaryTable.module.scss │ ├── V8DeoptInfoPanel │ │ ├── DeoptTables.jsx │ │ ├── DeoptTables.module.scss │ │ ├── MapExplorer.jsx │ │ ├── MapExplorer.module.scss │ │ ├── index.jsx │ │ └── index.module.scss │ └── appState.jsx ├── index.d.ts ├── index.jsx ├── modules.d.ts ├── prism.scss ├── routes.js ├── spectre.module.scss ├── theme.module.scss └── utils │ ├── deoptMarkers.js │ ├── deoptMarkers.module.scss │ ├── mapUtils.js │ └── useHashLocation.js ├── test └── generateTestData.mjs └── vite.config.mjs /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/v8-deopt-parser/src/v8-tools-core -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/html-external/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Adders (external) 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/html-inline/adders.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Adders (inline) 7 | 8 | 9 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/v8-deopt-webapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewiggins/v8-deopt-viewer/192f4d4d1df4d56b215e23ec9a1aec2c99e227af/examples/v8-deopt-webapp.png -------------------------------------------------------------------------------- /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-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-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-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 | -------------------------------------------------------------------------------- /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-generate-log/test/index.test.js: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { readFile } from "fs/promises"; 3 | import { pathToFileURL, fileURLToPath } from "url"; 4 | import test from "tape"; 5 | import { generateV8Log } from "../src/index.js"; 6 | 7 | // @ts-ignore 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 | const pkgPath = (...args) => path.join(__dirname, "..", ...args); 10 | const repoRoot = (...args) => pkgPath("..", "..", ...args); 11 | const getGHPageUrl = (path) => 12 | "https://andrewiggins.github.io/v8-deopt-viewer/examples/" + path; 13 | 14 | const traceMapMatches = [/^map,/m, /^map-create,/m, /^map-details,/m]; 15 | 16 | /** 17 | * @param {string} srcFilePath 18 | * @param {string} extraLogFileName 19 | * @param {import('..').Options} [options] 20 | * @returns {Promise} 21 | */ 22 | async function runGenerateV8Log( 23 | srcFilePath, 24 | extraLogFileName = "", 25 | options = {} 26 | ) { 27 | let outputParentDir, outputFileName; 28 | if (srcFilePath.startsWith("http:") || srcFilePath.startsWith("https:")) { 29 | const url = new URL(srcFilePath); 30 | const pathParts = url.pathname.split("/"); 31 | outputParentDir = url.host + "-" + pathParts[pathParts.length - 2]; 32 | outputFileName = 33 | pathParts[pathParts.length - 1] + extraLogFileName + ".v8.log"; 34 | } else { 35 | outputParentDir = path.basename(path.dirname(srcFilePath)); 36 | outputFileName = path.basename(srcFilePath) + extraLogFileName + ".v8.log"; 37 | } 38 | 39 | const outputPath = pkgPath("test", "logs", outputParentDir, outputFileName); 40 | 41 | await generateV8Log(srcFilePath, { 42 | ...options, 43 | logFilePath: outputPath, 44 | browserTimeoutMs: 2e3, 45 | }); 46 | 47 | return readFile(outputPath, "utf8"); 48 | } 49 | 50 | /** 51 | * @param {import('tape').Test} t 52 | * @param {string} content 53 | * @param {string[]} srcFiles 54 | */ 55 | function verifySrcFiles(t, content, srcFiles) { 56 | for (let srcFile of srcFiles) { 57 | srcFile = srcFile.replace(/\\/g, "\\\\"); 58 | t.equal(content.includes(srcFile), true, `Content contains ${srcFile}`); 59 | } 60 | 61 | traceMapMatches.forEach((matcher) => { 62 | t.equal(matcher.test(content), false, "Content does not match " + matcher); 63 | }); 64 | } 65 | 66 | test("generateV8Log(simple/adders.js)", async (t) => { 67 | const srcFilePath = repoRoot("examples/simple/adders.js"); 68 | const logContent = await runGenerateV8Log(srcFilePath); 69 | 70 | verifySrcFiles(t, logContent, [srcFilePath]); 71 | }); 72 | 73 | test("generateV8Log(simple/adders.js) relative path", async (t) => { 74 | const fullPath = repoRoot("examples/simple/adders.js"); 75 | const srcFilePath = path.relative(process.cwd(), fullPath); 76 | const logContent = await runGenerateV8Log(srcFilePath); 77 | 78 | verifySrcFiles(t, logContent, [fullPath]); 79 | }); 80 | 81 | test("generateV8Log(two-modules/adders.js)", async (t) => { 82 | const srcFilePath = repoRoot("examples/two-modules/adders.js"); 83 | const logContent = await runGenerateV8Log(srcFilePath); 84 | 85 | verifySrcFiles(t, logContent, [ 86 | srcFilePath, 87 | repoRoot("examples/two-modules/objects.js"), 88 | ]); 89 | }); 90 | 91 | test("generateV8Log(html-inline/adders.html)", async (t) => { 92 | const srcFilePath = repoRoot("examples/html-inline/adders.html"); 93 | const logContent = await runGenerateV8Log(srcFilePath); 94 | 95 | verifySrcFiles(t, logContent, [pathToFileURL(srcFilePath).toString()]); 96 | }); 97 | 98 | test("generateV8Log(html-external/index.html)", async (t) => { 99 | const srcFilePath = repoRoot("examples/html-external/index.html"); 100 | const logContent = await runGenerateV8Log(srcFilePath); 101 | 102 | verifySrcFiles(t, logContent, [ 103 | pathToFileURL(repoRoot("examples/html-external/adders.js")).toString(), 104 | pathToFileURL(repoRoot("examples/html-external/objects.js")).toString(), 105 | ]); 106 | }); 107 | 108 | test("generateV8Log(GitHub Pages html-inline/adders.html)", async (t) => { 109 | const srcFilePath = getGHPageUrl("html-inline/adders.html"); 110 | const logContent = await runGenerateV8Log(srcFilePath); 111 | 112 | verifySrcFiles(t, logContent, [srcFilePath]); 113 | }); 114 | 115 | test("generateV8Log(GitHub Pages html-external/index.html)", async (t) => { 116 | const srcFilePath = getGHPageUrl("html-external/index.html"); 117 | const logContent = await runGenerateV8Log(srcFilePath); 118 | 119 | verifySrcFiles(t, logContent, [ 120 | getGHPageUrl("html-external/adders.js"), 121 | getGHPageUrl("html-external/objects.js"), 122 | ]); 123 | }); 124 | 125 | test("generateV8Log(simple/adders.js, traceMaps: true)", async (t) => { 126 | const fullPath = repoRoot("examples/simple/adders.js"); 127 | const srcFilePath = path.relative(process.cwd(), fullPath); 128 | const logContent = await runGenerateV8Log(srcFilePath, ".traceMaps", { 129 | traceMaps: true, 130 | }); 131 | 132 | traceMapMatches.forEach((matcher) => { 133 | t.equal(matcher.test(logContent), true, "Content does match " + matcher); 134 | }); 135 | }); 136 | 137 | test("generateV8Log(two-modules/adders.js, traceMaps: true)", async (t) => { 138 | const fullPath = repoRoot("examples/two-modules/adders.js"); 139 | const srcFilePath = path.relative(process.cwd(), fullPath); 140 | const logContent = await runGenerateV8Log(srcFilePath, ".traceMaps", { 141 | traceMaps: true, 142 | }); 143 | 144 | traceMapMatches.forEach((matcher) => { 145 | t.equal(matcher.test(logContent), true, "Content does match " + matcher); 146 | }); 147 | }); 148 | 149 | test("generateV8Log(html-inline/adders.html, traceMaps: true)", async (t) => { 150 | const srcFilePath = repoRoot("examples/html-inline/adders.html"); 151 | const logContent = await runGenerateV8Log(srcFilePath, ".traceMaps", { 152 | traceMaps: true, 153 | }); 154 | 155 | traceMapMatches.forEach((matcher) => { 156 | t.equal(matcher.test(logContent), true, "Content does match " + matcher); 157 | }); 158 | }); 159 | 160 | test("generateV8Log(html-external/index.html, traceMaps: true)", async (t) => { 161 | const srcFilePath = repoRoot("examples/html-external/index.html"); 162 | const logContent = await runGenerateV8Log(srcFilePath, ".traceMaps", { 163 | traceMaps: true, 164 | }); 165 | 166 | traceMapMatches.forEach((matcher) => { 167 | t.equal(matcher.test(logContent), true, "Content does match " + matcher); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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-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-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/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-parser/src/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface FilePosition { 4 | functionName: string; 5 | file: string; 6 | line: number; 7 | column: number; 8 | optimizationState?: CodeState; 9 | } 10 | 11 | // ====================================== 12 | // #region Code types 13 | 14 | type CodeState = "compiled" | "optimizable" | "optimized" | "unknown"; 15 | 16 | interface CodeEntry { 17 | type: "codes"; 18 | id: string; 19 | functionName: string; 20 | file: string; 21 | line: number; 22 | column: number; 23 | isScript: boolean; 24 | severity: number; 25 | updates: CodeEntryUpdate[]; 26 | } 27 | 28 | interface CodeEntryUpdate { 29 | timestamp: number; 30 | state: CodeState; 31 | severity: number; 32 | } 33 | 34 | // #endregion 35 | // ====================================== 36 | 37 | // ====================================== 38 | // #region Deopt types 39 | 40 | interface DeoptEntry { 41 | type: "deopts"; 42 | id: string; 43 | functionName: string; 44 | file: string; 45 | line: number; 46 | column: number; 47 | severity: number; 48 | updates: DeoptEntryUpdate[]; 49 | } 50 | 51 | interface DeoptEntryUpdate { 52 | timestamp: number; 53 | bailoutType: string; 54 | deoptReason: string; 55 | optimizationState: string; 56 | inlined: boolean; 57 | severity: number; 58 | inlinedAt?: InlinedLocation; 59 | } 60 | 61 | interface InlinedLocation { 62 | file: string; 63 | line: number; 64 | column: number; 65 | } 66 | 67 | // #endregion 68 | // ====================================== 69 | 70 | // ====================================== 71 | // #region Inline Cache types 72 | type ICState = 73 | | "uninitialized" 74 | | "premonomorphic" 75 | | "monomorphic" 76 | | "recompute_handler" 77 | | "polymorphic" 78 | | "megamorphic" 79 | | "generic" 80 | | "megadom" 81 | | "no_feedback" 82 | | "unknown"; 83 | 84 | interface ICEntry { 85 | type: "ics"; 86 | id: string; 87 | functionName: string; 88 | file: string; 89 | line: number; 90 | column: number; 91 | severity: number; 92 | updates: ICEntryUpdate[]; 93 | } 94 | 95 | interface ICEntryUpdate { 96 | type: string; 97 | oldState: ICState; 98 | newState: ICState; 99 | key: string; 100 | map: string; 101 | optimizationState: string; 102 | severity: number; 103 | modifier: string; 104 | slowReason: string; 105 | } 106 | 107 | // #endregion 108 | // ====================================== 109 | 110 | // ====================================== 111 | // #region V8 Map types 112 | 113 | interface MapEntry { 114 | // Add an artificial type property to differentiate between MapEntries and MapEdges 115 | type: "maps"; 116 | id: string; 117 | address: number; 118 | time: number; 119 | description: string; 120 | 121 | /** Parent Edge ID */ 122 | edge?: string; 123 | 124 | /** Children Edge IDs */ 125 | children?: string[]; 126 | isDeprecated?: boolean; 127 | filePosition?: FilePosition; 128 | } 129 | 130 | type MapEdgeType = 131 | | "Transition" 132 | | "Normalize" // FastToSlow 133 | | "SlowToFast" 134 | | "InitialMap" 135 | | "new" 136 | | "ReplaceDescriptors" 137 | | "CopyAsPrototype" 138 | | "OptimizeAsPrototype"; 139 | 140 | interface MapEdge { 141 | type: "mapsEdge"; 142 | subtype: MapEdgeType; 143 | id: string; 144 | name: string; 145 | reason: string; 146 | time: number; 147 | from: string; 148 | to: string; 149 | } 150 | 151 | interface MapData { 152 | nodes: Record; 153 | edges: Record; 154 | } 155 | 156 | // #endregion 157 | // ====================================== 158 | 159 | type Entry = ICEntry | DeoptEntry | CodeEntry; 160 | 161 | interface FileV8DeoptInfo { 162 | id: string; 163 | ics: ICEntry[]; 164 | deopts: DeoptEntry[]; 165 | codes: CodeEntry[]; 166 | } 167 | 168 | interface V8DeoptInfo extends FileV8DeoptInfo { 169 | maps: MapData; 170 | } 171 | 172 | interface PerFileV8DeoptInfo { 173 | files: Record; 174 | maps: MapData; 175 | } 176 | 177 | interface Options { 178 | keepInternals?: boolean; 179 | sortEntries?: boolean; 180 | } 181 | 182 | // ====================================== 183 | // #region Exports 184 | 185 | /** 186 | * Parse the deoptimizations from a v8.log file 187 | * @param v8LogContent The contents of a v8.log file 188 | * @param options Options to influence the parsing of the V8 log 189 | */ 190 | export function parseV8Log( 191 | v8LogContent: string, 192 | options?: Options 193 | ): Promise; 194 | /** 195 | * Parse the deoptimizations from a v8.log file, but this version does it from stream 196 | * @param v8LogContent The contents of a v8.log file 197 | * @param options Options to influence the parsing of the V8 log 198 | */ 199 | export function parseV8LogStream( 200 | v8LogContent: Generator | string>, 201 | options?: Options 202 | ): Promise; 203 | 204 | /** 205 | * Group the V8 deopt information into an object mapping files to the relevant 206 | * data 207 | * @param rawDeoptInfo A V8DeoptInfo object from `parseV8Log` 208 | */ 209 | export function groupByFile(rawDeoptInfo: V8DeoptInfo): PerFileV8DeoptInfo; 210 | 211 | /** 212 | * Find an entry in a V8DeoptInfo object 213 | * @param deoptInfo A V8DeoptInfo object from `parseV8Log` 214 | * @param entryId The ID of the entry to find 215 | */ 216 | export function findEntry( 217 | // TODO: Either update to handle map entries or change type to FileV8DeoptInfo? 218 | deoptInfo: V8DeoptInfo, 219 | entryId: string 220 | ): Entry | null; 221 | 222 | /** 223 | * Sort V8 Deopt entries by line, number, and type. Modifies the original array. 224 | * @param entries A list of V8 Deopt Entries 225 | * @returns The sorted entries 226 | */ 227 | // TODO: Either update to handle map entries or change type to exclude map entries? 228 | export function sortEntries(entries: Entry[]): Entry[]; 229 | 230 | /** 231 | * Get the severity of an Inline Cache state 232 | * @param state An Inline Cache state 233 | */ 234 | export function severityIcState(state: ICState): number; 235 | 236 | /** The minimum severity an update or entry can be. */ 237 | export const MIN_SEVERITY = 1; 238 | 239 | /** The value used when severity cannot be determined. */ 240 | export const UNKNOWN_SEVERITY = -1; 241 | 242 | // #endregion 243 | // ====================================== 244 | -------------------------------------------------------------------------------- /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-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-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 | -------------------------------------------------------------------------------- /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-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/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-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-parser/src/v8-tools-core/logreader.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 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 | import CsvParser from './csvparser.js'; 29 | 30 | /** 31 | * @fileoverview Log Reader is used to process log file produced by V8. 32 | */ 33 | 34 | 35 | /** 36 | * Base class for processing log files. 37 | * 38 | * @param {Object} dispatchTable A table used for parsing and processing 39 | * log records. 40 | * @param {boolean} timedRange Ignore ticks outside timed range. 41 | * @param {boolean} pairwiseTimedRange Ignore ticks outside pairs of timer 42 | * markers. 43 | * @constructor 44 | */ 45 | export function LogReader(dispatchTable, timedRange, pairwiseTimedRange) { 46 | /** 47 | * @type {Object} 48 | */ 49 | this.dispatchTable_ = dispatchTable; 50 | 51 | /** 52 | * @type {boolean} 53 | */ 54 | this.timedRange_ = timedRange; 55 | 56 | /** 57 | * @type {boolean} 58 | */ 59 | this.pairwiseTimedRange_ = pairwiseTimedRange; 60 | if (pairwiseTimedRange) { 61 | this.timedRange_ = true; 62 | } 63 | 64 | /** 65 | * Current line. 66 | * @type {number} 67 | */ 68 | this.lineNum_ = 0; 69 | 70 | /** 71 | * CSV lines parser. 72 | * @type {CsvParser} 73 | */ 74 | this.csvParser_ = new CsvParser(); 75 | 76 | /** 77 | * Keeps track of whether we've seen a "current-time" tick yet. 78 | * @type {boolean} 79 | */ 80 | this.hasSeenTimerMarker_ = false; 81 | 82 | /** 83 | * List of log lines seen since last "current-time" tick. 84 | * @type {Array.} 85 | */ 86 | this.logLinesSinceLastTimerMarker_ = []; 87 | }; 88 | 89 | 90 | /** 91 | * Used for printing error messages. 92 | * 93 | * @param {string} str Error message. 94 | */ 95 | LogReader.prototype.printError = function(str) { 96 | // Do nothing. 97 | }; 98 | 99 | 100 | /** 101 | * Processes a portion of V8 profiler event log. 102 | * 103 | * @param {string} chunk A portion of log. 104 | */ 105 | LogReader.prototype.processLogChunk = function(chunk) { 106 | this.processLog_(chunk.split('\n')); 107 | }; 108 | 109 | 110 | /** 111 | * Processes a line of V8 profiler event log. 112 | * 113 | * @param {string} line A line of log. 114 | */ 115 | LogReader.prototype.processLogLine = function(line) { 116 | if (!this.timedRange_) { 117 | this.processLogLine_(line); 118 | return; 119 | } 120 | if (line.startsWith("current-time")) { 121 | if (this.hasSeenTimerMarker_) { 122 | this.processLog_(this.logLinesSinceLastTimerMarker_); 123 | this.logLinesSinceLastTimerMarker_ = []; 124 | // In pairwise mode, a "current-time" line ends the timed range. 125 | if (this.pairwiseTimedRange_) { 126 | this.hasSeenTimerMarker_ = false; 127 | } 128 | } else { 129 | this.hasSeenTimerMarker_ = true; 130 | } 131 | } else { 132 | if (this.hasSeenTimerMarker_) { 133 | this.logLinesSinceLastTimerMarker_.push(line); 134 | } else if (!line.startsWith("tick")) { 135 | this.processLogLine_(line); 136 | } 137 | } 138 | }; 139 | 140 | 141 | /** 142 | * Processes stack record. 143 | * 144 | * @param {number} pc Program counter. 145 | * @param {number} func JS Function. 146 | * @param {Array.} stack String representation of a stack. 147 | * @return {Array.} Processed stack. 148 | */ 149 | LogReader.prototype.processStack = function(pc, func, stack) { 150 | var fullStack = func ? [pc, func] : [pc]; 151 | var prevFrame = pc; 152 | for (var i = 0, n = stack.length; i < n; ++i) { 153 | var frame = stack[i]; 154 | var firstChar = frame.charAt(0); 155 | if (firstChar == '+' || firstChar == '-') { 156 | // An offset from the previous frame. 157 | prevFrame += parseInt(frame, 16); 158 | fullStack.push(prevFrame); 159 | // Filter out possible 'overflow' string. 160 | } else if (firstChar != 'o') { 161 | fullStack.push(parseInt(frame, 16)); 162 | } else { 163 | this.printError("dropping: " + frame); 164 | } 165 | } 166 | return fullStack; 167 | }; 168 | 169 | 170 | /** 171 | * Returns whether a particular dispatch must be skipped. 172 | * 173 | * @param {!Object} dispatch Dispatch record. 174 | * @return {boolean} True if dispatch must be skipped. 175 | */ 176 | LogReader.prototype.skipDispatch = function(dispatch) { 177 | return false; 178 | }; 179 | 180 | // Parses dummy variable for readability; 181 | export const parseString = 'parse-string'; 182 | export const parseVarArgs = 'parse-var-args'; 183 | 184 | /** 185 | * Does a dispatch of a log record. 186 | * 187 | * @param {Array.} fields Log record. 188 | * @private 189 | */ 190 | LogReader.prototype.dispatchLogRow_ = function(fields) { 191 | // Obtain the dispatch. 192 | var command = fields[0]; 193 | var dispatch = this.dispatchTable_[command]; 194 | if (dispatch === undefined) return; 195 | if (dispatch === null || this.skipDispatch(dispatch)) { 196 | return; 197 | } 198 | 199 | // Parse fields. 200 | var parsedFields = []; 201 | for (var i = 0; i < dispatch.parsers.length; ++i) { 202 | var parser = dispatch.parsers[i]; 203 | if (parser === parseString) { 204 | parsedFields.push(fields[1 + i]); 205 | } else if (typeof parser == 'function') { 206 | parsedFields.push(parser(fields[1 + i])); 207 | } else if (parser === parseVarArgs) { 208 | // var-args 209 | parsedFields.push(fields.slice(1 + i)); 210 | break; 211 | } else { 212 | throw new Error("Invalid log field parser: " + parser); 213 | } 214 | } 215 | 216 | // Run the processor. 217 | dispatch.processor.apply(this, parsedFields); 218 | }; 219 | 220 | 221 | /** 222 | * Processes log lines. 223 | * 224 | * @param {Array.} lines Log lines. 225 | * @private 226 | */ 227 | LogReader.prototype.processLog_ = function(lines) { 228 | for (var i = 0, n = lines.length; i < n; ++i) { 229 | this.processLogLine_(lines[i]); 230 | } 231 | } 232 | 233 | /** 234 | * Processes a single log line. 235 | * 236 | * @param {String} a log line 237 | * @private 238 | */ 239 | LogReader.prototype.processLogLine_ = function(line) { 240 | if (line.length > 0) { 241 | try { 242 | var fields = this.csvParser_.parseLine(line); 243 | this.dispatchLogRow_(fields); 244 | } catch (e) { 245 | this.printError('line ' + (this.lineNum_ + 1) + ': ' + (e.message || e) + '\n' + e.stack); 246 | } 247 | } 248 | this.lineNum_++; 249 | }; 250 | -------------------------------------------------------------------------------- /packages/v8-deopt-parser/test/constants.js: -------------------------------------------------------------------------------- 1 | import { repoRoot, repoFileURL } from "./helpers.js"; 2 | 3 | /** 4 | * @typedef {"adders" | "two-modules" | "html-inline" | "html-external"} Examples 5 | * @type {{ [key in Examples]: import('../').ICEntry }} */ 6 | export const expectedICSLogs = { 7 | adders: { 8 | type: "ics", 9 | id: "327", 10 | functionName: "addAny", 11 | file: repoRoot("examples/simple/adders.js"), 12 | line: 93, 13 | column: 27, 14 | severity: 3, 15 | updates: [ 16 | { 17 | type: "LoadIC", 18 | oldState: "premonomorphic", 19 | newState: "monomorphic", 20 | key: "x", 21 | map: "0x017b7663a951", 22 | optimizationState: "optimizable", 23 | severity: 1, 24 | modifier: "", 25 | slowReason: "", 26 | }, 27 | { 28 | type: "LoadIC", 29 | oldState: "monomorphic", 30 | newState: "polymorphic", 31 | key: "x", 32 | map: "0x017b76637b61", 33 | optimizationState: "optimizable", 34 | severity: 2, 35 | modifier: "", 36 | slowReason: "", 37 | }, 38 | { 39 | type: "LoadIC", 40 | oldState: "polymorphic", 41 | newState: "polymorphic", 42 | key: "x", 43 | map: "0x017b76637481", 44 | optimizationState: "optimizable", 45 | severity: 2, 46 | modifier: "", 47 | slowReason: "", 48 | }, 49 | { 50 | type: "LoadIC", 51 | oldState: "polymorphic", 52 | newState: "polymorphic", 53 | key: "x", 54 | map: "0x017b76636e41", 55 | optimizationState: "optimizable", 56 | severity: 2, 57 | modifier: "", 58 | slowReason: "", 59 | }, 60 | { 61 | type: "LoadIC", 62 | oldState: "polymorphic", 63 | newState: "megamorphic", 64 | key: "x", 65 | map: "0x017b76637021", 66 | optimizationState: "optimizable", 67 | severity: 3, 68 | modifier: "", 69 | slowReason: "", 70 | }, 71 | { 72 | type: "LoadIC", 73 | oldState: "megamorphic", 74 | newState: "megamorphic", 75 | key: "x", 76 | map: "0x017b76637251", 77 | optimizationState: "optimized", 78 | severity: 3, 79 | modifier: "", 80 | slowReason: "", 81 | }, 82 | { 83 | type: "LoadIC", 84 | oldState: "megamorphic", 85 | newState: "megamorphic", 86 | key: "x", 87 | map: "0x017b76636bc1", 88 | optimizationState: "optimized", 89 | severity: 3, 90 | modifier: "", 91 | slowReason: "", 92 | }, 93 | { 94 | type: "LoadIC", 95 | oldState: "megamorphic", 96 | newState: "megamorphic", 97 | key: "x", 98 | map: "0x017b76635d11", 99 | optimizationState: "optimized", 100 | severity: 3, 101 | modifier: "", 102 | slowReason: "", 103 | }, 104 | ], 105 | }, 106 | 107 | "two-modules": { 108 | type: "ics", 109 | id: "333", 110 | functionName: "addAny", 111 | file: repoRoot("examples/two-modules/adders.js"), 112 | line: 38, 113 | column: 27, 114 | severity: 3, 115 | updates: [ 116 | { 117 | type: "LoadIC", 118 | oldState: "premonomorphic", 119 | newState: "monomorphic", 120 | key: "x", 121 | map: "0x37cdf3b7a951", 122 | optimizationState: "optimizable", 123 | severity: 1, 124 | modifier: "", 125 | slowReason: "", 126 | }, 127 | { 128 | type: "LoadIC", 129 | oldState: "monomorphic", 130 | newState: "polymorphic", 131 | key: "x", 132 | map: "0x37cdf3b77b61", 133 | optimizationState: "optimizable", 134 | severity: 2, 135 | modifier: "", 136 | slowReason: "", 137 | }, 138 | { 139 | type: "LoadIC", 140 | oldState: "polymorphic", 141 | newState: "polymorphic", 142 | key: "x", 143 | map: "0x37cdf3b77481", 144 | optimizationState: "optimizable", 145 | severity: 2, 146 | modifier: "", 147 | slowReason: "", 148 | }, 149 | { 150 | type: "LoadIC", 151 | oldState: "polymorphic", 152 | newState: "polymorphic", 153 | key: "x", 154 | map: "0x37cdf3b76e41", 155 | optimizationState: "optimizable", 156 | severity: 2, 157 | modifier: "", 158 | slowReason: "", 159 | }, 160 | { 161 | type: "LoadIC", 162 | oldState: "polymorphic", 163 | newState: "megamorphic", 164 | key: "x", 165 | map: "0x37cdf3b77021", 166 | optimizationState: "optimizable", 167 | severity: 3, 168 | modifier: "", 169 | slowReason: "", 170 | }, 171 | { 172 | type: "LoadIC", 173 | oldState: "megamorphic", 174 | newState: "megamorphic", 175 | key: "x", 176 | map: "0x37cdf3b77251", 177 | optimizationState: "optimized", 178 | severity: 3, 179 | modifier: "", 180 | slowReason: "", 181 | }, 182 | { 183 | type: "LoadIC", 184 | oldState: "megamorphic", 185 | newState: "megamorphic", 186 | key: "x", 187 | map: "0x37cdf3b76bc1", 188 | optimizationState: "optimized", 189 | severity: 3, 190 | modifier: "", 191 | slowReason: "", 192 | }, 193 | { 194 | type: "LoadIC", 195 | oldState: "megamorphic", 196 | newState: "megamorphic", 197 | key: "x", 198 | map: "0x37cdf3b75d11", 199 | optimizationState: "optimized", 200 | severity: 3, 201 | modifier: "", 202 | slowReason: "", 203 | }, 204 | ], 205 | }, 206 | 207 | "html-inline": { 208 | type: "ics", 209 | id: "19", 210 | functionName: "addAny", 211 | file: repoFileURL("examples/html-inline/adders.html"), 212 | line: 98, 213 | column: 33, 214 | severity: 3, 215 | updates: [ 216 | { 217 | type: "LoadIC", 218 | oldState: "uninitialized", 219 | newState: "monomorphic", 220 | key: "x", 221 | map: "0x14cd08283fc9", 222 | optimizationState: "optimizable", 223 | severity: 1, 224 | modifier: "", 225 | slowReason: "", 226 | }, 227 | { 228 | type: "LoadIC", 229 | oldState: "monomorphic", 230 | newState: "polymorphic", 231 | key: "x", 232 | map: "0x14cd08284091", 233 | optimizationState: "optimizable", 234 | severity: 2, 235 | modifier: "", 236 | slowReason: "", 237 | }, 238 | { 239 | type: "LoadIC", 240 | oldState: "polymorphic", 241 | newState: "polymorphic", 242 | key: "x", 243 | map: "0x14cd08284181", 244 | optimizationState: "optimizable", 245 | severity: 2, 246 | modifier: "", 247 | slowReason: "", 248 | }, 249 | { 250 | type: "LoadIC", 251 | oldState: "polymorphic", 252 | newState: "polymorphic", 253 | key: "x", 254 | map: "0x14cd08284271", 255 | optimizationState: "optimizable", 256 | severity: 2, 257 | modifier: "", 258 | slowReason: "", 259 | }, 260 | { 261 | type: "LoadIC", 262 | oldState: "polymorphic", 263 | newState: "megamorphic", 264 | key: "x", 265 | map: "0x14cd08284361", 266 | optimizationState: "optimizable", 267 | severity: 3, 268 | modifier: "", 269 | slowReason: "", 270 | }, 271 | { 272 | type: "LoadIC", 273 | oldState: "megamorphic", 274 | newState: "megamorphic", 275 | key: "x", 276 | map: "0x14cd08284479", 277 | optimizationState: "optimized", 278 | severity: 3, 279 | modifier: "", 280 | slowReason: "", 281 | }, 282 | { 283 | type: "LoadIC", 284 | oldState: "megamorphic", 285 | newState: "megamorphic", 286 | key: "x", 287 | map: "0x14cd08284591", 288 | optimizationState: "optimized", 289 | severity: 3, 290 | modifier: "", 291 | slowReason: "", 292 | }, 293 | { 294 | type: "LoadIC", 295 | oldState: "megamorphic", 296 | newState: "megamorphic", 297 | key: "x", 298 | map: "0x14cd082846a9", 299 | optimizationState: "optimized", 300 | severity: 3, 301 | modifier: "", 302 | slowReason: "", 303 | }, 304 | ], 305 | }, 306 | 307 | "html-external": { 308 | type: "ics", 309 | id: "20", 310 | functionName: "addAny", 311 | file: repoFileURL("examples/html-external/adders.js"), 312 | line: 38, 313 | column: 27, 314 | severity: 3, 315 | updates: [ 316 | { 317 | type: "LoadIC", 318 | oldState: "uninitialized", 319 | newState: "monomorphic", 320 | key: "x", 321 | map: "0x420708283f51", 322 | optimizationState: "optimizable", 323 | severity: 1, 324 | modifier: "", 325 | slowReason: "", 326 | }, 327 | { 328 | type: "LoadIC", 329 | oldState: "monomorphic", 330 | newState: "polymorphic", 331 | key: "x", 332 | map: "0x420708284019", 333 | optimizationState: "optimizable", 334 | severity: 2, 335 | modifier: "", 336 | slowReason: "", 337 | }, 338 | { 339 | type: "LoadIC", 340 | oldState: "polymorphic", 341 | newState: "polymorphic", 342 | key: "x", 343 | map: "0x420708284109", 344 | optimizationState: "optimizable", 345 | severity: 2, 346 | modifier: "", 347 | slowReason: "", 348 | }, 349 | { 350 | type: "LoadIC", 351 | oldState: "polymorphic", 352 | newState: "polymorphic", 353 | key: "x", 354 | map: "0x4207082841f9", 355 | optimizationState: "optimizable", 356 | severity: 2, 357 | modifier: "", 358 | slowReason: "", 359 | }, 360 | { 361 | type: "LoadIC", 362 | oldState: "polymorphic", 363 | newState: "megamorphic", 364 | key: "x", 365 | map: "0x4207082842e9", 366 | optimizationState: "optimizable", 367 | severity: 3, 368 | modifier: "", 369 | slowReason: "", 370 | }, 371 | { 372 | type: "LoadIC", 373 | oldState: "megamorphic", 374 | newState: "megamorphic", 375 | key: "x", 376 | map: "0x420708284401", 377 | optimizationState: "optimized", 378 | severity: 3, 379 | modifier: "", 380 | slowReason: "", 381 | }, 382 | { 383 | type: "LoadIC", 384 | oldState: "megamorphic", 385 | newState: "megamorphic", 386 | key: "x", 387 | map: "0x420708284519", 388 | optimizationState: "optimized", 389 | severity: 3, 390 | modifier: "", 391 | slowReason: "", 392 | }, 393 | { 394 | type: "LoadIC", 395 | oldState: "megamorphic", 396 | newState: "megamorphic", 397 | key: "x", 398 | map: "0x420708284631", 399 | optimizationState: "optimized", 400 | severity: 3, 401 | modifier: "", 402 | slowReason: "", 403 | }, 404 | ], 405 | }, 406 | }; 407 | -------------------------------------------------------------------------------- /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-parser/test/helpers.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, pathToFileURL } from "url"; 2 | import * as path from "path"; 3 | import * as stream from "stream"; 4 | import { createReadStream, createWriteStream } from "fs"; 5 | import { readFile, writeFile, access } from "fs/promises"; 6 | import zlib from "zlib"; 7 | import escapeRegex from "escape-string-regexp"; 8 | import { parseV8Log, parseV8LogStream } from "../src/index.js"; 9 | 10 | // @ts-ignore 11 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 12 | export const pkgRoot = (...args) => path.join(__dirname, "..", ...args); 13 | export const repoRoot = (...args) => pkgRoot("..", "..", ...args); 14 | export const repoFileURL = (...args) => 15 | pathToFileURL(repoRoot(...args)).toString(); 16 | 17 | // Mapping of test paths in test logs to real paths 18 | const logPathReplacements = { 19 | ["adders.v8.log"]: [ 20 | [ 21 | "/tmp/v8-deopt-viewer/examples/simple/adders.js", 22 | repoRoot("examples/simple/adders.js"), 23 | ], 24 | ], 25 | ["adders.traceMaps.v8.log"]: [ 26 | [ 27 | "/tmp/v8-deopt-viewer/examples/simple/adders.js", 28 | repoRoot("examples/simple/adders.js"), 29 | ], 30 | ], 31 | ["two-modules.v8.log"]: [ 32 | [ 33 | "/tmp/v8-deopt-viewer/examples/two-modules/adders.js", 34 | repoRoot("examples/two-modules/adders.js"), 35 | ], 36 | [ 37 | "/tmp/v8-deopt-viewer/examples/two-modules/objects.js", 38 | repoRoot("examples/two-modules/objects.js"), 39 | ], 40 | ], 41 | ["html-inline.v8.log"]: [ 42 | [ 43 | "file:///tmp/v8-deopt-viewer/examples/html-inline/adders.html", 44 | pathToFileURL(repoRoot("examples/html-inline/adders.html")).toString(), 45 | ], 46 | ], 47 | ["html-inline.traceMaps.v8.log"]: [ 48 | [ 49 | "file:///tmp/v8-deopt-viewer/examples/html-inline/adders.html", 50 | pathToFileURL(repoRoot("examples/html-inline/adders.html")).toString(), 51 | ], 52 | ], 53 | ["html-external.v8.log"]: [ 54 | [ 55 | "file:///tmp/v8-deopt-viewer/examples/html-external/adders.js", 56 | pathToFileURL(repoRoot("examples/html-external/adders.js")).toString(), 57 | ], 58 | [ 59 | "file:///tmp/v8-deopt-viewer/examples/html-external/objects.js", 60 | pathToFileURL(repoRoot("examples/html-external/objects.js")).toString(), 61 | ], 62 | ], 63 | ["html-external.traceMaps.v8.log"]: [ 64 | [ 65 | "file:///tmp/v8-deopt-viewer/examples/html-external/adders.js", 66 | pathToFileURL(repoRoot("examples/html-external/adders.js")).toString(), 67 | ], 68 | [ 69 | "file:///tmp/v8-deopt-viewer/examples/html-external/objects.js", 70 | pathToFileURL(repoRoot("examples/html-external/objects.js")).toString(), 71 | ], 72 | ], 73 | }; 74 | 75 | /** 76 | * Replace the fake paths in the example test log files with paths to real files 77 | * to test handling paths on the OS the tests are running on. Our cloud tests 78 | * run these tests on Linux and Window 79 | * @param {string} logFilename 80 | * @param {string} logPath 81 | * @returns {Promise} 82 | */ 83 | export async function readLogFile(logFilename, logPath) { 84 | let error; 85 | try { 86 | await access(logPath); 87 | } catch (e) { 88 | error = e; 89 | } 90 | 91 | if (error) { 92 | let brotliPath = logPath + ".br"; 93 | try { 94 | error = null; 95 | await access(brotliPath); 96 | } catch (e) { 97 | error = e; 98 | } 99 | 100 | if (error) { 101 | throw new Error(`Could not access log file: ${logPath}[.br]: ` + error); 102 | } 103 | 104 | await decompress(brotliPath); 105 | } 106 | 107 | let contents = await readFile(logPath, "utf8"); 108 | 109 | // Windows + Git shenanigans - make sure log files end in only '\n' 110 | // as required by v8 tooling 111 | contents = contents.replace(/\r\n/g, "\n"); 112 | 113 | const replacements = logPathReplacements[logFilename]; 114 | if (replacements) { 115 | for (const [template, realPath] of replacements) { 116 | contents = contents.replace( 117 | new RegExp(escapeRegex(template), "g"), 118 | // Windows paths need to be double escaped in the logs 119 | realPath.replace(/\\/g, "\\\\") 120 | ); 121 | } 122 | } 123 | 124 | return contents; 125 | } 126 | 127 | /** 128 | * @param {import('tape').Test} t 129 | * @param {string} logFileName 130 | * @param {import('../').Options} [options] 131 | */ 132 | export async function runParserNonStream(t, logFileName, options) { 133 | const logPath = pkgRoot("test", "logs", logFileName); 134 | 135 | const logContents = await readLogFile(logFileName, logPath); 136 | 137 | const origConsoleError = console.error; 138 | const errorArgs = []; 139 | console.error = function (...args) { 140 | origConsoleError.apply(console, args); 141 | errorArgs.push(args); 142 | }; 143 | 144 | let result; 145 | try { 146 | result = await parseV8Log(logContents, options); 147 | } finally { 148 | console.error = origConsoleError; 149 | } 150 | 151 | t.equal(errorArgs.length, 0, "No console.error calls"); 152 | 153 | return result; 154 | } 155 | 156 | /** 157 | * @param {import('tape').Test} t 158 | * @param {string} logFileName 159 | * @param {import('../').Options} [options] 160 | */ 161 | export async function runParserStream(t, logFileName, options) { 162 | const logPath = pkgRoot("test", "logs", logFileName); 163 | 164 | const logContents = await readLogFile(logFileName, logPath); 165 | const logStream = stream.Readable.from(logContents); 166 | 167 | const origConsoleError = console.error; 168 | const errorArgs = []; 169 | console.error = function (...args) { 170 | origConsoleError.apply(console, args); 171 | errorArgs.push(args); 172 | }; 173 | 174 | 175 | let result; 176 | try { 177 | result = await parseV8LogStream(logStream, options); 178 | } finally { 179 | console.error = origConsoleError; 180 | } 181 | 182 | t.equal(errorArgs.length, 0, "No console.error calls"); 183 | 184 | return result; 185 | } 186 | 187 | /** Redact some properties from the result to keep snapshots clean */ 188 | function redactResult(key, value) { 189 | switch (key) { 190 | case "id": 191 | case "edge": 192 | case "children": 193 | return value ? "" : undefined; 194 | default: 195 | return value; 196 | } 197 | } 198 | 199 | export async function writeSnapshot(logFileName, result) { 200 | // Undo replacements when writing snapshots so they are consistent 201 | const replacements = logPathReplacements[logFileName]; 202 | let contents = JSON.stringify(result, redactResult, 2); 203 | 204 | if (replacements) { 205 | for (const [snapshotPath, template] of replacements) { 206 | contents = contents.replace( 207 | new RegExp(escapeRegex(template.replace(/\\/g, "\\\\")), "g"), 208 | snapshotPath 209 | ); 210 | } 211 | } 212 | 213 | const outFileName = logFileName.replace(".v8.log", ".json"); 214 | const outPath = path.join(__dirname, "snapshots", outFileName); 215 | await writeFile(outPath, contents, "utf8"); 216 | } 217 | 218 | /** 219 | * @param {import('tape').Test} t 220 | * @param {string} message 221 | * @param {Array} entries 222 | * @param {import('../').Entry} expectedEntry 223 | */ 224 | export function validateEntry(t, message, entries, expectedEntry) { 225 | const { functionName, file, line, column } = expectedEntry; 226 | const matches = entries.filter((entry) => { 227 | return ( 228 | entry.functionName === functionName && 229 | entry.file === file && 230 | entry.line === line && 231 | entry.column === column 232 | ); 233 | }); 234 | 235 | if (matches.length !== 1) { 236 | throw new Error( 237 | `Expected to only find one match for "${functionName} ${file}:${line}:${column}". Found ${matches.length}.` 238 | ); 239 | } 240 | 241 | t.deepEqual(matches[0], expectedEntry, message); 242 | } 243 | 244 | export function decompress(inputPath) { 245 | return new Promise((resolve, reject) => { 246 | const stream = createReadStream(inputPath) 247 | .pipe(zlib.createBrotliDecompress()) 248 | .pipe(createWriteStream(inputPath.replace(/.br$/, ""))); 249 | 250 | stream 251 | .on("end", resolve) 252 | .on("close", resolve) 253 | .on("finish", resolve) 254 | .on("error", reject); 255 | }); 256 | } 257 | -------------------------------------------------------------------------------- /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-parser/test/logs/adders.node16.v8.log.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewiggins/v8-deopt-viewer/192f4d4d1df4d56b215e23ec9a1aec2c99e227af/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/192f4d4d1df4d56b215e23ec9a1aec2c99e227af/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/192f4d4d1df4d56b215e23ec9a1aec2c99e227af/packages/v8-deopt-parser/test/logs/adders.traceMaps.v8.log.br -------------------------------------------------------------------------------- /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-parser/test/logs/v8-deopt-parser.v8.log.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewiggins/v8-deopt-viewer/192f4d4d1df4d56b215e23ec9a1aec2c99e227af/packages/v8-deopt-parser/test/logs/v8-deopt-parser.v8.log.br -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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-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/traceMapsHelpers.js: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { writeFile } from "fs/promises"; 4 | 5 | // @ts-ignore 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | const numberSorter = (a, b) => Number(a) - Number(b); 8 | 9 | /** 10 | * @param {import('..').V8DeoptInfo} deoptInfo 11 | */ 12 | export function validateMapData(t, deoptInfo) { 13 | const mapData = deoptInfo.maps; 14 | 15 | const icMapIds = Array.from(getMapIdsFromICs(deoptInfo.ics)); 16 | const mapEntryIds = Object.keys(mapData.nodes); 17 | const mapIdsFromEdges = Array.from(getAllMapIdsFromEdges(mapData)); 18 | 19 | const edgeEntryIds = Object.keys(mapData.edges); 20 | const edgeIdsFromMaps = Array.from(getAllEdgeIdsFromMaps(mapData)); 21 | 22 | // Ensure all maps referenced in ics exist in map.entries 23 | let missingMaps = icMapIds.filter((id) => !mapEntryIds.includes(id)); 24 | if (missingMaps.length > 0) { 25 | console.log( 26 | "IC Map IDs with no map entry:", 27 | missingMaps.sort(numberSorter) 28 | ); 29 | } 30 | t.equal(missingMaps.length, 0, "All IC map IDs have map entries"); 31 | 32 | // Ensure all maps references in maps.edges exists in maps.entries 33 | missingMaps = mapIdsFromEdges.filter((id) => !mapEntryIds.includes(id)); 34 | if (missingMaps.length > 0) { 35 | console.log( 36 | "Map IDs referenced from an edge without an corresponding map entry:", 37 | missingMaps.sort(numberSorter) 38 | ); 39 | } 40 | t.equal(missingMaps.length, 0, "All edge from/to map IDs have map entries"); 41 | 42 | // Ensure all edges references in maps.entries exist in maps.edges 43 | let missingEdges = edgeIdsFromMaps.filter((id) => !edgeEntryIds.includes(id)); 44 | if (missingEdges.length > 0) { 45 | console.log( 46 | "Edge IDs referenced from a Map without an edge entry:", 47 | missingEdges.sort(numberSorter) 48 | ); 49 | } 50 | t.equal(missingEdges.length, 0, "All map edge references have edge entries"); 51 | 52 | // Ensure there are no superfluous maps or edges. Walk the entire map graph 53 | // and ensure no missing nodes or unused nodes 54 | 55 | /** @type {Set} */ 56 | const allMapIds = new Set([...icMapIds, ...mapIdsFromEdges, ...mapEntryIds]); 57 | 58 | /** @type {Set} */ 59 | const allEdgeIds = new Set([...edgeIdsFromMaps, ...edgeEntryIds]); 60 | 61 | const rootMaps = new Set( 62 | icMapIds 63 | .map((mapId) => getRootMap(mapData, mapData.nodes[mapId])) 64 | .filter(Boolean) 65 | ); 66 | 67 | for (const rootMap of rootMaps) { 68 | visitAllMaps(mapData, rootMap, (map) => { 69 | allMapIds.delete(map.id); 70 | allEdgeIds.delete(map.edge); 71 | if (map.children) { 72 | map.children.forEach((edgeId) => allEdgeIds.delete(edgeId)); 73 | } 74 | }); 75 | } 76 | 77 | t.equal(allMapIds.size, 0, "All maps are connected to a root"); 78 | t.equal(allEdgeIds.size, 0, "All edges are connected to a root"); 79 | } 80 | 81 | /** 82 | * @param {string} logFileName 83 | * @param {import('..').V8DeoptInfo} deoptInfo 84 | */ 85 | export async function writeMapSnapshot(logFileName, deoptInfo) { 86 | const mapData = deoptInfo.maps; 87 | const icMapIds = Array.from(getMapIdsFromICs(deoptInfo.ics)); 88 | const rootMaps = new Set( 89 | icMapIds 90 | .map((mapId) => getRootMap(mapData, mapData.nodes[mapId])) 91 | .filter(Boolean) 92 | ); 93 | 94 | let snapshot = ""; 95 | let totalMapCount = 0; 96 | let totalEdgeCount = 0; 97 | 98 | for (let rootMap of rootMaps) { 99 | const { tree, mapCount, edgeCount } = generateMapTree(mapData, rootMap); 100 | 101 | snapshot += tree + "\n"; 102 | totalMapCount += mapCount; 103 | totalEdgeCount += edgeCount; 104 | } 105 | 106 | snapshot += "\n\n"; 107 | snapshot += `Total Map Count : ${totalMapCount}\n`; 108 | snapshot += `Total Edge Count: ${totalEdgeCount}\n`; 109 | snapshot += "\n"; 110 | 111 | const outFileName = logFileName.replace(".v8.log", ".mapTree.txt"); 112 | const outPath = path.join(__dirname, "snapshots", outFileName); 113 | await writeFile(outPath, snapshot, "utf8"); 114 | } 115 | 116 | /** 117 | * @param {Iterable} ics 118 | * @returns {Set} 119 | */ 120 | function getMapIdsFromICs(ics) { 121 | /** @type {Set} */ 122 | const mapIds = new Set(); 123 | for (const entry of ics) { 124 | for (const update of entry.updates) { 125 | mapIds.add(update.map); 126 | } 127 | } 128 | 129 | return mapIds; 130 | } 131 | 132 | /** 133 | * @param {import('../src').MapData} data 134 | * @param {import('../src').MapEntry} map 135 | * @returns {import('../src').MapEntry} 136 | */ 137 | function getRootMap(data, map) { 138 | if (!map) { 139 | return null; 140 | } 141 | 142 | let parentMapId = map.edge ? data.edges[map.edge]?.from : null; 143 | while (parentMapId && parentMapId in data.nodes) { 144 | map = data.nodes[parentMapId]; 145 | parentMapId = map.edge ? data.edges[map.edge]?.from : null; 146 | } 147 | 148 | return map; 149 | } 150 | 151 | /** 152 | * @param {import('../src').MapData} mapData 153 | * @returns {Iterable} 154 | */ 155 | function getAllEdgeIdsFromMaps(mapData) { 156 | /** @type {Set} */ 157 | const edgeIds = new Set(); 158 | for (let mapId in mapData.nodes) { 159 | const map = mapData.nodes[mapId]; 160 | if (map.edge) { 161 | edgeIds.add(map.edge); 162 | } 163 | 164 | if (map.children) { 165 | map.children.forEach((child) => edgeIds.add(child)); 166 | } 167 | } 168 | 169 | return edgeIds; 170 | } 171 | 172 | /** 173 | * @param {import('../src').MapData} mapData 174 | * @returns {Iterable} 175 | */ 176 | function getAllMapIdsFromEdges(mapData) { 177 | /** @type {Set} */ 178 | const mapIds = new Set(); 179 | for (let edgeId in mapData.edges) { 180 | const edge = mapData.edges[edgeId]; 181 | if (edge.from != null) { 182 | mapIds.add(edge.from); 183 | } 184 | 185 | if (edge.to != null) { 186 | mapIds.add(edge.to); 187 | } 188 | } 189 | 190 | return mapIds; 191 | } 192 | 193 | const CROSS = " ├─"; 194 | const CORNER = " └─"; 195 | const VERTICAL = " │ "; 196 | const SPACE = " "; 197 | 198 | const prepareNewLine = (indent, isLast) => 199 | isLast ? indent + CORNER : indent + CROSS; 200 | 201 | const increaseIndent = (indent, isLast) => 202 | isLast ? indent + SPACE : indent + VERTICAL; 203 | 204 | /** 205 | * @typedef {{ tree: string; mapCount: number; edgeCount: number; }} Output 206 | * @param {import('../src').MapData} data 207 | * @param {import('../src').MapEntry} map 208 | * @param {string} [indent] 209 | * @param {boolean} [isLast] 210 | * @returns {Output} 211 | */ 212 | function generateMapTree( 213 | data, 214 | map, 215 | output = { tree: "", mapCount: 0, edgeCount: 0 }, 216 | indent = "", 217 | isLast = true 218 | ) { 219 | output.mapCount += 1; 220 | 221 | let line = prepareNewLine(indent, isLast); 222 | indent = increaseIndent(indent, isLast); 223 | 224 | if (map.edge) { 225 | output.edgeCount += 1; 226 | const edge = data.edges[map.edge]; 227 | line += edgeToString(edge) + `\t[${map.id}]`; 228 | } else { 229 | line += map.id; 230 | } 231 | // console.log(line); 232 | output.tree += line + "\n"; 233 | 234 | if (!Array.isArray(map.children)) { 235 | return output; 236 | } 237 | 238 | const lastChildEdgeId = map.children[map.children.length - 1]; 239 | for (const childEdgeId of map.children) { 240 | const isLastChild = childEdgeId == lastChildEdgeId; 241 | 242 | const childEdge = data.edges[childEdgeId]; 243 | if (childEdge == null) { 244 | output.tree += 245 | prepareNewLine(indent, isLastChild) + `MISSING EDGE: ${childEdgeId}\n`; 246 | continue; 247 | } 248 | 249 | const childMap = data.nodes[childEdge.to]; 250 | if (childMap == null) { 251 | output.tree += 252 | prepareNewLine(indent, isLastChild) + `MISSING MAP: ${childEdge.to}\n`; 253 | continue; 254 | } 255 | 256 | generateMapTree(data, childMap, output, indent, isLastChild); 257 | } 258 | 259 | return output; 260 | } 261 | 262 | /** 263 | * @param {import('../').MapEdge} edge 264 | */ 265 | function getEdgeSymbol(edge) { 266 | switch (edge.subtype) { 267 | case "Transition": 268 | return "+"; 269 | case "Normalize": // FastToSlow 270 | return "⊡"; 271 | case "SlowToFast": 272 | return "⊛"; 273 | case "ReplaceDescriptors": 274 | return edge.name ? "+" : "∥"; 275 | default: 276 | return ""; 277 | } 278 | } 279 | 280 | /** 281 | * @param {import('../').MapEdge} edge 282 | */ 283 | function edgeToString(edge) { 284 | let s = getEdgeSymbol(edge); 285 | switch (edge.subtype) { 286 | case "Transition": 287 | return s + edge.name; 288 | case "SlowToFast": 289 | return s + edge.reason; 290 | case "CopyAsPrototype": 291 | return s + "Copy as Prototype"; 292 | case "OptimizeAsPrototype": 293 | return s + "Optimize as Prototype"; 294 | default: 295 | if (edge.subtype == "ReplaceDescriptors" && edge.name) { 296 | return edge.subtype + " " + getEdgeSymbol(edge) + edge.name; 297 | } else { 298 | return `${edge.subtype} ${edge?.reason ?? ""} ${edge?.name ?? ""}`; 299 | } 300 | } 301 | } 302 | 303 | /** 304 | * @param {import('..').MapData} mapData 305 | * @param {import('..').MapEntry} map 306 | * @param {(map: import('..').MapEntry) => void} visitor 307 | */ 308 | function visitAllMaps(mapData, map, visitor) { 309 | visitor(map); 310 | if (map.children) { 311 | for (const edgeId of map.children) { 312 | const edge = mapData.edges[edgeId]; 313 | const nextMap = mapData.nodes[edge.to]; 314 | visitAllMaps(mapData, nextMap, visitor); 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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-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-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-webapp/.gitignore: -------------------------------------------------------------------------------- 1 | stats.html 2 | -------------------------------------------------------------------------------- /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-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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/_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/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/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-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 |