wukong-profiler 1.0.3 → 1.0.5
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/README.md +19 -1
- package/README.zh-CN.md +0 -2
- package/bin/wukong-profiler +0 -0
- package/package.json +7 -7
- package/src/api/testFunction.js +56 -56
- package/src/index.mjs +6 -12
- package/src/report.mjs +6 -2
- package/src/utils/diff.mjs +21 -21
- package/src/utils/format.mjs +11 -11
- package/src/utils/profiler.mjs +1 -2
package/README.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# wukong-profiler
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/wukong-profiler">
|
|
5
|
+
<img src="https://img.shields.io/npm/v/wukong-profiler.svg" alt="npm version">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/wukong-profiler">
|
|
8
|
+
<img src="https://img.shields.io/npm/dm/wukong-profiler.svg" alt="downloads">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://github.com/tomatobybike/wukong-profiler/blob/master/LICENSE">
|
|
11
|
+
<img src="https://img.shields.io/github/license/tomatobybike/wukong-profiler.svg" alt="license">
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://github.com/tomatobybike/wukong-profiler">
|
|
14
|
+
<img src="https://img.shields.io/github/stars/tomatobybike/wukong-profiler.svg?style=social" alt="GitHub stars">
|
|
15
|
+
</a>
|
|
16
|
+
<a href="https://github.com/tomatobybike/wukong-profiler/issues">
|
|
17
|
+
<img src="https://img.shields.io/github/issues/tomatobybike/wukong-profiler.svg" alt="issues">
|
|
18
|
+
</a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
3
21
|
🔥 High-performance Node/CLI profiler supporting:
|
|
4
22
|
|
|
5
23
|
- Nested steps (true Flame Graph)
|
|
@@ -216,7 +234,7 @@ console.log('Total time:', total, 'ms')
|
|
|
216
234
|
- Chrome Trace export compatible with Chrome's `chrome://tracing`
|
|
217
235
|
- Profile diff for performance regression detection
|
|
218
236
|
|
|
219
|
-
|
|
237
|
+
|
|
220
238
|
|
|
221
239
|
<!--
|
|
222
240
|
Node.js profiler, JavaScript profiler, Node performance analysis, CLI profiler,
|
package/README.zh-CN.md
CHANGED
package/bin/wukong-profiler
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wukong-profiler",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "🔥 高性能 CLI/Node Profiler,支持 Flame Graph、Chrome Trace、HOT 步骤检测、性能回归分析",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"profiler",
|
|
@@ -73,12 +73,12 @@
|
|
|
73
73
|
]
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"chalk": "^5.
|
|
77
|
-
"commander": "^
|
|
76
|
+
"chalk": "^5.6.2",
|
|
77
|
+
"commander": "^14.0.2",
|
|
78
78
|
"open": "^11.0.0"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@trivago/prettier-plugin-sort-imports": "
|
|
81
|
+
"@trivago/prettier-plugin-sort-imports": "6.0.0",
|
|
82
82
|
"eslint": "8.57.1",
|
|
83
83
|
"eslint-config-airbnb-base": "15.0.0",
|
|
84
84
|
"eslint-config-prettier": "10.1.8",
|
|
@@ -88,9 +88,9 @@
|
|
|
88
88
|
"eslint-plugin-simple-import-sort": "12.1.1",
|
|
89
89
|
"husky": "9.1.7",
|
|
90
90
|
"lint-staged": "16.2.7",
|
|
91
|
-
"prettier": "3.
|
|
92
|
-
"prettier-plugin-packagejson": "2.5.
|
|
93
|
-
"sort-package-json": "3.
|
|
91
|
+
"prettier": "3.7.4",
|
|
92
|
+
"prettier-plugin-packagejson": "2.5.20",
|
|
93
|
+
"sort-package-json": "3.6.0",
|
|
94
94
|
"standard-version": "9.5.0"
|
|
95
95
|
},
|
|
96
96
|
"packageManager": "yarn@1.22.22",
|
package/src/api/testFunction.js
CHANGED
|
@@ -1,56 +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
|
-
}
|
|
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
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { formatTime, makeBar } from './utils/format.mjs'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
export {
|
|
7
|
-
createProfiler,
|
|
8
|
-
formatTime,
|
|
9
|
-
makeBar,
|
|
10
|
-
exportChromeTrace,
|
|
11
|
-
diffProfiles
|
|
12
|
-
}
|
|
1
|
+
import { diffProfiles } from './utils/diff.mjs'
|
|
2
|
+
import { formatTime, makeBar } from './utils/format.mjs'
|
|
3
|
+
import { createProfiler } from './utils/profiler.mjs'
|
|
4
|
+
import { exportChromeTrace } from './utils/trace.mjs'
|
|
5
|
+
|
|
6
|
+
export { createProfiler, formatTime, makeBar, exportChromeTrace, diffProfiles }
|
package/src/report.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// report.mjs
|
|
2
2
|
export const generateReport = (profile) => {
|
|
3
|
-
const rows = profile.events
|
|
3
|
+
const rows = profile.events
|
|
4
|
+
.map(
|
|
5
|
+
(e) => `
|
|
4
6
|
<tr class="${e.duration / profile.total > 0.8 ? 'hot' : ''}">
|
|
5
7
|
<td>${e.name}</td>
|
|
6
8
|
<td>${e.duration.toFixed(2)} ms</td>
|
|
@@ -12,7 +14,9 @@ export const generateReport = (profile) => {
|
|
|
12
14
|
: `${e.source.file}:${e.source.line}`
|
|
13
15
|
: ''
|
|
14
16
|
}</td>
|
|
15
|
-
</tr>`
|
|
17
|
+
</tr>`
|
|
18
|
+
)
|
|
19
|
+
.join('')
|
|
16
20
|
|
|
17
21
|
const html = `<!doctype html>
|
|
18
22
|
<html>
|
package/src/utils/diff.mjs
CHANGED
|
@@ -1,21 +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
|
-
}
|
|
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
|
+
}
|
package/src/utils/format.mjs
CHANGED
|
@@ -1,11 +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
|
-
}
|
|
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
|
+
}
|
package/src/utils/profiler.mjs
CHANGED
|
@@ -55,8 +55,7 @@ export const createProfiler = ({
|
|
|
55
55
|
const top = frames[0]
|
|
56
56
|
|
|
57
57
|
const match =
|
|
58
|
-
top.match(/\((.*):(\d+):(\d+)\)/) ||
|
|
59
|
-
top.match(/at (.*):(\d+):(\d+)/)
|
|
58
|
+
top.match(/\((.*):(\d+):(\d+)\)/) || top.match(/at (.*):(\d+):(\d+)/)
|
|
60
59
|
|
|
61
60
|
if (!match) return { raw: top }
|
|
62
61
|
|