wukong-profiler 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2025] [ęØē¼]
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.
package/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # wukong-profiler
2
+
3
+ šŸ”„ High-performance Node/CLI profiler supporting:
4
+
5
+ - Nested steps (true Flame Graph)
6
+ - Chrome Trace export (`--trace trace.json`)
7
+ - HOT step detection with CI failure
8
+ - Profile diff for performance regression detection
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install wukong-profiler
14
+ ```
15
+
16
+ Or use directly via npx:
17
+
18
+ ```bash
19
+ npx wukong-profiler [options]
20
+ ```
21
+
22
+ ---
23
+
24
+ ## CLI Usage
25
+
26
+ ```bash
27
+ npx wukong-profiler --flame --trace trace.json --hot-threshold 0.8 --fail-on-hot
28
+ ```
29
+
30
+ ### Run profiler
31
+
32
+ ```bash
33
+ # Simple run
34
+ npx wukong-profiler --flame --trace trace.json
35
+
36
+ # Set HOT threshold
37
+ npx wukong-profiler --hot-threshold 0.8 --fail-on-hot
38
+
39
+ # With baseline profile for regression detection
40
+ npx wukong-profiler --diff-base baseline.json --diff-threshold 0.2
41
+ ```
42
+
43
+ ### Generate HTML report
44
+
45
+ ```bash
46
+ # Generate HTML report from profile.json
47
+ npx wukong-profiler report ./profile.json
48
+
49
+ # Generate and open automatically in browser
50
+ npx wukong-profiler report ./profile.json --open
51
+
52
+ # Specify output HTML file
53
+ npx wukong-profiler report ./profile.json -o my-report.html
54
+ ```
55
+
56
+ **Options:**
57
+
58
+ | Option | Description |
59
+ | ---------------------- | ------------------------------------------------------- |
60
+ | `--profile` | Save profile JSON for analysis |
61
+ | `--flame` | Display flame-like console output |
62
+ | `--trace <file>` | Export Chrome Trace JSON file |
63
+ | `--hot-threshold <n>` | HOT step threshold (default: 0.8) |
64
+ | `--fail-on-hot` | Exit with non-zero code if a HOT step exceeds threshold |
65
+ | `--diff-base <file>` | Compare current profile with baseline for regression |
66
+ | `--diff-threshold <n>` | Diff threshold for regression (default: 0.2) |
67
+ | `-v, --version` | Show version |
68
+ | `-h, --help` | Show help |
69
+
70
+ ---
71
+
72
+ ## Programmatic Usage
73
+
74
+ ```js
75
+ import { createProfiler } from 'wukong-profiler'
76
+
77
+ const profiler = createProfiler({
78
+ enabled: true,
79
+ flame: true,
80
+ traceFile: 'trace.json',
81
+ hotThreshold: 0.8,
82
+ failOnHot: true,
83
+ diffBaseFile: 'baseline.json',
84
+ diffThreshold: 0.2
85
+ })
86
+
87
+ profiler.step('load data', () => {
88
+ // heavy operation
89
+ })
90
+ profiler.step('process data', () => {
91
+ // another heavy operation
92
+ })
93
+
94
+ profiler.end('Total')
95
+ ```
96
+
97
+ ---
98
+
99
+ ## API Reference
100
+
101
+ ### `createProfiler(options)`
102
+
103
+ Returns a profiler instance.
104
+
105
+ #### Options
106
+
107
+ | Name | Default | Description |
108
+ | --------------- | ----------- | --------------------------- |
109
+ | `enabled` | `false` | Enable output & JSON export |
110
+ | `verbose` | `false` | Verbose logging |
111
+ | `flame` | `false` | Flame-style tree output |
112
+ | `slowThreshold` | `500` | Slow step threshold (ms) |
113
+ | `hotThreshold` | `0.8` | HOT step ratio |
114
+ | `traceFile` | `undefined` | Chrome trace file |
115
+ | `failOnHot` | `false` | Fail CI on HOT step |
116
+ | `diffBaseFile` | `undefined` | Base profile for diff |
117
+ | `diffThreshold` | `0.2` | Regression threshold |
118
+
119
+ ---
120
+
121
+ ### `profiler.step(name, fn)`
122
+
123
+ Measure a synchronous step.
124
+
125
+ ---
126
+
127
+ ### `profiler.measure(name, fn)`
128
+
129
+ Measure sync or async function.
130
+
131
+ ---
132
+
133
+ ### `profiler.end(label?)`
134
+
135
+ Finish profiling and output results.
136
+
137
+ ---
138
+
139
+ ## Examples
140
+
141
+ ```bash
142
+ node examples/basic.mjs
143
+ node examples/flame.mjs
144
+ node examples/async.mjs
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Chrome Trace
150
+
151
+ ```bash
152
+ node examples/basic.mjs
153
+ chrome://tracing
154
+ ```
155
+
156
+ Load the generated trace file.
157
+
158
+ or
159
+
160
+ ```text
161
+ https://ui.perfetto.dev
162
+ ```
163
+
164
+ drag to Load the generated trace file.
165
+
166
+ ---
167
+
168
+ ### šŸ“Š Profile Summary (Top HOT Paths)
169
+
170
+ ````js
171
+ const summary = profiler.summary({ top: 3 });
172
+
173
+ summary.top.forEach(step => {
174
+ console.log(step.path, step.ratio);
175
+ });
176
+
177
+ ---
178
+
179
+ ## API Usage
180
+
181
+ ```js
182
+ import { createProfiler } from 'wukong-profiler'
183
+
184
+ const profiler = createProfiler({ enabled: true, flame: true })
185
+
186
+ // Measure a function
187
+ await profiler.measure('heavyTask', async () => {
188
+ await doHeavyWork()
189
+ })
190
+
191
+ // Nested steps
192
+ await profiler.measure('outer', async () => {
193
+ await profiler.measure('inner1', task1)
194
+ await profiler.measure('inner2', task2)
195
+ })
196
+
197
+ const { total, events, exitCode } = profiler.end('Total')
198
+ console.log('Total time:', total, 'ms')
199
+ ````
200
+
201
+ - `measure(name, fn)` : measure a function (sync/async)
202
+ - `step(name)` : manually log a step
203
+ - `end(label)` : end profiling, optionally export Chrome Trace JSON
204
+
205
+ ---
206
+
207
+ **Features:**
208
+
209
+ - Nested steps for Flame Graph visualization
210
+ - Slow steps (> threshold) marked as HOT šŸ”„
211
+ - Automatic exit code for CI if HOT steps detected
212
+ - Chrome Trace export compatible with Chrome's `chrome://tracing`
213
+ - Profile diff for performance regression detection
214
+
215
+ ## šŸ” å…³é”®čÆ
216
+
217
+ <!--
218
+ Node.js profiler, JavaScript profiler, Node performance analysis, CLI profiler,
219
+ Flame Graph, Flame Chart, Chrome Trace, Chrome tracing, Perfetto,
220
+ Performance regression detection, Profile diff, CI performance check,
221
+ HOT path detection, Slow function detection,
222
+ Async performance profiling, Nested performance steps,
223
+ Node.js benchmarking, Build performance monitoring,
224
+ Developer tooling, DevOps performance, Continuous Integration profiling
225
+ -->
package/README.zh.md ADDED
@@ -0,0 +1,242 @@
1
+ # wukong-profiler
2
+
3
+ šŸ”„ **é«˜ę€§čƒ½ Node / CLI ę€§čƒ½åˆ†ęžå™Øļ¼ˆProfiler)**ļ¼Œę”ÆęŒļ¼š
4
+
5
+ - āœ… **ēœŸę­£ēš„åµŒå„—ę­„éŖ¤**ļ¼ˆēœŸå®ž Flame Graph ē»“ęž„ļ¼‰
6
+
7
+ - āœ… **Chrome Trace 导出**(`--trace trace.json`)
8
+
9
+ - āœ… **HOT 歄骤检测**ļ¼ˆę”ÆęŒ CI ē›“ęŽ„å¤±č“„ļ¼‰
10
+
11
+ - āœ… **Profile Diff**ļ¼ˆē”ØäŗŽę€§čƒ½å›žé€€ / å›žå½’ę£€ęµ‹ļ¼‰
12
+
13
+ ---
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ npm install wukong-profiler
19
+ ```
20
+
21
+ ęˆ–ē›“ęŽ„ä½æē”Ø `npx`:
22
+
23
+ ```bash
24
+ npx wukong-profiler [options]
25
+ ```
26
+
27
+ ---
28
+
29
+ ## CLI ä½æē”Øę–¹å¼
30
+
31
+ ```bash
32
+ npx wukong-profiler --flame --trace trace.json --hot-threshold 0.8 --fail-on-hot
33
+ ```
34
+
35
+ ### 运蔌 Profiler
36
+
37
+ ```bash
38
+ # ē®€å•čæč”Œ
39
+ npx wukong-profiler --flame --trace trace.json
40
+
41
+ # 设置 HOT 阈值
42
+ npx wukong-profiler --hot-threshold 0.8 --fail-on-hot
43
+
44
+ # ä½æē”ØåŸŗå‡† profile ę£€ęµ‹ę€§čƒ½å›žå½’
45
+ npx wukong-profiler --diff-base baseline.json --diff-threshold 0.2
46
+ ```
47
+
48
+ ### ē”Ÿęˆ HTML ęŠ„å‘Š
49
+
50
+ ````bash
51
+ # 从 profile.json ē”Ÿęˆ HTML ęŠ„å‘Š
52
+ npx wukong-profiler report ./profile.json
53
+
54
+ # ē”Ÿęˆå¹¶č‡ŖåŠØåœØęµč§ˆå™Øę‰“å¼€
55
+ npx wukong-profiler report ./profile.json --open
56
+
57
+ # ęŒ‡å®šč¾“å‡ŗ HTML ꖇ件
58
+ npx wukong-profiler report ./profile.json -o my-report.html
59
+
60
+ ### CLI å‚ę•°čÆ“ę˜Ž
61
+
62
+ | å‚ę•° | čÆ“ę˜Ž |
63
+ | ---------------------- | --------------------------------------------- |
64
+ | `--profile` | äæå­˜ profile JSON ę–‡ä»¶ļ¼Œē”ØäŗŽåŽē»­åˆ†ęž |
65
+ | `--flame` | åœØęŽ§åˆ¶å°č¾“å‡ŗ Flame é£Žę ¼ēš„ę ‘ēŠ¶ē»“ęžœ |
66
+ | `--trace <file>` | 导出 Chrome Trace JSON ꖇ件 |
67
+ | `--hot-threshold <n>` | HOT ę­„éŖ¤å ęÆ”é˜ˆå€¼ļ¼ˆé»˜č®¤ļ¼š0.8) |
68
+ | `--fail-on-hot` | å¦‚ęžœå­˜åœØ HOT ę­„éŖ¤ļ¼Œčæ›ēØ‹ä»„éž 0 é€€å‡ŗļ¼ˆCI 失蓄) |
69
+ | `--diff-base <file>` | äøŽåŸŗå‡† profile åÆ¹ęÆ”ļ¼Œę£€ęµ‹ę€§čƒ½å›žé€€ |
70
+ | `--diff-threshold <n>` | ę€§čƒ½å›žé€€é˜ˆå€¼ļ¼ˆé»˜č®¤ļ¼š0.2) |
71
+ | `-v, --version` | ę˜¾ē¤ŗē‰ˆęœ¬å· |
72
+ | `-h, --help` | 显示帮助俔息 |
73
+
74
+ ---
75
+
76
+ ## ē¼–ēØ‹ę–¹å¼ļ¼ˆProgrammatic Usage)
77
+
78
+ ```js
79
+ import { createProfiler } from 'wukong-profiler'
80
+
81
+ const profiler = createProfiler({
82
+ enabled: true,
83
+ flame: true,
84
+ traceFile: 'trace.json',
85
+ hotThreshold: 0.8,
86
+ failOnHot: true,
87
+ diffBaseFile: 'baseline.json',
88
+ diffThreshold: 0.2
89
+ })
90
+
91
+ profiler.step('åŠ č½½ę•°ę®', () => {
92
+ // 重任劔
93
+ })
94
+
95
+ profiler.step('å¤„ē†ę•°ę®', () => {
96
+ // å¦äø€äøŖé‡ä»»åŠ”
97
+ })
98
+
99
+ profiler.end('ꀻ耗ꗶ')
100
+ ````
101
+
102
+ ---
103
+
104
+ ## API 文攣
105
+
106
+ ### `createProfiler(options)`
107
+
108
+ åˆ›å»ŗå¹¶čæ”å›žäø€äøŖ profiler å®žä¾‹ć€‚
109
+
110
+ #### Options å‚ę•°čÆ“ę˜Ž
111
+
112
+ | å‚ę•°å | é»˜č®¤å€¼ | čÆ“ę˜Ž |
113
+ | --------------- | ----------- | -------------------------------- |
114
+ | `enabled` | `false` | ę˜Æå¦åÆē”Ø profilerļ¼ˆč¾“å‡ŗ & JSON) |
115
+ | `verbose` | `false` | č¾“å‡ŗę›“čÆ¦ē»†ēš„ę—„åæ— |
116
+ | `flame` | `false` | 输出 Flame é£Žę ¼ēš„ę ‘ē»“ęž„ |
117
+ | `slowThreshold` | `500` | ę…¢ę­„éŖ¤é˜ˆå€¼ļ¼ˆęÆ«ē§’ļ¼‰ |
118
+ | `hotThreshold` | `0.8` | HOT ę­„éŖ¤å ęÆ”é˜ˆå€¼ |
119
+ | `traceFile` | `undefined` | Chrome Trace 输出文件 |
120
+ | `failOnHot` | `false` | ę£€ęµ‹åˆ° HOT 歄骤时 CI 失蓄 |
121
+ | `diffBaseFile` | `undefined` | ē”ØäŗŽ diff ēš„åŸŗå‡† profile |
122
+ | `diffThreshold` | `0.2` | ę€§čƒ½å›žé€€é˜ˆå€¼ |
123
+
124
+ ---
125
+
126
+ ### `profiler.step(name, fn)`
127
+
128
+ ęµ‹é‡äø€äøŖ **同歄歄骤**怂
129
+
130
+ ---
131
+
132
+ ### `profiler.measure(name, fn)`
133
+
134
+ ęµ‹é‡äø€äøŖ **åŒę­„ęˆ–å¼‚ę­„å‡½ę•°**怂
135
+
136
+ ---
137
+
138
+ ### `profiler.end(label?)`
139
+
140
+ ē»“ęŸ profilingļ¼Œå¹¶č¾“å‡ŗęœ€ē»ˆē»“ęžœć€‚
141
+
142
+ ---
143
+
144
+ ## 示例
145
+
146
+ ```bash
147
+ node examples/basic.mjs
148
+ node examples/flame.mjs
149
+ node examples/async.mjs
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Chrome Trace ä½æē”Øę–¹å¼
155
+
156
+ ```bash
157
+ node examples/basic.mjs
158
+ ```
159
+
160
+ ē„¶åŽåœØęµč§ˆå™Øäø­ę‰“å¼€ļ¼š
161
+
162
+ ```text
163
+ chrome://tracing
164
+ ```
165
+
166
+ åŠ č½½ē”Ÿęˆēš„ trace ꖇ件怂
167
+
168
+ ęˆ–ä½æē”Ø Perfettoļ¼ˆęŽØčļ¼‰ļ¼š
169
+
170
+ ```text
171
+ https://ui.perfetto.dev
172
+ ```
173
+
174
+ å°†ē”Ÿęˆēš„ trace ę–‡ä»¶ę‹–å…„å³åÆęŸ„ēœ‹ć€‚
175
+
176
+ ---
177
+
178
+ ### šŸ“Š ę€§čƒ½ę‘˜č¦ļ¼ˆTop HOT 路径)
179
+
180
+ ```js
181
+ const summary = profiler.summary({ top: 3 })
182
+ ```
183
+
184
+ ---
185
+
186
+ ## API 使用示例
187
+
188
+ ```js
189
+ import { createProfiler } from 'wukong-profiler'
190
+
191
+ const profiler = createProfiler({ enabled: true, flame: true })
192
+
193
+ // ęµ‹é‡å‡½ę•°
194
+ await profiler.measure('heavyTask', async () => {
195
+ await doHeavyWork()
196
+ })
197
+
198
+ // åµŒå„—ę­„éŖ¤
199
+ await profiler.measure('outer', async () => {
200
+ await profiler.measure('inner1', task1)
201
+ await profiler.measure('inner2', task2)
202
+ })
203
+
204
+ const { total, events, exitCode } = profiler.end('Total')
205
+ console.log('ꀻ耗ꗶ:', total, 'ms')
206
+ ```
207
+
208
+ ### API čÆ“ę˜Ž
209
+
210
+ - `measure(name, fn)`ļ¼šęµ‹é‡å‡½ę•°ļ¼ˆę”ÆęŒ sync / async)
211
+
212
+ - `step(name)`ļ¼šę‰‹åŠØč®°å½•äø€äøŖę­„éŖ¤
213
+
214
+ - `end(label)`ļ¼šē»“ęŸ profilingļ¼ŒåÆåÆ¼å‡ŗ Chrome Trace
215
+
216
+ ---
217
+
218
+ ## ę øåæƒē‰¹ę€§ę€»ē»“
219
+
220
+ - šŸ”„ **ę”ÆęŒåµŒå„—ę­„éŖ¤**ļ¼Œå¤©ē„¶é€‚åˆ Flame Graph
221
+
222
+ - šŸ”„ **慢歄骤 / HOT ę­„éŖ¤č‡ŖåŠØę ‡č®°**
223
+
224
+ - šŸ”„ **CI 友儽**ļ¼šę€§čƒ½é—®é¢˜åÆē›“ęŽ„č®©ęž„å»ŗå¤±č“„
225
+
226
+ - šŸ”„ **Chrome Trace 导出**ļ¼ŒåÆč§†åŒ–ē²¾ē”®åˆ°ęÆ«ē§’
227
+
228
+ - šŸ”„ **Profile Diff**ļ¼Œē”ØäŗŽę€§čƒ½å›žé€€ę£€ęµ‹
229
+
230
+ ---
231
+
232
+ ## šŸ” Keywords
233
+
234
+ <!--
235
+ Node.js profiler, JavaScript profiler, Node performance analysis, CLI profiler,
236
+ Flame Graph, Flame Chart, Chrome Trace, Chrome tracing, Perfetto,
237
+ Performance regression detection, Profile diff, CI performance check,
238
+ HOT path detection, Slow function detection,
239
+ Async performance profiling, Nested performance steps,
240
+ Node.js benchmarking, Build performance monitoring,
241
+ Developer tooling, DevOps performance, Continuous Integration profiling
242
+ -->
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import chalk from 'chalk'
3
+ import { Command } from 'commander'
4
+ import fs from 'fs'
5
+ import path from 'path'
6
+ import { fileURLToPath } from 'url'
7
+ import open from "open";
8
+
9
+ import { generateReport } from '../src/report.mjs'
10
+ import { formatTime } from '../src/utils/format.mjs'
11
+ import { createProfiler } from '../src/utils/profiler.mjs'
12
+
13
+ // čŽ·å– package.json ēš„ē‰ˆęœ¬å·
14
+ // 兼容 Windows
15
+ const __filename = fileURLToPath(import.meta.url)
16
+ const __dirname = path.dirname(__filename)
17
+
18
+ const pkgPath = path.resolve(__dirname, '../package.json')
19
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
20
+
21
+ const program = new Command()
22
+
23
+ program
24
+ .name('wukong-profiler')
25
+ .description(
26
+ 'šŸ”„ Node/CLI profiler with flame graph, Chrome Trace and HOT detection'
27
+ )
28
+ .version(pkg.version, '-v, --version', 'show version')
29
+
30
+ /* ------------------------------
31
+ profile / runtime options
32
+ --------------------------------*/
33
+
34
+ program
35
+ .option('--profile', 'save profile.json')
36
+ .option('--flame', 'flame-like console output')
37
+ .option('--trace <file>', 'Chrome Trace export file')
38
+ .option('--hot-threshold <n>', 'HOT step threshold', parseFloat, 0.8)
39
+ .option('--fail-on-hot', 'exit with non-zero if HOT step detected')
40
+ .option('--diff-base <file>', 'baseline profile for regression check')
41
+ .option('--diff-threshold <n>', 'regression diff threshold', parseFloat, 0.2)
42
+
43
+ /* ------------------------------
44
+ report subcommand
45
+ --------------------------------*/
46
+
47
+ program
48
+ .command('report <profile>')
49
+ .description('šŸ“Š Generate HTML report from profile.json')
50
+ .option('-o, --output <file>', 'output html file', 'wukong-report.html')
51
+ .option('--open', 'open the report in default browser after generation')
52
+ .action((profile, options) => {
53
+ // options å°±ę˜Æęœ€åŽäø€äøŖå‚ę•°ļ¼ŒåŒ…å« output
54
+ const output = options.output
55
+
56
+ // TODO: remove debug log before production
57
+ console.log('āœ…', 'output', output)
58
+
59
+ if (!fs.existsSync(profile)) {
60
+ console.error(`āŒ profile file not found: ${profile}`)
61
+ process.exit(1)
62
+ }
63
+
64
+ const json = JSON.parse(fs.readFileSync(profile, 'utf-8'))
65
+
66
+ const html = generateReport(json)
67
+ fs.writeFileSync(output, html, 'utf8')
68
+
69
+ console.log(`āœ… report generated: ${output}`)
70
+ if (options.open) {
71
+ open(output)
72
+ .then(() => console.log(`🌐 report opened in browser`))
73
+ .catch((err) => console.error('āŒ failed to open report', err))
74
+ }
75
+ })
76
+
77
+ program.parse(process.argv)
78
+
79
+ const opts = program.opts()
80
+
81
+ const profiler = createProfiler({
82
+ enabled: opts.profile || opts.flame || !!opts.trace || !!opts.diffBase,
83
+ flame: opts.flame,
84
+ traceFile: opts.trace,
85
+ hotThreshold: opts.hotThreshold,
86
+ failOnHot: opts.failOnHot,
87
+ diffBaseFile: opts.diffBase,
88
+ diffThreshold: opts.diffThreshold
89
+ })
90
+
91
+ console.log(chalk.green('šŸ”„ Wukong Profiler CLI started'))
92
+ console.log(
93
+ chalk.gray('Use profiler.step(name, fn) in your code or CLI script')
94
+ )
95
+
96
+ // 输出 total 讔时俔息
97
+ process.on('exit', () => {
98
+ const profile = profiler.end('Total')
99
+ console.log(
100
+ chalk.cyan('Profiler finished. Total:'),
101
+ formatTime(profile.total)
102
+ )
103
+ })
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "wukong-profiler",
3
+ "version": "1.0.0",
4
+ "description": "šŸ”„ é«˜ę€§čƒ½ CLI/Node Profilerļ¼Œę”ÆęŒ Flame Graph态Chrome Trace态HOT ę­„éŖ¤ę£€ęµ‹ć€ę€§čƒ½å›žå½’åˆ†ęž",
5
+ "keywords": [
6
+ "profiler",
7
+ "flamegraph",
8
+ "node",
9
+ "cli",
10
+ "performance",
11
+ "hotstep",
12
+ "chrome-trace",
13
+ "ci"
14
+ ],
15
+ "homepage": "https://tomatobybike.github.io/md-structure/",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/tomatobybike/md-structure"
19
+ },
20
+ "license": "MIT",
21
+ "author": "Tom <tomatobybike@gmail.com>",
22
+ "type": "module",
23
+ "imports": {
24
+ "#src/*": "./src/*",
25
+ "#utils/*": "src/utils/*",
26
+ "source-map": "./test/__mocks__/source-map.mjs"
27
+ },
28
+ "main": "src/index.mjs",
29
+ "bin": {
30
+ "wukong-profiler": "./bin/wukong-profiler"
31
+ },
32
+ "files": [
33
+ "bin/",
34
+ "src/",
35
+ "README.md",
36
+ "README.zh.md"
37
+ ],
38
+ "scripts": {
39
+ "async-example": "node examples/async.mjs",
40
+ "basic-example": "node examples/basic.mjs",
41
+ "check:npm:login": "node scripts/check-npm-login.mjs",
42
+ "flame-example": "node examples/flame.mjs",
43
+ "format": "prettier --write \"src/**/*.{js,mjs}\"",
44
+ "lint": "eslint src --ext .js,.mjs src",
45
+ "lint:fix": "eslint src --ext .js,.mjs --fix",
46
+ "pack": "npm pack",
47
+ "prepare": "husky install",
48
+ "prepublishOnly": "yarn lint",
49
+ "prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
50
+ "push:tags": "git push origin main --follow-tags",
51
+ "prerelease": "yarn lint",
52
+ "release": "yarn check:npm:login && npm publish && yarn push:tags",
53
+ "release:rollback": "git reset --hard HEAD~1 && git tag -d $(git tag --points-at HEAD~1)",
54
+ "release:major": "yarn lint && standard-version --release-as major",
55
+ "release:minor": "yarn lint && standard-version --release-as minor",
56
+ "release:patch": "yarn lint && standard-version --release-as patch",
57
+ "report": "wukong-profiler report ./profile.json -o my-report.html --open",
58
+ "sort": "sort-package-json",
59
+ "test": "node --test"
60
+ },
61
+ "husky": {
62
+ "hooks": {
63
+ "pre-commit": "lint-staged"
64
+ }
65
+ },
66
+ "lint-staged": {
67
+ "package.json": [
68
+ "sort-package-json"
69
+ ],
70
+ "src/**/*.js": [
71
+ "prettier --write",
72
+ "eslint --fix"
73
+ ]
74
+ },
75
+ "dependencies": {
76
+ "chalk": "^5.3.0",
77
+ "commander": "^11.0.0",
78
+ "open": "^11.0.0"
79
+ },
80
+ "devDependencies": {
81
+ "@trivago/prettier-plugin-sort-imports": "5.2.2",
82
+ "eslint": "8.57.1",
83
+ "eslint-config-airbnb-base": "15.0.0",
84
+ "eslint-config-prettier": "10.1.8",
85
+ "eslint-import-resolver-alias": "1.1.2",
86
+ "eslint-plugin-import": "2.32.0",
87
+ "eslint-plugin-prettier": "5.5.4",
88
+ "eslint-plugin-simple-import-sort": "12.1.1",
89
+ "husky": "9.1.7",
90
+ "lint-staged": "16.2.7",
91
+ "prettier": "3.6.2",
92
+ "prettier-plugin-packagejson": "2.5.19",
93
+ "sort-package-json": "3.4.0",
94
+ "standard-version": "9.5.0"
95
+ },
96
+ "packageManager": "yarn@1.22.22",
97
+ "engines": {
98
+ "node": ">=18.0.0"
99
+ }
100
+ }
@@ -0,0 +1,56 @@
1
+ // testFunction.mjs
2
+ import { measure, createProfiler } from 'wukong-profiler'
3
+ import fs from 'fs'
4
+
5
+ // åˆ›å»ŗ Profilerļ¼ŒåÆå…Øå±€é…ē½®
6
+ const profiler = createProfiler({
7
+ enabled: true,
8
+ verbose: true,
9
+ flame: true,
10
+ trace: 'trace.json',
11
+ hotThreshold: 0.8,
12
+ failOnHot: true
13
+ })
14
+
15
+ // 被测试函数
16
+ function heavyComputation(n) {
17
+ let sum = 0
18
+ for (let i = 0; i < n * 1e6; i++) sum += i
19
+ return sum
20
+ }
21
+
22
+ function nestedComputation(n) {
23
+ return measure('nestedComputation', () => {
24
+ const a = measure('heavyComputation', () => heavyComputation(n))
25
+ const b = measure('heavyComputation_half', () => heavyComputation(n / 2))
26
+ return a + b
27
+ })
28
+ }
29
+
30
+ // === ęµ‹čÆ•å…„å£ ===
31
+ measure('Test: heavyComputation', () => heavyComputation(5))
32
+ measure('Test: nestedComputation', () => nestedComputation(3))
33
+
34
+ // å®Œęˆå¹¶č¾“å‡ŗę€»č€—ę—¶
35
+ const result = profiler.end('Total Test')
36
+
37
+ // 输出 Chrome Trace
38
+ if (profiler.traceFile) {
39
+ const traceEvents = result.events.map(ev => ({
40
+ name: ev.name,
41
+ ph: 'X',
42
+ ts: Math.round(ev.sinceStart * 1000), // μs
43
+ dur: Math.round(ev.duration * 1000), // μs
44
+ pid: 1,
45
+ tid: 1
46
+ }))
47
+ fs.writeFileSync(profiler.traceFile, JSON.stringify({ traceEvents }, null, 2))
48
+ console.log(`Chrome Trace å·²ē”Ÿęˆ: ${profiler.traceFile}`)
49
+ }
50
+
51
+ // HOT ę­„éŖ¤č§¦å‘ CI éžé›¶é€€å‡ŗē 
52
+ const anyHot = result.events.some(ev => ev.hot)
53
+ if (anyHot && profiler.failOnHot) {
54
+ console.error('šŸ”„ HOT step detected! Exiting with code 1.')
55
+ process.exit(1)
56
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,12 @@
1
+ import { createProfiler } from './utils/profiler.mjs'
2
+ import { formatTime, makeBar } from './utils/format.mjs'
3
+ import { exportChromeTrace } from './utils/trace.mjs'
4
+ import { diffProfiles } from './utils/diff.mjs'
5
+
6
+ export {
7
+ createProfiler,
8
+ formatTime,
9
+ makeBar,
10
+ exportChromeTrace,
11
+ diffProfiles
12
+ }
package/src/report.mjs ADDED
@@ -0,0 +1,50 @@
1
+ // report.mjs
2
+ export const generateReport = (profile) => {
3
+ const rows = profile.events.map(e => `
4
+ <tr class="${e.duration / profile.total > 0.8 ? 'hot' : ''}">
5
+ <td>${e.name}</td>
6
+ <td>${e.duration.toFixed(2)} ms</td>
7
+ <td>${((e.duration / profile.total) * 100).toFixed(1)}%</td>
8
+ <td>${
9
+ e.source
10
+ ? e.source.original
11
+ ? `${e.source.original.source}:${e.source.original.line}`
12
+ : `${e.source.file}:${e.source.line}`
13
+ : ''
14
+ }</td>
15
+ </tr>`).join('')
16
+
17
+ const html = `<!doctype html>
18
+ <html>
19
+ <head>
20
+ <meta charset="utf-8"/>
21
+ <title>Wukong Profiler Report</title>
22
+ <style>
23
+ body { font-family: system-ui; padding: 20px; }
24
+ table { border-collapse: collapse; width: 100%; }
25
+ th, td { padding: 8px; border-bottom: 1px solid #eee; }
26
+ .hot { background: #ffe5e5; }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <h1>šŸ”„ Wukong Profiler Report</h1>
31
+ <p>Total: ${profile.total.toFixed(2)} ms</p>
32
+
33
+ <table>
34
+ <thead>
35
+ <tr>
36
+ <th>Step</th>
37
+ <th>Duration</th>
38
+ <th>Ratio</th>
39
+ <th>Source</th>
40
+ </tr>
41
+ </thead>
42
+ <tbody>
43
+ ${rows}
44
+ </tbody>
45
+ </table>
46
+ </body>
47
+ </html>`
48
+
49
+ return html
50
+ }
@@ -0,0 +1,21 @@
1
+ export const diffProfiles = (prev, curr, threshold = 0.2) => {
2
+ const map = new Map()
3
+ prev.events.forEach(e => map.set(e.name, e.duration))
4
+ const regressions = []
5
+
6
+ curr.events.forEach(e => {
7
+ const before = map.get(e.name)
8
+ if (!before) return
9
+ const diff = (e.duration - before) / before
10
+ if (diff >= threshold) {
11
+ regressions.push({
12
+ name: e.name,
13
+ before,
14
+ after: e.duration,
15
+ diff
16
+ })
17
+ }
18
+ })
19
+
20
+ return regressions
21
+ }
@@ -0,0 +1,11 @@
1
+ export const formatTime = (ms) => {
2
+ if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
3
+ if (ms < 1000) return `${ms.toFixed(2)} ms`
4
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
5
+ return `${(ms / 60000).toFixed(2)} min`
6
+ }
7
+
8
+ export const makeBar = (ratio, width = 32) => {
9
+ const len = Math.max(1, Math.round(ratio * width))
10
+ return 'ā–ˆ'.repeat(len)
11
+ }
@@ -0,0 +1,281 @@
1
+ import chalk from 'chalk'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ import { diffProfiles } from './diff.mjs'
7
+ import { formatTime, makeBar } from './format.mjs'
8
+ import { exportChromeTrace } from './trace.mjs'
9
+
10
+ /* ----------------------------------
11
+ Optional sourcemap resolver
12
+ ----------------------------------- */
13
+ let SourceMapConsumer = null
14
+ try {
15
+ // optional dependency
16
+ const sm = await import('source-map')
17
+ SourceMapConsumer = sm.SourceMapConsumer
18
+ } catch {
19
+ // ignore – sourcemap is optional
20
+ }
21
+
22
+ const __filename = fileURLToPath(import.meta.url)
23
+ const __dirname = path.dirname(__filename)
24
+
25
+ export const createProfiler = ({
26
+ enabled = false,
27
+ verbose = false,
28
+ flame = false,
29
+ slowThreshold = 500,
30
+ hotThreshold = 0.8,
31
+ traceFile,
32
+ failOnHot = false,
33
+ diffBaseFile,
34
+ diffThreshold = 0.2,
35
+ captureSource = true // šŸ”„ new
36
+ } = {}) => {
37
+ const start = process.hrtime.bigint()
38
+ const stack = []
39
+ const events = []
40
+
41
+ const toMs = (a, b) => Number(b - a) / 1e6
42
+
43
+ /* ----------------------------------
44
+ Stack + sourcemap capture
45
+ ----------------------------------- */
46
+ const captureSourceInfo = async () => {
47
+ const err = new Error()
48
+ const frames = err.stack
49
+ ?.split('\n')
50
+ .slice(2)
51
+ .map((l) => l.trim())
52
+
53
+ if (!frames?.length) return null
54
+
55
+ const top = frames[0]
56
+
57
+ const match =
58
+ top.match(/\((.*):(\d+):(\d+)\)/) ||
59
+ top.match(/at (.*):(\d+):(\d+)/)
60
+
61
+ if (!match) return { raw: top }
62
+
63
+ const [, file, line, column] = match
64
+ const source = {
65
+ file,
66
+ line: Number(line),
67
+ column: Number(column)
68
+ }
69
+
70
+ if (!captureSource || !SourceMapConsumer) return source
71
+
72
+ try {
73
+ const mapFile = `${file}.map`
74
+ if (!fs.existsSync(mapFile)) return source
75
+
76
+ const rawMap = JSON.parse(fs.readFileSync(mapFile, 'utf8'))
77
+ const consumer = await new SourceMapConsumer(rawMap)
78
+
79
+ const pos = consumer.originalPositionFor({
80
+ line: source.line,
81
+ column: source.column
82
+ })
83
+
84
+ consumer.destroy?.()
85
+
86
+ if (pos?.source) {
87
+ return {
88
+ ...source,
89
+ original: {
90
+ source: pos.source,
91
+ line: pos.line,
92
+ column: pos.column,
93
+ name: pos.name
94
+ }
95
+ }
96
+ }
97
+ } catch {
98
+ /* silent */
99
+ }
100
+
101
+ return source
102
+ }
103
+
104
+ /* ----------------------------------
105
+ STEP / MEASURE
106
+ ----------------------------------- */
107
+ const step = (name, fn) => {
108
+ const parent = stack[stack.length - 1]
109
+ const startTime = process.hrtime.bigint()
110
+
111
+ const node = {
112
+ name,
113
+ start: toMs(start, startTime),
114
+ duration: 0,
115
+ depth: stack.length,
116
+ children: [],
117
+ source: null
118
+ }
119
+
120
+ if (parent) parent.children.push(node)
121
+ stack.push(node)
122
+
123
+ const finish = async () => {
124
+ const endTime = process.hrtime.bigint()
125
+ node.duration = toMs(startTime, endTime)
126
+ events.push(node)
127
+ stack.pop()
128
+
129
+ const totalSoFar = toMs(start, endTime)
130
+ const isSlow = node.duration >= slowThreshold
131
+ const isHot = totalSoFar > 0 && node.duration / totalSoFar >= hotThreshold
132
+
133
+ if ((isSlow || isHot) && captureSource) {
134
+ node.source = await captureSourceInfo()
135
+ }
136
+ }
137
+
138
+ try {
139
+ const result = fn?.()
140
+ if (result && typeof result.then === 'function') {
141
+ return result.finally(finish)
142
+ }
143
+ finish()
144
+ return result
145
+ } catch (err) {
146
+ finish()
147
+ throw err
148
+ }
149
+ }
150
+
151
+ const measure = (name, fn) => step(name, fn)
152
+
153
+ /* ----------------------------------
154
+ END
155
+ ----------------------------------- */
156
+ const end = (label = 'Total') => {
157
+ const total = toMs(start, process.hrtime.bigint())
158
+
159
+ let hasHot = false
160
+ for (const e of events) {
161
+ if (e.duration / total >= hotThreshold) {
162
+ hasHot = true
163
+ }
164
+ }
165
+
166
+ if (enabled || verbose) {
167
+ console.log(
168
+ chalk.cyan('ā±'),
169
+ chalk.bold(label),
170
+ chalk.yellow(formatTime(total))
171
+ )
172
+
173
+ if (flame) {
174
+ for (const e of events) {
175
+ const ratio = e.duration / total
176
+ const hot = ratio >= hotThreshold
177
+ const slow = e.duration >= slowThreshold
178
+
179
+ console.log(
180
+ chalk.gray(`${' '.repeat(e.depth)}ā”œā”€`),
181
+ chalk.white(e.name.padEnd(22)),
182
+ chalk.yellow(formatTime(e.duration)),
183
+ chalk.gray(makeBar(ratio)),
184
+ hot
185
+ ? chalk.red.bold(' šŸ”„ HOT')
186
+ : slow
187
+ ? chalk.yellow(' ⚠ SLOW')
188
+ : ''
189
+ )
190
+
191
+ if ((hot || slow) && e.source) {
192
+ console.log(
193
+ chalk.gray(
194
+ ` ↳ ${e.source.original?.source || e.source.file}:${
195
+ e.source.original?.line || e.source.line
196
+ }`
197
+ )
198
+ )
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ const profile = { total, events }
205
+
206
+ if (traceFile) exportChromeTrace(events, traceFile)
207
+
208
+ if (diffBaseFile && fs.existsSync(diffBaseFile)) {
209
+ const base = JSON.parse(fs.readFileSync(diffBaseFile, 'utf8'))
210
+ const regressions = diffProfiles(base, profile, diffThreshold)
211
+
212
+ if (regressions.length) {
213
+ if (enabled || verbose) {
214
+ console.log(chalk.red('\n⚠ Performance Regression Detected:\n'))
215
+ regressions.forEach((r) =>
216
+ console.log(
217
+ chalk.red(
218
+ ` ${r.name}: ${formatTime(r.before)} → ${formatTime(
219
+ r.after
220
+ )} (+${(r.diff * 100).toFixed(1)}%)`
221
+ )
222
+ )
223
+ )
224
+ }
225
+ process.exitCode = 1
226
+ }
227
+ }
228
+
229
+ if (enabled) {
230
+ fs.writeFileSync('profile.json', JSON.stringify(profile, null, 2))
231
+ }
232
+
233
+ if (failOnHot && hasHot) {
234
+ if (enabled || verbose) {
235
+ console.log(chalk.red('\nšŸ”„ HOT step detected — failing CI'))
236
+ }
237
+ process.exitCode = 1
238
+ }
239
+
240
+ return profile
241
+ }
242
+
243
+ /* ----------------------------------
244
+ SUMMARY
245
+ ----------------------------------- */
246
+ const flatten = (nodes, parentPath = []) => {
247
+ let res = []
248
+ for (const n of nodes) {
249
+ const pathArr = [...parentPath, n.name]
250
+ res.push({
251
+ name: n.name,
252
+ path: pathArr.join(' → '),
253
+ duration: n.duration,
254
+ depth: n.depth,
255
+ source: n.source
256
+ })
257
+ if (n.children?.length) {
258
+ res = res.concat(flatten(n.children, pathArr))
259
+ }
260
+ }
261
+ return res
262
+ }
263
+
264
+ // eslint-disable-next-line no-shadow
265
+ const summary = ({ top = 5, hotThreshold = 0.8 } = {}) => {
266
+ const total = toMs(start, process.hrtime.bigint())
267
+
268
+ const flat = flatten(events)
269
+ .map((e) => ({
270
+ ...e,
271
+ ratio: e.duration / total,
272
+ hot: e.duration / total >= hotThreshold
273
+ }))
274
+ .sort((a, b) => b.duration - a.duration)
275
+ .slice(0, top)
276
+
277
+ return { total, top: flat }
278
+ }
279
+
280
+ return { step, measure, end, summary }
281
+ }
@@ -0,0 +1,40 @@
1
+ import fs from 'fs'
2
+
3
+ export const exportChromeTrace = (events, file) => {
4
+ const traceEvents = []
5
+ const pid = 1
6
+ const tid = 1
7
+
8
+ const walk = (nodes) => {
9
+ for (const n of nodes) {
10
+ traceEvents.push({
11
+ name: n.name,
12
+ cat: 'wukong',
13
+ ph: 'X', // complete event
14
+ ts: Math.round(n.start * 1000), // ms → μs
15
+ dur: Math.round(n.duration * 1000),
16
+ pid,
17
+ tid,
18
+ args: {
19
+ duration_ms: n.duration,
20
+ depth: n.depth,
21
+ ...(n.source && {
22
+ source: n.source.original
23
+ ? `${n.source.original.source}:${n.source.original.line}`
24
+ : `${n.source.file}:${n.source.line}`
25
+ })
26
+ }
27
+ })
28
+
29
+ if (n.children?.length) walk(n.children)
30
+ }
31
+ }
32
+
33
+ walk(events)
34
+
35
+ fs.writeFileSync(
36
+ file,
37
+ JSON.stringify({ traceEvents }, null, 2),
38
+ 'utf8'
39
+ )
40
+ }