├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── README_Zh.md ├── example ├── app.js ├── bootstrap.js ├── config.ts ├── f.yml ├── package.json ├── pkg │ ├── package.json │ ├── v8_profiler_rs.d.ts │ ├── v8_profiler_rs.js │ ├── v8_profiler_rs_bg.wasm │ └── v8_profiler_rs_bg.wasm.d.ts ├── public │ ├── close.svg │ ├── file.txt │ ├── logo.jpeg │ └── worker.js ├── src │ ├── config │ │ └── config.default.ts │ ├── configuration.ts │ └── controller │ │ └── index.ts ├── tailwind.config.js ├── tsconfig.json └── web │ ├── @types │ └── global │ │ └── index.d.ts │ ├── analyze.ts │ ├── common.less │ ├── components │ └── layout │ │ ├── App.vue │ │ ├── index.less │ │ └── index.vue │ ├── hooks │ ├── config.ts │ └── index.ts │ ├── pages │ ├── index │ │ ├── index.less │ │ ├── node │ │ │ └── render$node.vue │ │ └── render.vue │ └── report │ │ └── render.vue │ ├── store │ └── index.ts │ ├── tsconfig.json │ ├── type.ts │ └── utils.ts ├── images ├── 5c286fe4dc2b5443.png ├── 6c52bdda69534e42bc51b57042242650_noop.png ├── 77d1693d497f482b8a61a7c2aaa8bce9_noop.png ├── v8-1.png ├── v8-10.png ├── v8-11.png ├── v8-12.png ├── v8-2.png ├── v8-3.png ├── v8-4.png ├── v8-5.png ├── v8-6.png ├── v8-7.png ├── v8-8.png └── v8-9.png └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.** linguist-language=Rust 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the dev branch 5 | on: 6 | push: 7 | branches: [dev, main, feat/**, ci/test, test/**] 8 | pull_request: 9 | branches: [dev, main] 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | deploy: 14 | if: ${{ contains(github.actor, 'zhangyuang') && github.ref_name == 'main'}} 15 | 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 18 23 | - run: | 24 | if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$"; 25 | then 26 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 27 | npm publish && cd example && yarn && npm run deploy 28 | elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"; 29 | then 30 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 31 | npm publish && cd example && yarn && npm run deploy 32 | else 33 | echo "Not a release, skipping publish" 34 | fi 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | SERVERLESS_DEPLOY_AK: ${{ secrets.SERVERLESS_DEPLOY_AK }} 39 | SERVERLESS_DEPLOY_ENDPOINT: ${{ secrets.SERVERLESS_DEPLOY_ENDPOINT }} 40 | SERVERLESS_DEPLOY_ID: ${{ secrets.SERVERLESS_DEPLOY_ID }} 41 | SERVERLESS_DEPLOY_SECRET: ${{ secrets.SERVERLESS_DEPLOY_SECRET }} 42 | SERVERLESS_DEPLOY_TIMEOUT: ${{ secrets.SERVERLESS_DEPLOY_TIMEOUT }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | # End of https://www.toptal.com/developers/gitignore/api/node 115 | 116 | 117 | #Added by cargo 118 | 119 | /target 120 | Cargo.lock 121 | 122 | *.node 123 | *.cpuprofile 124 | snapshot.json 125 | .DS_Store 126 | !example/**/*.json 127 | example/index.js 128 | example/index.d.ts 129 | .eslintcache 130 | run 131 | **/yarn.lock 132 | 133 | **.heapsnapshot 134 | 135 | !**/memoryleak-demo/**/*.heapsnapshot 136 | 137 | 138 | **/test.js 139 | 140 | .temp 141 | .temp2 142 | .s 143 | help 144 | HeapSnapshotLoader.js 145 | js-flags.json 146 | flags.json 147 | foo.js 148 | example/build 149 | **/yarn.lock 150 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 N-API for Rust 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 | [English](./README.md) | [简体中文](./README_Zh.md) 2 | 3 | # Introduction 4 | 5 | [v8-profiler-rs](https://github.com/zhangyuang/v8-profiler-rs) is a project developed in `Rust` for intelligent online analysis of `V8 heapsnapshot` files. It helps developers using V8-based applications like `Node.js/Chrome/Deno/Electron` to better understand program memory structure and assist in locating memory leaks. 6 | 7 | ## How to start 8 | 9 | Recommend using [Online Demo](https://v8.ssr-fc.com/), it's more convenient. Or you can start it in local. 10 | 11 | ```bash 12 | $ git clone https://github.com/zhangyuang/v8-profiler-rs.git 13 | $ cd v8-profiler-rs/example 14 | $ yarn && yarn start 15 | ``` 16 | 17 | ## Online Demo 18 | 19 | We have deployed an [online website](https://v8.ssr-fc.com/) where you can upload and analyze V8 memory snapshots in real-time. We recommend using `Safari` or `Firefox` browsers, as `wasm` execution performance is significantly better than in `Chrome` . 20 | 21 | ![image](https://res.wx.qq.com/shop/public/2025-02-12/e56b4e3d-a8d1-4638-b7b7-f92412e54a8f.png) 22 | 23 | ## Default example 24 | 25 | View default example by clicking the `Example` button on the top left corner. 26 | 27 | The code of default example is as follows: 28 | 29 | ```js 30 | const express = require('express'); 31 | const app = express(); 32 | const fs = require('fs') 33 | // the memory leak code 34 | let theThing = null; 35 | let replaceThing = function() { 36 | let leak = theThing; 37 | let unused = function() { 38 | if (leak) 39 | console.log("hi") 40 | }; 41 | 42 | theThing = { 43 | bigNumber: 1, 44 | bigArr: [], 45 | longStr: new Array(1000000), 46 | someMethod: function() { 47 | console.log('a'); 48 | } 49 | }; 50 | }; 51 | let index = 0 52 | app.get('/leak', function closureLeak(req, res, next) { 53 | replaceThing(); 54 | index++ 55 | if (index === 1) { 56 | const stream = require('v8').getHeapSnapshot() 57 | stream.pipe(fs.createWriteStream('small-closure.heapsnapshot')) 58 | } 59 | if (index === 40) { 60 | const stream = require('v8').getHeapSnapshot() 61 | stream.pipe(fs.createWriteStream('medium-closure.heapsnapshot')) 62 | } 63 | if (index === 50) { 64 | const stream = require('v8').getHeapSnapshot() 65 | stream.pipe(fs.createWriteStream('big-closure.heapsnapshot')) 66 | } 67 | res.send('Hello Node'); 68 | }); 69 | 70 | app.listen(3001); 71 | ``` 72 | 73 | ## Analyze Report 74 | 75 | There are two types of reports: single report and compare report. 76 | 77 | ### Single Report 78 | 79 | ```yml 80 | Analyze Report with single 81 | Only show nodes with more than 20 occurrences 82 | Memory retained by constructor type 83 | closure: 156409.05 MB 84 | 85 | Object: 156408.32 MB 86 | 87 | system / Context: 154881.33 MB 88 | 89 | array: 4579.70 MB 90 | 91 | synthetic: 1533.60 MB 92 | 93 | Array: 1526.16 MB 94 | 95 | WeakMap: 1526.01 MB 96 | 97 | (compiled code): 2.96 MB 98 | 99 | string: 2.10 MB 100 | 101 | There are some nodes that are duplicated too many times 102 | someMethod @70761: 202(memoryleak - demo / closure / index.js) 103 | HttpError @95957: 44(node_modules / http - errors / index.js) 104 | 105 | parse @89241: 33(node_modules / ms / index.js) 106 | 107 | ClientError @50469: 31(node_modules / http - errors / index.js) 108 | 109 | indexOf @49211: 27(node_modules / object - inspect / index.js) 110 | 111 | send @126827: 23(node_modules / send / index.js) 112 | 113 | resolve @98543: 23(node_modules / express / lib / view.js) 114 | ``` 115 | 116 | ### Compare Report 117 | 118 | ```yml 119 | Analyze Report with compare 120 | Only show nodes with more than 20 occurrences 121 | Additional nodes by constructor type 122 | (compiled code): 242 123 | 124 | ArrayBuffer: 127 125 | 126 | system / JSArrayBufferData: 127 127 | 128 | Buffer: 126 129 | 130 | Node / std::basic_string: 78 131 | 132 | closure: 69 133 | 134 | Array: 46 135 | 136 | array: 44 137 | 138 | system / Context: 34 139 | 140 | Object: 29 141 | 142 | Bigger nodes by constructor type 143 | (compiled code): 48 144 | closure: 25 145 | 146 | Bigger nodes by increased size 147 | @1: +83.54 MB 148 | Object @6613: +76.30 MB 149 | 150 | Array @154471: +76.30 MB 151 | 152 | Array @154499: +76.30 MB 153 | 154 | app @6603: +76.30 MB(node_modules / .pnpm / express @4 .18 .2 / node_modules / express / lib / express.js) 155 | 156 | router @6533: +76.30 MB(node_modules / .pnpm / express @4 .18 .2 / node_modules / express / lib / router / index.js) 157 | 158 | closureLeak @6481: +76.30 MB(memoryleak - demo / closure / index.js) 159 | 160 | Route @6511: +76.30 MB 161 | 162 | Layer @6517: +76.30 MB 163 | 164 | Layer @6497: +76.30 MB 165 | 166 | Server @6609: +76.30 MB 167 | 168 | system / Context @6483: +76.30 MB 169 | ``` 170 | 171 | ## Implemented Features 172 | 173 | 🚀 indicates implemented features. This application is continuously being updated, and updates will be synchronized to the README.md. Please stay tuned. If this application helps you, please give it a Star ✨ 174 | 175 | | Milestone | Status | 176 | |-----------|--------| 177 | | Parse V8 snapshot to generate complete node information | 🚀 | 178 | | View node source location and constructor | 🚀 | 179 | | Generate analysis reports | 🚀 | 180 | | Frontend visualization support | 🚀 | 181 | | Filter nodes by ID and name | 🚀 | 182 | | Filter number of nodes and view detailed node references | 🚀 | 183 | | Filter reference depth and number of references | 🚀 | 184 | | Compare two snapshot files | 🚀 | 185 | | Support two comparison types: new nodes/increased GC size | 🚀 | 186 | | Automatically filter non-business nodes based on node count | 🚀 | 187 | | Support uploading local serialized JSON files | 🚀 | 188 | | Implement `Wasm + WebWorker` parsing to avoid site unresponsiveness | 🚀 | 189 | | Optimize parsing performance and reduce memory usage | 🚀 | 190 | 191 | ## Why Rust 192 | 193 | Parsing V8 memory snapshots involves a lot of computational operations. CPU-intensive scenarios are inherently not JavaScript's strong suit. 194 | 195 | After reading Chrome's official memory analysis tool source code, I found it uses many tricks to ensure performance, making the code highly complex and difficult to maintain. 196 | 197 | Rust is perfect for this scenario, providing excellent performance while maintaining code readability. Rust's superior multi-threading capabilities are exactly what we need, as parsing computation logic is ideal for multi-threaded optimization. We will continue optimizing computational performance in future versions. 198 | 199 | This application's approach can be applied to any programming language with GC. We will strive to support memory analysis for more programming languages in the future. 200 | 201 | Finally, Rust's official WebAssembly support is excellent, allowing us to easily compile Rust code to WebAssembly for browser use. 202 | 203 | ## Handling Parse Timeout Issues 204 | 205 | For very large files, you may encounter Wasm memory overflow or long parse times. If this occurs, try using `Safari` or `Firefox` browsers. 206 | 207 | ## Contact 208 | 209 | The prohect has been updating, if you have any questions or suggestions, please submit an [issue](https://github.com/zhangyuang/v8-profiler-rs/issues) 210 | 211 |
212 | 213 | 214 | ### How to sponsor me 215 | 216 | There are two ways to sponsor me both Alipay and WeChat 217 | 218 | Eth address: 0x87a2575a5d4dbD5f965e3e3a3d20641BC9a5d192 219 | 220 |
221 | 222 | 223 |
224 | 225 | ## How to use 226 | 227 | Open the [online demo](https://v8.ssr-fc.com/) and upload the `heapsnapshot` file. You can upload one or two files for comparison. 228 | 229 | There will render heapsnapshot nodes in web page. 230 | 231 | The node fields are as follows: 232 | 233 | ```js 234 | { 235 | "node_type": string; // node type 236 | "name": string; // node name 237 | "id": number; // node id 238 | "size": number; // node self size 239 | "edge_count": number; // node edge count 240 | "retained_size": number; // node retained size, the free size of the node after GC 241 | "pt": number; // the ratio of self size / retained size 242 | "edges": { 243 | "edge_type": string; // edge type 244 | "to_node": number; // the id of the node that the edge points to 245 | "name_or_index": string; // the name or index(for array) of the edge 246 | } []; 247 | "source": string; // the source file of the node 248 | "constructor": string; // the constructor of the node 249 | "percent": string; // the retained size ratio of the node 250 | } 251 | ``` 252 | -------------------------------------------------------------------------------- /README_Zh.md: -------------------------------------------------------------------------------- 1 | # 项目介绍 2 | 3 | [v8-profiler-rs](https://github.com/zhangyuang/v8-profiler-rs) 是一个基于 `Rust` 开发的智能在线分析 `V8 堆快照` 文件的项目。它可以帮助使用 `Node.js/Chrome/Deno/Electron` 等 V8 应用的开发者更好地理解程序内存结构,辅助定位内存泄漏问题。 4 | 5 | ## 在线演示 6 | 7 | 我们部署了[在线网站](https://v8.ssr-fc.com/),您可以实时上传并分析 V8 内存快照。推荐使用 `Safari` 或 `Firefox` 浏览器,其 `wasm` 执行性能显著优于 `Chrome` 。 8 | 9 | ## 已实现功能 10 | 11 | 🚀 表示已实现功能。本应用持续更新中,更新将同步至 README.md,敬请关注。如果本应用对您有帮助,请给它一个 Star ✨ 12 | 13 | | 功能模块 | 状态 | 14 | |-----------|--------| 15 | | 解析 V8 快照生成完整节点信息 | 🚀 | 16 | | 查看节点源码位置及构造函数 | 🚀 | 17 | | 生成分析报告 | 🚀 | 18 | | 前端可视化支持 | 🚀 | 19 | | 按 ID 和名称过滤节点 | 🚀 | 20 | | 过滤节点数量并查看详细节点引用 | 🚀 | 21 | | 过滤引用深度和引用次数 | 🚀 | 22 | | 对比两份快照文件 | 🚀 | 23 | | 支持两种对比类型:新增节点/GC 体积增长 | 🚀 | 24 | | 根据节点数量自动过滤非业务节点 | 🚀 | 25 | | 支持上传本地序列化 JSON 文件 | 🚀 | 26 | | 实现 `Wasm + WebWorker` 解析避免站点无响应 | 🚀 | 27 | | 优化解析性能并降低内存占用 | 🚀 | 28 | 29 | ## 为什么选择 Rust 30 | 31 | 解析 V8 内存快照涉及大量计算操作。CPU 密集型场景本就不是 JavaScript 的强项。 32 | 33 | 阅读 Chrome 官方内存分析工具源码后发现,其使用了许多技巧来保证性能,导致代码复杂度极高且难以维护。 34 | 35 | Rust 完美契合这个场景,在提供卓越性能的同时保持代码可读性。Rust 优异的多线程能力正是我们需要的,解析计算逻辑非常适合多线程优化。我们将在后续版本持续优化计算性能。 36 | 37 | 本应用的实现思路可应用于任何带 GC 的编程语言。未来我们将努力支持更多编程语言的内存分析。 38 | 39 | 最后,Rust 官方的 WebAssembly 支持非常完善,使我们能够轻松将 Rust 代码编译为 WebAssembly 供浏览器使用。 40 | 41 | ## 处理解析超时问题 42 | 43 | 对于非常大的文件,可能会遇到 Wasm 内存溢出或解析时间过长的情况。如果出现此问题,请尝试使用 `Safari` 或 `Firefox` 浏览器。 44 | 45 | ## 联系我们 46 | 47 | 项目持续更新中,如有任何问题或建议,请提交 [issue](https://github.com/zhangyuang/v8-profiler-rs/issues) 48 | 49 |
50 | 51 | 52 | ## 赞助支持 53 | 54 | 如果您觉得本项目对您有帮助,请赞助作者一杯咖啡,您的支持是我最大的动力! 55 | 56 | 57 | 58 |
59 | 60 | ## 如何使用 61 | 62 | 打开[在线演示](https://v8.ssr-fc.com/)并上传 `heapsnapshot` 文件。您可以上传一个文件进行解析,或上传两个文件进行对比分析。 63 | 64 | 页面将渲染堆快照节点,节点字段说明如下: 65 | 66 | ```js 67 | { 68 | 69 | "node_type": string; // 节点类型 70 | "name": string; // 节点名称 71 | "id": number; // 节点ID 72 | "size": number; // 节点自身大小 73 | "edge_count": number; // 节点边数量 74 | "retained_size": number; // 节点保留大小(GC 后可释放大小) 75 | "pt": number; // 自身大小与保留大小的比值 76 | "edges": { 77 | "edge_type": string; // 边类型 78 | "to_node": number; // 边指向的节点ID 79 | "name_or_index": string; // 边的名称或数组索引 80 | } []; 81 | "source": string; // 节点来源文件 82 | "constructor": string; // 节点构造函数 83 | "percent": string; // 节点保留大小占比 84 | 85 | } 86 | ``` 87 | 88 | ## 默认示例 89 | 90 | 点击左上角的 `Example` 按钮查看默认示例。 91 | 92 | 默认示例的代码如下: 93 | 94 | ```js 95 | const express = require('express'); 96 | const app = express(); 97 | const fs = require('fs') 98 | // the memory leak code 99 | let theThing = null; 100 | let replaceThing = function() { 101 | let leak = theThing; 102 | let unused = function() { 103 | if (leak) 104 | console.log("hi") 105 | }; 106 | 107 | // 不断修改theThing的引用 108 | theThing = { 109 | bigNumber: 1, 110 | bigArr: [], 111 | longStr: new Array(1000000), 112 | someMethod: function() { 113 | console.log('a'); 114 | } 115 | }; 116 | }; 117 | let index = 0 118 | app.get('/leak', function closureLeak(req, res, next) { 119 | replaceThing(); 120 | index++ 121 | if (index === 1) { 122 | const stream = require('v8').getHeapSnapshot() 123 | stream.pipe(fs.createWriteStream('small-closure.heapsnapshot')) 124 | } 125 | if (index === 40) { 126 | const stream = require('v8').getHeapSnapshot() 127 | stream.pipe(fs.createWriteStream('medium-closure.heapsnapshot')) 128 | } 129 | if (index === 50) { 130 | const stream = require('v8').getHeapSnapshot() 131 | stream.pipe(fs.createWriteStream('big-closure.heapsnapshot')) 132 | } 133 | res.send('Hello Node'); 134 | }); 135 | 136 | app.listen(3001); 137 | ``` 138 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | // serverless 部署使用 2 | const WebFramework = require('@midwayjs/koa').Framework 3 | const { Bootstrap } = require('@midwayjs/bootstrap') 4 | 5 | module.exports = async () => { 6 | // 加载框架并执行 7 | await Bootstrap.run() 8 | // 获取依赖注入容器 9 | const container = Bootstrap.getApplicationContext() 10 | // 获取 koa framework 11 | const framework = container.get(WebFramework) 12 | // 返回 app 对象 13 | return framework.getApplication() 14 | } 15 | -------------------------------------------------------------------------------- /example/bootstrap.js: -------------------------------------------------------------------------------- 1 | 2 | const { Bootstrap } = require('@midwayjs/bootstrap') 3 | const { loadConfig } = require('ssr-common-utils') 4 | 5 | const { serverPort } = loadConfig() 6 | 7 | Bootstrap 8 | .configure({ 9 | globalConfig: { 10 | koa: { 11 | port: serverPort 12 | } 13 | } 14 | }) 15 | .run() 16 | -------------------------------------------------------------------------------- /example/config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from 'ssr-types' 2 | 3 | const userConfig: UserConfig = { 4 | mode: 'csr', 5 | css: () => { 6 | const tailwindcss = require('tailwindcss') 7 | const autoprefixer = require('autoprefixer') 8 | return { 9 | loaderOptions: { 10 | postcss: { 11 | plugins: [ 12 | tailwindcss, 13 | autoprefixer 14 | ] 15 | } 16 | } 17 | } 18 | }, 19 | webpackDevServerConfig: { 20 | headers: { 21 | 'Access-Control-Allow-Origin': '*', 22 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 23 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', 24 | "Cross-Origin-Embedder-Policy": "require-corp", 25 | "Cross-Origin-Opener-Policy": "same-origin", 26 | } 27 | }, 28 | customeFooterScript: [ 29 | { 30 | tagName: 'script', 31 | describe: { 32 | type: 'module' 33 | 34 | }, 35 | content: ` 36 | import * as v8_profiler_rs from '/v8_profiler_rs.js' 37 | window.v8_profiler_rs = v8_profiler_rs 38 | window.v8_profiler_rs.default() 39 | ` 40 | } 41 | ] 42 | } 43 | 44 | export { userConfig } 45 | -------------------------------------------------------------------------------- /example/f.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: v8-profile-rs 3 | provider: 4 | name: aliyun 5 | memorySize: 8192 6 | timeout: 20 7 | initTimeout: 20 8 | concurrency: 10 9 | runtime: nodejs18 10 | 11 | custom: # 发布后自动生成测试域名 12 | customDomain: 13 | domainName: auto 14 | 15 | package: 16 | include: 17 | - build 18 | - public 19 | - pkg 20 | exclude: 21 | - package-lock.json 22 | - src 23 | artifact: code.zip 24 | 25 | deployType: 26 | type: koa ## 部署的应用类型 27 | version: 3.0.0 28 | name: ssr -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "midway-vue3-ssr", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@midwayjs/bootstrap": "^3.0.0", 7 | "@midwayjs/core": "^3.0.0", 8 | "@midwayjs/decorator": "^3.0.0", 9 | "@midwayjs/koa": "^3.0.0", 10 | "@midwayjs/logger": "^3.0.0", 11 | "axios": "^1.1.3", 12 | "comlink": "^4.3.1", 13 | "koa-static": "^5.0.0", 14 | "markdown-it": "^14.1.0", 15 | "pinia": "^2.0.27", 16 | "ssr-common-utils": "^6.0.0", 17 | "ssr-core-vue3": "^6.0.0", 18 | "vant": "^3.4.3", 19 | "vue": "^3.0.0", 20 | "vue-router": "^4.0.0", 21 | "vuex": "^4.0.0" 22 | }, 23 | "devDependencies": { 24 | "@midwayjs/mock": "^3.0.0", 25 | "autoprefixer": "^10.4.13", 26 | "echarts": "^5.4.0", 27 | "eslint-config-standard-vue-ts": "^1.0.5", 28 | "ssr": "^6.0.0", 29 | "ssr-plugin-midway": "^6.0.0", 30 | "ssr-plugin-vue3": "^6.0.0", 31 | "ssr-types": "^6.0.0", 32 | "tailwindcss": "^3.0.0", 33 | "typescript": "^4.0.0", 34 | "worker-loader": "^3.0.8" 35 | }, 36 | "scripts": { 37 | "start": "ssr start", 38 | "start:vite": "ssr start --vite", 39 | "build": "ssr build", 40 | "build:o": "ssr build --optimize", 41 | "build:vite": "ssr build --vite", 42 | "deploy": "ssr build && ssr deploy", 43 | "lint:fix": "eslint example --ext .js,.tsx,.ts,.vue --fix" 44 | }, 45 | "optionalDependencies": { 46 | "@midwayjs/cli-plugin-faas": "^2.0.10", 47 | "@midwayjs/fcli-plugin-fc": "^2.0.10" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v8-profiler-rs", 3 | "type": "module", 4 | "collaborators": [ 5 | "yuuangzhang" 6 | ], 7 | "version": "1.0.6", 8 | "files": [ 9 | "v8_profiler_rs_bg.wasm", 10 | "v8_profiler_rs.js", 11 | "v8_profiler_rs.d.ts" 12 | ], 13 | "main": "v8_profiler_rs.js", 14 | "types": "v8_profiler_rs.d.ts", 15 | "sideEffects": [ 16 | "./snippets/*" 17 | ] 18 | } -------------------------------------------------------------------------------- /example/pkg/v8_profiler_rs.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * @param {Uint8Array} path 5 | * @returns {Uint8Array} 6 | */ 7 | export function parse_v8_snapshot(path: Uint8Array): Uint8Array; 8 | 9 | export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; 10 | 11 | export interface InitOutput { 12 | readonly parse_v8_snapshot: (a: number, b: number) => Array; 13 | readonly __wbindgen_export_0: WebAssembly.Table; 14 | readonly memory: WebAssembly.Memory; 15 | readonly __wbindgen_malloc: (a: number, b: number) => number; 16 | readonly __wbindgen_free: (a: number, b: number, c: number) => void; 17 | readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; 18 | readonly __wbindgen_thread_destroy: (a?: number, b?: number, c?: number) => void; 19 | readonly __wbindgen_start: (a: number) => void; 20 | } 21 | 22 | export type SyncInitInput = BufferSource | WebAssembly.Module; 23 | /** 24 | * Instantiates the given `module`, which can either be bytes or 25 | * a precompiled `WebAssembly.Module`. 26 | * 27 | * @param {{ module: SyncInitInput, memory?: WebAssembly.Memory, thread_stack_size?: number }} module - Passing `SyncInitInput` directly is deprecated. 28 | * @param {WebAssembly.Memory} memory - Deprecated. 29 | * 30 | * @returns {InitOutput} 31 | */ 32 | export function initSync(module: { module: SyncInitInput, memory?: WebAssembly.Memory, thread_stack_size?: number } | SyncInitInput, memory?: WebAssembly.Memory): InitOutput; 33 | 34 | /** 35 | * If `module_or_path` is {RequestInfo} or {URL}, makes a request and 36 | * for everything else, calls `WebAssembly.instantiate` directly. 37 | * 38 | * @param {{ module_or_path: InitInput | Promise, memory?: WebAssembly.Memory, thread_stack_size?: number }} module_or_path - Passing `InitInput` directly is deprecated. 39 | * @param {WebAssembly.Memory} memory - Deprecated. 40 | * 41 | * @returns {Promise} 42 | */ 43 | export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise, memory?: WebAssembly.Memory, thread_stack_size?: number } | InitInput | Promise, memory?: WebAssembly.Memory): Promise; 44 | -------------------------------------------------------------------------------- /example/pkg/v8_profiler_rs.js: -------------------------------------------------------------------------------- 1 | let wasm; 2 | 3 | let cachedUint8ArrayMemory0 = null; 4 | 5 | function getUint8ArrayMemory0() { 6 | if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.buffer !== wasm.memory.buffer) { 7 | cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); 8 | } 9 | return cachedUint8ArrayMemory0; 10 | } 11 | 12 | let WASM_VECTOR_LEN = 0; 13 | 14 | function passArray8ToWasm0(arg, malloc) { 15 | const ptr = malloc(arg.length * 1, 1) >>> 0; 16 | getUint8ArrayMemory0().set(arg, ptr / 1); 17 | WASM_VECTOR_LEN = arg.length; 18 | return ptr; 19 | } 20 | 21 | function getArrayU8FromWasm0(ptr, len) { 22 | ptr = ptr >>> 0; 23 | return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); 24 | } 25 | /** 26 | * @param {Uint8Array} path 27 | * @returns {Uint8Array} 28 | */ 29 | export function parse_v8_snapshot(path) { 30 | const ptr0 = passArray8ToWasm0(path, wasm.__wbindgen_malloc); 31 | const len0 = WASM_VECTOR_LEN; 32 | const ret = wasm.parse_v8_snapshot(ptr0, len0); 33 | var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice(); 34 | wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); 35 | return v2; 36 | } 37 | 38 | const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); 39 | 40 | if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; 41 | 42 | function getStringFromWasm0(ptr, len) { 43 | ptr = ptr >>> 0; 44 | return cachedTextDecoder.decode(getUint8ArrayMemory0().slice(ptr, ptr + len)); 45 | } 46 | 47 | const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); 48 | 49 | const encodeString = function (arg, view) { 50 | const buf = cachedTextEncoder.encode(arg); 51 | view.set(buf); 52 | return { 53 | read: arg.length, 54 | written: buf.length 55 | }; 56 | }; 57 | 58 | function passStringToWasm0(arg, malloc, realloc) { 59 | 60 | if (realloc === undefined) { 61 | const buf = cachedTextEncoder.encode(arg); 62 | const ptr = malloc(buf.length, 1) >>> 0; 63 | getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); 64 | WASM_VECTOR_LEN = buf.length; 65 | return ptr; 66 | } 67 | 68 | let len = arg.length; 69 | let ptr = malloc(len, 1) >>> 0; 70 | 71 | const mem = getUint8ArrayMemory0(); 72 | 73 | let offset = 0; 74 | 75 | for (; offset < len; offset++) { 76 | const code = arg.charCodeAt(offset); 77 | if (code > 0x7F) break; 78 | mem[ptr + offset] = code; 79 | } 80 | 81 | if (offset !== len) { 82 | if (offset !== 0) { 83 | arg = arg.slice(offset); 84 | } 85 | ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; 86 | const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); 87 | const ret = encodeString(arg, view); 88 | 89 | offset += ret.written; 90 | ptr = realloc(ptr, len, offset, 1) >>> 0; 91 | } 92 | 93 | WASM_VECTOR_LEN = offset; 94 | return ptr; 95 | } 96 | 97 | let cachedDataViewMemory0 = null; 98 | 99 | function getDataViewMemory0() { 100 | if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer !== wasm.memory.buffer) { 101 | cachedDataViewMemory0 = new DataView(wasm.memory.buffer); 102 | } 103 | return cachedDataViewMemory0; 104 | } 105 | 106 | async function __wbg_load(module, imports) { 107 | if (typeof Response === 'function' && module instanceof Response) { 108 | if (typeof WebAssembly.instantiateStreaming === 'function') { 109 | try { 110 | return await WebAssembly.instantiateStreaming(module, imports); 111 | 112 | } catch (e) { 113 | if (module.headers.get('Content-Type') != 'application/wasm') { 114 | console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); 115 | 116 | } else { 117 | throw e; 118 | } 119 | } 120 | } 121 | 122 | const bytes = await module.arrayBuffer(); 123 | return await WebAssembly.instantiate(bytes, imports); 124 | 125 | } else { 126 | const instance = await WebAssembly.instantiate(module, imports); 127 | 128 | if (instance instanceof WebAssembly.Instance) { 129 | return { instance, module }; 130 | 131 | } else { 132 | return instance; 133 | } 134 | } 135 | } 136 | 137 | function __wbg_get_imports() { 138 | const imports = {}; 139 | imports.wbg = {}; 140 | imports.wbg.__wbg_new_abda76e883ba8a5f = function() { 141 | const ret = new Error(); 142 | return ret; 143 | }; 144 | imports.wbg.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { 145 | const ret = arg1.stack; 146 | const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 147 | const len1 = WASM_VECTOR_LEN; 148 | getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); 149 | getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); 150 | }; 151 | imports.wbg.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { 152 | let deferred0_0; 153 | let deferred0_1; 154 | try { 155 | deferred0_0 = arg0; 156 | deferred0_1 = arg1; 157 | console.error(getStringFromWasm0(arg0, arg1)); 158 | } finally { 159 | wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); 160 | } 161 | }; 162 | imports.wbg.__wbindgen_init_externref_table = function() { 163 | const table = wasm.__wbindgen_export_0; 164 | const offset = table.grow(4); 165 | table.set(0, undefined); 166 | table.set(offset + 0, undefined); 167 | table.set(offset + 1, null); 168 | table.set(offset + 2, true); 169 | table.set(offset + 3, false); 170 | ; 171 | }; 172 | 173 | return imports; 174 | } 175 | 176 | function __wbg_init_memory(imports, memory) { 177 | imports.wbg.memory = memory || new WebAssembly.Memory({initial:18,maximum:49152,shared:true}); 178 | } 179 | 180 | function __wbg_finalize_init(instance, module, thread_stack_size) { 181 | wasm = instance.exports; 182 | __wbg_init.__wbindgen_wasm_module = module; 183 | cachedDataViewMemory0 = null; 184 | cachedUint8ArrayMemory0 = null; 185 | 186 | if (typeof thread_stack_size !== 'undefined' && (typeof thread_stack_size !== 'number' || thread_stack_size === 0 || thread_stack_size % 65536 !== 0)) { throw 'invalid stack size' } 187 | wasm.__wbindgen_start(thread_stack_size); 188 | return wasm; 189 | } 190 | 191 | function initSync(module, memory) { 192 | if (wasm !== undefined) return wasm; 193 | 194 | let thread_stack_size 195 | if (typeof module !== 'undefined') { 196 | if (Object.getPrototypeOf(module) === Object.prototype) { 197 | ({module, memory, thread_stack_size} = module) 198 | } else { 199 | console.warn('using deprecated parameters for `initSync()`; pass a single object instead') 200 | } 201 | } 202 | 203 | const imports = __wbg_get_imports(); 204 | 205 | __wbg_init_memory(imports, memory); 206 | 207 | if (!(module instanceof WebAssembly.Module)) { 208 | module = new WebAssembly.Module(module); 209 | } 210 | 211 | const instance = new WebAssembly.Instance(module, imports); 212 | 213 | return __wbg_finalize_init(instance, module, thread_stack_size); 214 | } 215 | 216 | async function __wbg_init(module_or_path, memory) { 217 | if (wasm !== undefined) return wasm; 218 | 219 | let thread_stack_size 220 | if (typeof module_or_path !== 'undefined') { 221 | if (Object.getPrototypeOf(module_or_path) === Object.prototype) { 222 | ({module_or_path, memory, thread_stack_size} = module_or_path) 223 | } else { 224 | console.warn('using deprecated parameters for the initialization function; pass a single object instead') 225 | } 226 | } 227 | 228 | if (typeof module_or_path === 'undefined') { 229 | module_or_path = new URL('v8_profiler_rs_bg.wasm', import.meta.url); 230 | } 231 | const imports = __wbg_get_imports(); 232 | 233 | if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { 234 | module_or_path = fetch(module_or_path); 235 | } 236 | 237 | __wbg_init_memory(imports, memory); 238 | 239 | const { instance, module } = await __wbg_load(await module_or_path, imports); 240 | 241 | return __wbg_finalize_init(instance, module, thread_stack_size); 242 | } 243 | 244 | export { initSync }; 245 | export default __wbg_init; 246 | -------------------------------------------------------------------------------- /example/pkg/v8_profiler_rs_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/example/pkg/v8_profiler_rs_bg.wasm -------------------------------------------------------------------------------- /example/pkg/v8_profiler_rs_bg.wasm.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export function parse_v8_snapshot(a: number, b: number): Array; 4 | export const __wbindgen_export_0: WebAssembly.Table; 5 | export const memory: WebAssembly.Memory; 6 | export function __wbindgen_malloc(a: number, b: number): number; 7 | export function __wbindgen_free(a: number, b: number, c: number): void; 8 | export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number; 9 | export function __wbindgen_thread_destroy(a: number, b: number, c: number): void; 10 | export function __wbindgen_start(a: number): void; 11 | -------------------------------------------------------------------------------- /example/public/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icons_outlined_close 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/public/file.txt: -------------------------------------------------------------------------------- 1 | This is a static file -------------------------------------------------------------------------------- /example/public/logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/example/public/logo.jpeg -------------------------------------------------------------------------------- /example/public/worker.js: -------------------------------------------------------------------------------- 1 | import * as v8_profiler_rs from '/v8_profiler_rs.js' 2 | v8_profiler_rs.default() 3 | 4 | self.onmessage = async (e) => { 5 | const res= v8_profiler_rs.parse_v8_snapshot(e.data) 6 | self.postMessage(res) 7 | } 8 | -------------------------------------------------------------------------------- /example/src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | import { MidwayConfig } from '@midwayjs/core' 2 | 3 | export default { 4 | // use for cookie sign key, should change to your own and keep security 5 | keys: '1650192482948_2252' 6 | } as MidwayConfig -------------------------------------------------------------------------------- /example/src/configuration.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, App } from '@midwayjs/decorator' 2 | import * as koa from '@midwayjs/koa' 3 | import { join } from 'path' 4 | import { initialSSRDevProxy, getCwd } from 'ssr-common-utils' 5 | 6 | const koaStatic = require('koa-static') 7 | const cwd = getCwd() 8 | 9 | @Configuration({ 10 | imports: [ 11 | koa 12 | ], 13 | importConfigs: [join(__dirname, './config')] 14 | }) 15 | export class ContainerLifeCycle { 16 | @App() 17 | app: koa.Application 18 | 19 | async onReady() { 20 | const commonConfig = { 21 | maxage: 31536000, 22 | setHeaders: (res) => { 23 | res.setHeader("Cross-Origin-Embedder-Policy", "require-corp") 24 | res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); 25 | } 26 | } 27 | this.app.use(koaStatic(join(cwd, './build'), commonConfig)) 28 | this.app.use(koaStatic(join(cwd, './build/client'), commonConfig)) 29 | this.app.use(koaStatic(join(cwd, './public'), { 30 | ...commonConfig, 31 | maxage: 'no-cache' 32 | })) 33 | this.app.use(koaStatic(join(cwd, './pkg'), { 34 | ...commonConfig, 35 | maxage: 'no-cache' 36 | })) 37 | await initialSSRDevProxy(this.app) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/src/controller/index.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Provide, Inject } from '@midwayjs/decorator' 2 | import { Context } from '@midwayjs/koa' 3 | import { render } from 'ssr-core-vue3' 4 | 5 | @Provide() 6 | @Controller('/') 7 | export class Index { 8 | @Inject() 9 | ctx: Context 10 | 11 | @Get('/') 12 | @Get('/report') 13 | async handler(): Promise { 14 | // 渲染降级参考文档 http://doc.ssr-fc.com/docs/features$csr#%E5%A4%84%E7%90%86%20%E6%B5%81%20%E8%BF%94%E5%9B%9E%E5%BD%A2%E5%BC%8F%E7%9A%84%E9%99%8D%E7%BA%A7 15 | const { ctx } = this 16 | try { 17 | const stream = await render(this.ctx, { 18 | stream: true 19 | }) 20 | ctx.set("Cross-Origin-Embedder-Policy", "require-corp") 21 | ctx.set("Cross-Origin-Opener-Policy", "same-origin"); 22 | ctx.body = stream 23 | } catch (error) { 24 | console.log('ssr error', error) 25 | const stream = await render(ctx, { 26 | stream: true, 27 | mode: 'csr' 28 | }) 29 | ctx.body = stream 30 | } 31 | } 32 | @Get('/index/node/:node') 33 | async handlerNode(): Promise { 34 | const { ctx } = this 35 | ctx.redirect('/') 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./web/**/*.{vue,js,ts,jsx,tsx}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | variants: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "target": "ES2018", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "inlineSourceMap": true, 11 | "noImplicitThis": true, 12 | "noUnusedLocals": true, 13 | "stripInternal": true, 14 | "pretty": true, 15 | "skipLibCheck": true, 16 | "declaration": true, 17 | "outDir": "dist", 18 | "resolveJsonModule": true, 19 | "typeRoots": [ 20 | "./typings", 21 | "./node_modules/@types" 22 | ], 23 | "paths": { 24 | "@/*": [ 25 | "./src/*" 26 | ], 27 | "~/*": [ 28 | "./*" 29 | ] 30 | } 31 | }, 32 | "include": [ 33 | "src" 34 | ] 35 | } -------------------------------------------------------------------------------- /example/web/@types/global/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IWindow } from 'ssr-types' 2 | import * as v8_profiler_rs from '~/pkg/v8_profiler_rs' 3 | 4 | declare global { 5 | interface Window { 6 | __USE_SSR__?: IWindow['__USE_SSR__'] 7 | __INITIAL_DATA__?: IWindow['__INITIAL_DATA__'] 8 | v8_profiler_rs?: typeof v8_profiler_rs 9 | } 10 | const __isBrowser__: Boolean 11 | } 12 | -------------------------------------------------------------------------------- /example/web/analyze.ts: -------------------------------------------------------------------------------- 1 | import type { Node } from './type' 2 | 3 | 4 | 5 | const oneKb = 1024 6 | const oneMb = 1024 * 1024 7 | export const unitConvert = (byte: number | string) => { 8 | const val = Number(byte) 9 | if (val > 5 * oneMb) { 10 | return byteToMb(byte) 11 | } 12 | if (val > oneKb) { 13 | return (val / oneKb).toFixed(2) + 'kb' 14 | } else { 15 | return val + 'byte' 16 | } 17 | } 18 | export const byteToMb = (byte: number | string) => { 19 | const val = Number(byte) 20 | return (val / oneMb).toFixed(2) + 'mb' 21 | } 22 | 23 | export const isNativeSource = (source: string) => source.startsWith('node:') || source.startsWith('internal/') 24 | 25 | 26 | export const memoryPercent = (node: Node, root: Node) => { 27 | return ((node.retained_size / root.retained_size) * 100).toFixed(2) + '%' 28 | } 29 | 30 | -------------------------------------------------------------------------------- /example/web/common.less: -------------------------------------------------------------------------------- 1 | .verticalCenter { 2 | position: absolute; 3 | top: 50%; 4 | transform: translate(0, -50%); 5 | } 6 | 7 | html,body,#app { 8 | height: 100%; 9 | } 10 | 11 | .levelCenter { 12 | position: absolute; 13 | left: 50%; 14 | transform: translate(-50%, 0); 15 | } 16 | 17 | .verticalLevelCenter { 18 | position: absolute; 19 | left: 50%; 20 | top: 50%; 21 | transform: translate(-50%, -50%); 22 | } 23 | 24 | .verticalCenterFlex { 25 | display: flex; 26 | align-items: center; 27 | } 28 | 29 | .levelCenterFlex { 30 | display: flex; 31 | justify-content: center; 32 | } 33 | 34 | .verticalLevelCenterFlex { 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | } 39 | 40 | .overflowEllipsis { 41 | overflow: hidden; 42 | text-overflow: ellipsis; 43 | white-space: nowrap; 44 | } 45 | 46 | .pName { 47 | font-size: 15px; 48 | color: #000000; 49 | margin-top: 10px; 50 | padding-left: 9px; 51 | overflow: hidden; 52 | white-space: nowrap; 53 | text-overflow: ellipsis; 54 | height: 25px; 55 | line-height: 25px; 56 | z-index: 99; 57 | } 58 | 59 | .pDesc { 60 | padding-left: 9px; 61 | font-size: 13px; 62 | color: #999999; 63 | margin-bottom: 20px; 64 | overflow: hidden; 65 | white-space: nowrap; 66 | text-overflow: ellipsis; 67 | z-index: 99; 68 | } 69 | 70 | * { 71 | margin: 0; 72 | padding: 0; 73 | } 74 | 75 | a { 76 | color: #fff; 77 | text-decoration: none; 78 | } 79 | 80 | ul { 81 | list-style: none; 82 | } 83 | 84 | .van-loading__text { 85 | white-space: nowrap; 86 | } 87 | .van-dropdown-item { 88 | left: 20px; 89 | } 90 | .van-popover__action { 91 | height: auto; 92 | padding: 10px; 93 | white-space: nowrap; 94 | width: 100%; 95 | } 96 | 97 | .loadingContainer { 98 | display: flex; 99 | flex-direction: column; 100 | 101 | .text { 102 | white-space: nowrap; 103 | color: rgba(0, 0, 0, .5); 104 | } 105 | 106 | img { 107 | width: 30px; 108 | height: 30px; 109 | }; 110 | align-items: center; 111 | .verticalLevelCenter; 112 | } 113 | 114 | .tooltipText { 115 | max-width: 800px; 116 | white-space: normal; 117 | 118 | .item { 119 | display: flex; 120 | margin-top: 10px; 121 | 122 | .name { 123 | color: #666; 124 | white-space: nowrap; 125 | } 126 | 127 | .code { 128 | color: #666; 129 | margin-left: 6px; 130 | } 131 | } 132 | } 133 | .red { 134 | color: red !important 135 | } 136 | 137 | @tailwind components; 138 | @tailwind utilities; 139 | -------------------------------------------------------------------------------- /example/web/components/layout/App.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 354 | 355 | 359 | -------------------------------------------------------------------------------- /example/web/components/layout/index.less: -------------------------------------------------------------------------------- 1 | .container { 2 | min-width: 100% !important; 3 | height: 100%; 4 | display: flex; 5 | 6 | .analyze { 7 | background-color: #fff; 8 | height: 100%; 9 | box-shadow: -9px -1px 10px 2px rgb(0 0 0 / 10%); 10 | } 11 | 12 | .board { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | background-color: #eee; 17 | box-sizing: border-box; 18 | padding: 0 20px; 19 | } 20 | 21 | .title { 22 | color: rgba(0, 0green, 0, .5); 23 | margin-bottom: 20px; 24 | margin-top: 20px; 25 | } 26 | 27 | .inputFile { 28 | position: fixed; 29 | top: -100px; 30 | } 31 | 32 | .field { 33 | min-width: 220px; 34 | max-width: 220px; 35 | margin: 10px 0; 36 | min-height: 44px 37 | } 38 | 39 | .tips { 40 | .verticalLevelCenter; 41 | color: rgba(0, 0green, 0, .5); 42 | } 43 | 44 | .btn { 45 | .field; 46 | } 47 | 48 | .relative { 49 | position: relative; 50 | 51 | } 52 | 53 | .formatBtn { 54 | .verticalCenter; 55 | width: 80%; 56 | 57 | } 58 | 59 | .chartContainer { 60 | width: 100%; 61 | position: relative; 62 | } 63 | 64 | .refText { 65 | display: flex; 66 | justify-content: center; 67 | color: #646566; 68 | margin-bottom: 10px; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example/web/components/layout/index.vue: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /example/web/hooks/config.ts: -------------------------------------------------------------------------------- 1 | export {} -------------------------------------------------------------------------------- /example/web/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config' -------------------------------------------------------------------------------- /example/web/pages/index/index.less: -------------------------------------------------------------------------------- 1 | html,body,#app,#main,#nodeMain { 2 | width: 100%; 3 | height: 100%; 4 | } -------------------------------------------------------------------------------- /example/web/pages/index/node/render$node.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 156 | -------------------------------------------------------------------------------- /example/web/pages/index/render.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 88 | -------------------------------------------------------------------------------- /example/web/pages/report/render.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 123 | 124 | -------------------------------------------------------------------------------- /example/web/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { Node } from '@/type' 2 | 3 | 4 | 5 | 6 | export const globalStore = { 7 | idToNode: {} as Record, 8 | data: [] as Node[], 9 | additionalNodes: [] as Node[], 10 | biggerNodes: [] as Node[], 11 | } -------------------------------------------------------------------------------- /example/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | // 此文件仅用于本地开发配置IDE在检查 web 文件夹下ts语法时的规则,不用于实际构建 2 | { 3 | "compileOnSave": true, 4 | "compilerOptions": { 5 | "target": "ES2018", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "inlineSourceMap": true, 11 | "noImplicitThis": true, 12 | "noUnusedLocals": true, 13 | "stripInternal": true, 14 | "pretty": true, 15 | "declaration": false, 16 | "resolveJsonModule": true, 17 | "strict": true, 18 | "noEmit": true, 19 | "baseUrl": "./", 20 | "strictNullChecks": true, 21 | "esModuleInterop": true, 22 | "jsx": "preserve", 23 | "paths": { 24 | "@/*": [ 25 | "./*" 26 | ], 27 | "~/*": [ 28 | "../*" 29 | ] 30 | } 31 | }, 32 | "include": [ 33 | "." 34 | ] 35 | } -------------------------------------------------------------------------------- /example/web/type.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Handlers = (params: string | Uint8Array, type: 'single', filterEdge: boolean) => { data: string } 3 | export type SortDetail = { 4 | name: string 5 | source?: string[] 6 | sourceString: string 7 | count: number 8 | shallowSize: number 9 | retainedSize: number 10 | } 11 | 12 | export type Params = { 13 | data: Node[] 14 | type?: string 15 | write?: boolean 16 | } 17 | export type Node = { 18 | node_type: string; 19 | name: string; 20 | id: number; 21 | size: number; 22 | edge_count: number; 23 | retained_size: number; 24 | pt: number; 25 | edges: { 26 | edge_type: string; 27 | to_node: number; 28 | name_or_index: string; 29 | is_weak: boolean; 30 | is_ref: boolean; 31 | }[]; 32 | parents: number[]; 33 | bigger_number?: number; 34 | source?: string; 35 | constructor: string; 36 | percent?: string; 37 | retainer: number 38 | } 39 | 40 | export type Analyze = { 41 | sortByCount?: SortDetail[] 42 | sortRetainedSize?: Node[], 43 | } 44 | export type RenderOptions = { 45 | isIndex: boolean; 46 | maxNodes: number; 47 | nodeSize: number; 48 | edgeLength: number; 49 | edgeCounts: number; 50 | childDepth: number; 51 | nodeName: string; 52 | nodeId: string; 53 | filterNative: number, 54 | tooltip: {}, 55 | force: {}, 56 | label: {}, 57 | analyze: { 58 | type: 'single' | 'compare', 59 | compare: 'addtional' | 'bigger' 60 | }, 61 | filterNode: number 62 | nodeByConstructor?: Record 63 | filterNodeByConstructor?: string 64 | } -------------------------------------------------------------------------------- /example/web/utils.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { Notify } from 'vant' 3 | import { globalStore } from '@/store' 4 | import type { Node } from './type' 5 | export const noRepeat = (arr: number[]) => Array.from(new Set(arr)) 6 | 7 | 8 | export enum NodeType { 9 | kHidden = 'hidden', // Hidden node, may be filtered when shown to user. 10 | kArray = 'array', // An array of elements. 11 | kString = 'string', // A string. 12 | kObject = 'object', // A JS object (except for arrays and strings). 13 | kCode = 'code', // Compiled code. 14 | kClosure = 'closure', // Function closure. 15 | kRegExp = 'regexp', // RegExp. 16 | kHeapNumber = 'number', // Number stored in the heap. 17 | kNative = 'native', // Native object (not from V8 heap). 18 | kSynthetic = 'synthetic', // Synthetic object, usually used for grouping 19 | // snapshot items together. 20 | kConsString = 'concatenated string', // Concatenated string. A pair of pointers to strings. 21 | kSlicedString = 'sliced string', // Sliced string. A fragment of another string. 22 | kSymbol = 'symbol', // A Symbol (ES6). 23 | kBigInt = 'bigint', // BigInt. 24 | kObjectShape = 14, // Internal data used for tracking the shapes (or 25 | // "hidden classes") of JS objects. 26 | } 27 | export const NODE_COLORS: Record = { 28 | '(system)': '#D32F2F', // bright red for system 29 | 'array': '#1976D2', // bright blue for arrays 30 | 'string': '#388E3C', // bright green for strings 31 | 'object': '#E65100', 32 | '(compiled code)': '#7B1FA2', // bright purple for compiled code 33 | 'closure': '#FF4081', // bright pink for closures 34 | 'regexp': '#F57F17', // bright goldenrod for regexps 35 | 'number': '#2E7D32', // bright green for numbers 36 | 'native': '#795548', // brown for native 37 | 'synthetic': '#303F9F', // bright slate blue for synthetic 38 | 'concatenated string': '#388E3C', // same as string 39 | 'sliced string': '#388E3C', // same as string 40 | 'symbol': '#A1887F', // lighter brown for symbols 41 | 'bigint': '#2E7D32', // same as number 42 | 'hidden': '#757575', // medium gray for hidden 43 | 'unknown': '#212121' // dark gray for unknown 44 | } 45 | 46 | 47 | 48 | 49 | export const EDGE_COLORS: Record = { 50 | 'context': '#D32F2F', // bright red 51 | 'element': '#1976D2', // bright blue 52 | 'property': '#388E3C', // bright green 53 | 'internal': '#E65100', // bright orange 54 | 'shortcut': '#7B1FA2', // bright purple 55 | // 'hidden': '#757575', // medium gray 56 | // 'weak': '#FF4081', // bright pink 57 | }; 58 | export const EDGE_TYPES_PROPERTY = Object.keys(EDGE_COLORS) 59 | 60 | export const NODE_TYPES_PROPERTY = [ 61 | "hidden", 62 | "array", 63 | "string", 64 | "object", 65 | "code", 66 | "closure", 67 | "regexp", 68 | "number", 69 | "native", 70 | "synthetic", 71 | "concatenated string", 72 | "sliced string", 73 | "symbol", 74 | "bigint", 75 | ]; 76 | export const CONSTRUCTOR_PROPERTY = [ 77 | "(system)", 78 | "array", 79 | "string", 80 | "object", 81 | "(compiled code)", 82 | "closure", 83 | "regexp", 84 | "number", 85 | "native", 86 | "synthetic", 87 | "concatenated string", 88 | "sliced string", 89 | "symbol", 90 | "bigint", 91 | ]; 92 | 93 | export const tips = (text: string, type?: 'warning' | 'danger' | 'primary') => { 94 | Notify({ 95 | type: type || 'warning', 96 | duration: 5000, 97 | message: text 98 | }) 99 | } 100 | export enum Colors { 101 | jsNative = '#800080', 102 | jsNotNative = '#DA70D6', 103 | cNative = '#000', 104 | normal = '#4187f2', 105 | current = 'red' 106 | } 107 | 108 | export const renderNodeId = ref('') 109 | export const compareName = (name: string, target: string) => { 110 | return new RegExp(target).test(name) 111 | } 112 | export const nativeNode = [NodeType.kNative, NodeType.kHidden, NodeType.kSynthetic] 113 | 114 | export const read = async (file: any): Promise => { 115 | return new Promise((resolve) => { 116 | const reader = new FileReader() 117 | reader.readAsArrayBuffer(file); 118 | reader.onload = function (e: ProgressEvent) { 119 | const bytes = new Uint8Array(e.target?.result as ArrayBuffer); 120 | resolve(bytes); 121 | } 122 | }) 123 | } 124 | export const readAsStr = async (file: any): Promise => { 125 | return new Promise((resolve) => { 126 | const reader = new FileReader() 127 | reader.readAsText(file); 128 | reader.onload = function (e: any) { 129 | resolve(e.target.result); 130 | } 131 | }) 132 | } 133 | export const parseOptions = [ 134 | { text: 'Parse Method', value: 'wasm' }, 135 | { text: 'Wasm + WebWorker', value: 'webworker' }, 136 | ]; 137 | export const compareOptions = [ 138 | { text: 'Compare Type', value: 'addtional' }, 139 | { text: 'Additional Nodes', value: 'addtional' }, 140 | { text: 'Growing Nodes', value: 'bigger' }, 141 | ]; 142 | 143 | export const filterNodeOptions = [ 144 | { text: 'Filter Nodes', value: 0 }, 145 | { text: 'Show Nodes With Source Path', value: 1 }, 146 | ] 147 | 148 | export const filterConstructorOptions = ref([ 149 | { text: 'Filter Nodes type', value: 'all' }, 150 | ]) 151 | 152 | export const getNodeById = (data: Node[], idOrdinal: Record, nodeId: number) => { 153 | return data[idOrdinal[nodeId]] 154 | } 155 | 156 | export const MAXNODESCOUNT = 50 * 1024 * 1024 // 50 mb 默认开启过滤 157 | 158 | export const memoryPercent = (node: Node, root: Node) => { 159 | return ((node.retained_size / root.retained_size) * 100).toFixed(2) + '%' 160 | } 161 | 162 | export const calculateByConstructor = (nodes?: Node[]) => { 163 | if (!nodes) { 164 | nodes = globalStore.data 165 | } 166 | const constructorCounts: Record = {}; 167 | 168 | // Count nodes by constructor 169 | nodes.forEach(node => { 170 | const constructor = node.constructor || 'unknown'; 171 | constructorCounts[constructor] = (constructorCounts[constructor] || 0) + 1; 172 | }); 173 | 174 | // Sort constructors by count and get top 10 175 | const sortedConstructors = Object.entries(constructorCounts) 176 | .sort(([, a], [, b]) => b - a) 177 | .slice(0, 10) 178 | .reduce((obj, [key, value]) => ({ 179 | ...obj, 180 | [key]: value 181 | }), {}); 182 | filterConstructorOptions.value = [{ text: 'Filter Nodes type', value: 'all' }] 183 | .concat(Object.keys(sortedConstructors).map(item => ({ text: item, value: item }))) 184 | return sortedConstructors 185 | } 186 | 187 | export function sleep(ms: number) { 188 | return new Promise(resolve => setTimeout(resolve, ms)); 189 | } -------------------------------------------------------------------------------- /images/5c286fe4dc2b5443.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/5c286fe4dc2b5443.png -------------------------------------------------------------------------------- /images/6c52bdda69534e42bc51b57042242650_noop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/6c52bdda69534e42bc51b57042242650_noop.png -------------------------------------------------------------------------------- /images/77d1693d497f482b8a61a7c2aaa8bce9_noop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/77d1693d497f482b8a61a7c2aaa8bce9_noop.png -------------------------------------------------------------------------------- /images/v8-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-1.png -------------------------------------------------------------------------------- /images/v8-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-10.png -------------------------------------------------------------------------------- /images/v8-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-11.png -------------------------------------------------------------------------------- /images/v8-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-12.png -------------------------------------------------------------------------------- /images/v8-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-2.png -------------------------------------------------------------------------------- /images/v8-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-3.png -------------------------------------------------------------------------------- /images/v8-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-4.png -------------------------------------------------------------------------------- /images/v8-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-5.png -------------------------------------------------------------------------------- /images/v8-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-6.png -------------------------------------------------------------------------------- /images/v8-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-7.png -------------------------------------------------------------------------------- /images/v8-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-8.png -------------------------------------------------------------------------------- /images/v8-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyuang/v8-profiler-rs/d39a6910feac9326936d9f0c32a559d5a1811c99/images/v8-9.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v8-profiler-rs", 3 | "description": "Analyze v8 heapsnapshot By Rust", 4 | "version": "1.0.46", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "cd ./example && npm run start" 8 | }, 9 | "dependencies": {}, 10 | "files": [ 11 | "example", 12 | "README.md", 13 | "README_Zh.md" 14 | ] 15 | } 16 | --------------------------------------------------------------------------------