wukong-gitlog-cli 1.0.37 → 1.0.38
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/.eslintrc +1 -0
- package/CHANGELOG.md +11 -0
- package/package.json +18 -15
- package/src/index.mjs +55 -7
- package/src/utils/handleSuccess.mjs +1 -4
- package/src/utils/profiler/diff.mjs +26 -0
- package/src/utils/profiler/format.mjs +11 -0
- package/src/utils/profiler/index.mjs +144 -0
- package/src/utils/profiler/trace.mjs +26 -0
- package/src/utils/profiler.mjs +101 -0
- package/src/utils/time/scopeTimer.mjs +37 -0
- package/src/utils/time/timer.mjs +33 -0
- package/src/utils/time/withTimer.mjs +11 -0
package/.eslintrc
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.0.38](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.37...v1.0.38) (2025-12-27)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 🎸 增加性能分析 ([ec925cc](https://github.com/tomatobybike/wukong-gitlog-cli/commit/ec925cc3d20be2a0eee6e20aaee99b4449eff9be))
|
|
11
|
+
* 🎸 flame 和 显示 flame-like 日志 ([7b1a00c](https://github.com/tomatobybike/wukong-gitlog-cli/commit/7b1a00c26afa1757637e6c4a3a2be92d74e0ad65))
|
|
12
|
+
* 🎸 ignore ([eb3e5bc](https://github.com/tomatobybike/wukong-gitlog-cli/commit/eb3e5bc89183e0722056036f14f74806e3c3b1e3))
|
|
13
|
+
* 🎸 profiler ([48fd0a5](https://github.com/tomatobybike/wukong-gitlog-cli/commit/48fd0a5a8b25b55bbca7bbdbd2df11158049adcd))
|
|
14
|
+
* 🎸 Profiler ([a7f4623](https://github.com/tomatobybike/wukong-gitlog-cli/commit/a7f4623a162947d31f25702052f54a4ff0406fcb))
|
|
15
|
+
|
|
5
16
|
### [1.0.37](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.36...v1.0.37) (2025-12-12)
|
|
6
17
|
|
|
7
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wukong-gitlog-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.38",
|
|
4
4
|
"description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -37,40 +37,42 @@
|
|
|
37
37
|
"wukong-gitlog-cli": "./bin/wukong-gitlog-cli"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
+
"build": "node scripts/esbuild.config.mjs",
|
|
41
|
+
"check:npm:login": "node scripts/check-npm-login.mjs",
|
|
40
42
|
"cli:excel-demo": "node ./src/cli.mjs --overtime --format excel --stats --limit 5 --out commits.xlsx",
|
|
41
43
|
"cli:excel-demo-parent": "node ./src/cli.mjs --out-parent --format excel --stats --limit 5 --out commits-parent.xlsx",
|
|
42
44
|
"cli:gerrit-changeid-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{changeId}}\" --limit 5 --out commits-gerrit-changeid.txt",
|
|
43
45
|
"cli:gerrit-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{hash}}\" --limit 5 --out commits-gerrit.txt",
|
|
44
46
|
"cli:help": "node ./src/cli.mjs --help",
|
|
45
47
|
"cli:json-demo": "node ./src/cli.mjs --json --limit 5 --out commits.json",
|
|
46
|
-
"cli:text-demo": "node ./src/cli.mjs --format text --limit 5 --out commits.txt",
|
|
47
|
-
"cli:text-demo-parent": "node ./src/cli.mjs --out-parent --format text --limit 5 --out commits-parent.txt",
|
|
48
48
|
"cli:overtime-demo": "node ./src/cli.mjs --overtime --format text --limit 200",
|
|
49
|
-
"cli:overtime-text": "node ./src/cli.mjs --overtime --limit 100 --format text --out commits.txt",
|
|
50
49
|
"cli:overtime-excel": "node ./src/cli.mjs --overtime --limit 50 --format excel --out commits.xlsx",
|
|
51
|
-
"cli:overtime-text-us": "node ./src/cli.mjs --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
52
|
-
"cli:overtime-text-us-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
53
|
-
"cli:overtime-text-us-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
54
50
|
"cli:overtime-excel-cn": "node ./src/cli.mjs --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
55
|
-
"cli:overtime-excel-cn-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
56
51
|
"cli:overtime-excel-cn-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
52
|
+
"cli:overtime-excel-cn-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
57
53
|
"cli:overtime-per-period-csv-tab": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab",
|
|
58
|
-
"cli:overtime-per-period-xlsx-sheets": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-excel-mode sheets",
|
|
59
|
-
"cli:overtime-per-period-xlsx-files": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats xlsx --per-period-excel-mode files",
|
|
60
54
|
"cli:overtime-per-period-only": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-only",
|
|
55
|
+
"cli:overtime-per-period-xlsx-files": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats xlsx --per-period-excel-mode files",
|
|
56
|
+
"cli:overtime-per-period-xlsx-sheets": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-excel-mode sheets",
|
|
57
|
+
"cli:overtime-text": "node ./src/cli.mjs --overtime --limit 100 --format text --out commits.txt",
|
|
58
|
+
"cli:overtime-text-us": "node ./src/cli.mjs --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
59
|
+
"cli:overtime-text-us-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
60
|
+
"cli:overtime-text-us-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
61
61
|
"cli:serve": "node ./src/cli.mjs --serve --overtime --format text --out commits.txt",
|
|
62
62
|
"cli:serve-only": "node ./src/cli.mjs --serve-only",
|
|
63
|
+
"cli:text-demo": "node ./src/cli.mjs --format text --limit 5 --out commits.txt",
|
|
64
|
+
"cli:text-demo-parent": "node ./src/cli.mjs --out-parent --format text --limit 5 --out commits-parent.txt",
|
|
63
65
|
"format": "prettier --write \"src/**/*.{js,mjs}\"",
|
|
64
66
|
"lint": "eslint src --ext .js,.mjs src",
|
|
65
67
|
"lint:fix": "eslint src --ext .js,.mjs --fix",
|
|
66
68
|
"pack": "npm pack",
|
|
67
|
-
"build": "node scripts/esbuild.config.mjs",
|
|
68
69
|
"prepare": "husky install",
|
|
69
|
-
"prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
|
|
70
70
|
"prepublishOnly": "yarn lint && yarn build",
|
|
71
|
-
"
|
|
72
|
-
"release": "yarn check:npm:login && yarn release:patch && yarn push:tags && npm publish",
|
|
71
|
+
"prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
|
|
73
72
|
"push:tags": "git push origin main --follow-tags",
|
|
73
|
+
"prerelease": "yarn lint",
|
|
74
|
+
"release": "yarn check:npm:login && yarn release:patch && npm publish && yarn push:tags",
|
|
75
|
+
"release:rollback": "git reset --hard HEAD~1 && git tag -d $(git tag --points-at HEAD~1)",
|
|
74
76
|
"release:major": "yarn lint && standard-version --release-as major",
|
|
75
77
|
"release:minor": "yarn lint && standard-version --release-as minor",
|
|
76
78
|
"release:patch": "yarn lint && standard-version --release-as patch",
|
|
@@ -94,15 +96,16 @@
|
|
|
94
96
|
"boxen": "8.0.1",
|
|
95
97
|
"chalk": "5.6.2",
|
|
96
98
|
"commander": "12.1.0",
|
|
99
|
+
"date-fns": "4.1.0",
|
|
97
100
|
"date-holidays": "2.1.1",
|
|
98
101
|
"dayjs": "1.11.19",
|
|
99
|
-
"date-fns": "4.1.0",
|
|
100
102
|
"dotenv": "17.2.2",
|
|
101
103
|
"exceljs": "4.4.0",
|
|
102
104
|
"is-online": "12.0.2",
|
|
103
105
|
"ora": "9.0.0",
|
|
104
106
|
"semver": "6.3.1",
|
|
105
107
|
"string-width": "5.1.2",
|
|
108
|
+
"wukong-profiler": "^1.0.5",
|
|
106
109
|
"zx": "7.2.4"
|
|
107
110
|
},
|
|
108
111
|
"devDependencies": {
|
package/src/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import fs from 'fs'
|
|
|
6
6
|
import ora from 'ora'
|
|
7
7
|
import path from 'path'
|
|
8
8
|
import { fileURLToPath } from 'url'
|
|
9
|
+
import { createProfiler } from 'wukong-profiler'
|
|
9
10
|
|
|
10
11
|
import { parseOptions } from './cli/parseOptions.mjs'
|
|
11
12
|
// eslint-disable-next-line no-unused-vars
|
|
@@ -50,6 +51,8 @@ dayjs.extend(isoWeek)
|
|
|
50
51
|
const PKG_NAME = pkg.name
|
|
51
52
|
const VERSION = pkg.version
|
|
52
53
|
|
|
54
|
+
let profiler
|
|
55
|
+
|
|
53
56
|
const autoCheckUpdate = async () => {
|
|
54
57
|
// === CLI 主逻辑完成后提示更新 ===
|
|
55
58
|
await checkUpdateWithPatch({
|
|
@@ -83,8 +86,6 @@ export function getWeekRange(periodStr) {
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
const main = async () => {
|
|
86
|
-
const startTime = performance.now()
|
|
87
|
-
|
|
88
89
|
const program = new Command()
|
|
89
90
|
|
|
90
91
|
program
|
|
@@ -185,12 +186,31 @@ const main = async () => {
|
|
|
185
186
|
'仅启动 web 服务,不导出或分析数据(使用 output-wukong/data 中已有的数据)'
|
|
186
187
|
)
|
|
187
188
|
.option('--version', 'show version information')
|
|
189
|
+
.option('--profile', '输出性能分析 JSON')
|
|
190
|
+
.option('--verbose', '显示详细性能日志')
|
|
191
|
+
.option('--flame', '显示 flame-like 日志')
|
|
192
|
+
.option('--trace <file>', '生成 Chrome Trace')
|
|
193
|
+
.option('--hot-threshold <n>', 'HOT 比例阈值', parseFloat, 0.8)
|
|
194
|
+
.option('--fail-on-hot', 'HOT 时 CI 失败')
|
|
195
|
+
.option('--diff-base <file>', '基线 profile.json')
|
|
196
|
+
.option('--diff-threshold <n>', '回归阈值', parseFloat, 0.2)
|
|
188
197
|
.parse()
|
|
189
198
|
|
|
190
199
|
const opts = program.opts()
|
|
191
200
|
|
|
192
201
|
const config = parseOptions(opts)
|
|
193
202
|
|
|
203
|
+
profiler = createProfiler({
|
|
204
|
+
enabled: opts.profile,
|
|
205
|
+
verbose: opts.verbose,
|
|
206
|
+
flame: opts.flame,
|
|
207
|
+
traceFile: opts.trace,
|
|
208
|
+
hotThreshold: opts.hotThreshold,
|
|
209
|
+
failOnHot: opts.failOnHot,
|
|
210
|
+
diffBaseFile: opts.diffBase,
|
|
211
|
+
diffThreshold: opts.diffThreshold
|
|
212
|
+
})
|
|
213
|
+
|
|
194
214
|
// ❗只创建一次缓存实例
|
|
195
215
|
const getOvertimeStats = createOvertimeStats(config)
|
|
196
216
|
|
|
@@ -324,6 +344,7 @@ const main = async () => {
|
|
|
324
344
|
|
|
325
345
|
// --- 分组 ---
|
|
326
346
|
const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null
|
|
347
|
+
profiler.step('load config')
|
|
327
348
|
|
|
328
349
|
// If serve mode is enabled, write data modules and launch the web server
|
|
329
350
|
if (opts.serve) {
|
|
@@ -332,7 +353,12 @@ const main = async () => {
|
|
|
332
353
|
|
|
333
354
|
// --- Overtime analysis ---
|
|
334
355
|
if (opts.overtime) {
|
|
356
|
+
await profiler.stepAsync('getOvertimeStats', async () => {
|
|
357
|
+
await getOvertimeStats(records)
|
|
358
|
+
})
|
|
335
359
|
const stats = getOvertimeStats(records)
|
|
360
|
+
|
|
361
|
+
profiler.step('load getOvertimeStats')
|
|
336
362
|
// Output to console
|
|
337
363
|
console.log('\n--- Overtime analysis ---\n')
|
|
338
364
|
console.log(renderOvertimeText(stats))
|
|
@@ -579,7 +605,7 @@ const main = async () => {
|
|
|
579
605
|
const jsonText = renderAuthorChangesJson(records)
|
|
580
606
|
writeJSON(outputFilePath('author-changes.json', outDir), jsonText)
|
|
581
607
|
logDev(`JSON 已导出: ${filepath}`)
|
|
582
|
-
handleSuccess({
|
|
608
|
+
handleSuccess({ spinner })
|
|
583
609
|
return
|
|
584
610
|
}
|
|
585
611
|
|
|
@@ -593,11 +619,11 @@ const main = async () => {
|
|
|
593
619
|
renderChangedLinesText(records)
|
|
594
620
|
)
|
|
595
621
|
|
|
596
|
-
console.log('\n Commits List:\n', text, '\n')
|
|
622
|
+
// console.log('\n Commits List:\n', text, '\n')
|
|
597
623
|
|
|
598
624
|
logDev(`文本已导出: ${filepath}`)
|
|
599
625
|
|
|
600
|
-
handleSuccess({
|
|
626
|
+
handleSuccess({ spinner })
|
|
601
627
|
|
|
602
628
|
return
|
|
603
629
|
}
|
|
@@ -621,10 +647,32 @@ const main = async () => {
|
|
|
621
647
|
logDev(`Excel 已导出: ${excelPath}`)
|
|
622
648
|
logDev(`文本已自动导出: ${txtPath}`)
|
|
623
649
|
|
|
624
|
-
handleSuccess({
|
|
650
|
+
handleSuccess({ spinner })
|
|
625
651
|
}
|
|
626
652
|
|
|
627
653
|
await autoCheckUpdate()
|
|
654
|
+
|
|
655
|
+
handleSuccess({ spinner })
|
|
628
656
|
}
|
|
629
657
|
|
|
630
|
-
|
|
658
|
+
try {
|
|
659
|
+
await main()
|
|
660
|
+
} catch (err) {
|
|
661
|
+
console.error(err)
|
|
662
|
+
process.exitCode = 1
|
|
663
|
+
} finally {
|
|
664
|
+
if (profiler) {
|
|
665
|
+
const result = profiler.end('git-commits')
|
|
666
|
+
|
|
667
|
+
// --profile 时输出 JSON
|
|
668
|
+
if (process.argv.includes('--profile')) {
|
|
669
|
+
const json = {
|
|
670
|
+
command: 'git-commits',
|
|
671
|
+
version: VERSION,
|
|
672
|
+
timestamp: Date.now(),
|
|
673
|
+
profile: result
|
|
674
|
+
}
|
|
675
|
+
console.log(JSON.stringify(json, null, 2))
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
@@ -4,9 +4,6 @@ import { performance } from 'perf_hooks'
|
|
|
4
4
|
import { exitWithTime } from './exitWithTime.mjs'
|
|
5
5
|
import { costTimer } from './timer.mjs'
|
|
6
6
|
|
|
7
|
-
export const handleSuccess = ({ message = 'Done',
|
|
7
|
+
export const handleSuccess = ({ message = 'Done', spinner }) => {
|
|
8
8
|
spinner.succeed(message)
|
|
9
|
-
|
|
10
|
-
// 如果需要退出,可以调用 exitWithTime
|
|
11
|
-
exitWithTime(startTime, 0)
|
|
12
9
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const diffProfiles = (prev, curr, threshold = 0.2) => {
|
|
2
|
+
const map = new Map()
|
|
3
|
+
|
|
4
|
+
prev.events.forEach(e => {
|
|
5
|
+
map.set(e.name, e.duration)
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
const regressions = []
|
|
9
|
+
|
|
10
|
+
curr.events.forEach(e => {
|
|
11
|
+
const before = map.get(e.name)
|
|
12
|
+
if (!before) return
|
|
13
|
+
|
|
14
|
+
const diff = (e.duration - before) / before
|
|
15
|
+
if (diff >= threshold) {
|
|
16
|
+
regressions.push({
|
|
17
|
+
name: e.name,
|
|
18
|
+
before,
|
|
19
|
+
after: e.duration,
|
|
20
|
+
diff
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return regressions
|
|
26
|
+
}
|
|
@@ -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,144 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import { formatTime, makeBar } from './format.mjs'
|
|
4
|
+
import { exportChromeTrace } from './trace.mjs'
|
|
5
|
+
import { diffProfiles } from './diff.mjs'
|
|
6
|
+
|
|
7
|
+
export const createProfiler = ({
|
|
8
|
+
enabled = false,
|
|
9
|
+
verbose = false,
|
|
10
|
+
flame = false,
|
|
11
|
+
slowThreshold = 500,
|
|
12
|
+
hotThreshold = 0.8,
|
|
13
|
+
traceFile,
|
|
14
|
+
failOnHot = false,
|
|
15
|
+
diffBaseFile,
|
|
16
|
+
diffThreshold = 0.2
|
|
17
|
+
} = {}) => {
|
|
18
|
+
const start = process.hrtime.bigint()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
const stack = []
|
|
22
|
+
const events = []
|
|
23
|
+
|
|
24
|
+
const toMs = (a, b) => Number(b - a) / 1e6
|
|
25
|
+
|
|
26
|
+
/* ================= step ================= */
|
|
27
|
+
|
|
28
|
+
const step = (name, fn) => {
|
|
29
|
+
const parent = stack[stack.length - 1]
|
|
30
|
+
const startTime = process.hrtime.bigint()
|
|
31
|
+
|
|
32
|
+
const node = {
|
|
33
|
+
name,
|
|
34
|
+
start: toMs(start, startTime),
|
|
35
|
+
duration: 0,
|
|
36
|
+
depth: stack.length,
|
|
37
|
+
children: []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (parent) parent.children.push(node)
|
|
41
|
+
stack.push(node)
|
|
42
|
+
|
|
43
|
+
const finish = () => {
|
|
44
|
+
const endTime = process.hrtime.bigint()
|
|
45
|
+
node.duration = toMs(startTime, endTime)
|
|
46
|
+
events.push(node)
|
|
47
|
+
stack.pop()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof fn === 'function') {
|
|
51
|
+
try {
|
|
52
|
+
return fn()
|
|
53
|
+
} finally {
|
|
54
|
+
finish()
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
finish()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ================= end ================= */
|
|
62
|
+
|
|
63
|
+
const end = (label = 'Total') => {
|
|
64
|
+
const total = toMs(start, process.hrtime.bigint())
|
|
65
|
+
|
|
66
|
+
let hasHot = false
|
|
67
|
+
|
|
68
|
+
if (enabled || verbose) {
|
|
69
|
+
console.log(
|
|
70
|
+
chalk.cyan('⏱'),
|
|
71
|
+
chalk.bold(label),
|
|
72
|
+
chalk.yellow(formatTime(total))
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (flame) {
|
|
76
|
+
for (const e of events) {
|
|
77
|
+
const ratio = e.duration / total
|
|
78
|
+
const hot = ratio >= hotThreshold
|
|
79
|
+
const slow = e.duration >= slowThreshold
|
|
80
|
+
|
|
81
|
+
if (hot) hasHot = true
|
|
82
|
+
|
|
83
|
+
console.log(
|
|
84
|
+
chalk.gray(`${' '.repeat(e.depth) }├─`),
|
|
85
|
+
chalk.white(e.name.padEnd(22)),
|
|
86
|
+
chalk.yellow(formatTime(e.duration)),
|
|
87
|
+
chalk.gray(makeBar(ratio)),
|
|
88
|
+
hot
|
|
89
|
+
? chalk.red.bold(' 🔥 HOT')
|
|
90
|
+
: slow
|
|
91
|
+
? chalk.yellow(' ⚠ SLOW')
|
|
92
|
+
: ''
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const profile = { total, events }
|
|
99
|
+
|
|
100
|
+
/* ---------- Chrome Trace ---------- */
|
|
101
|
+
|
|
102
|
+
if (traceFile) {
|
|
103
|
+
exportChromeTrace(events, traceFile)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ---------- diff ---------- */
|
|
107
|
+
if (diffBaseFile && fs.existsSync(diffBaseFile)) {
|
|
108
|
+
const base = JSON.parse(fs.readFileSync(diffBaseFile, 'utf8'))
|
|
109
|
+
const regressions = diffProfiles(base, profile, diffThreshold)
|
|
110
|
+
|
|
111
|
+
if (regressions.length) {
|
|
112
|
+
console.log(chalk.red('\n⚠ Performance Regression Detected:\n'))
|
|
113
|
+
regressions.forEach(r => {
|
|
114
|
+
console.log(
|
|
115
|
+
chalk.red(
|
|
116
|
+
` ${r.name}: ${formatTime(r.before)} → ${formatTime(
|
|
117
|
+
r.after
|
|
118
|
+
)} (+${(r.diff * 100).toFixed(1)}%)`
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
process.exitCode = 1
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ---------- save profile ---------- */
|
|
127
|
+
if (enabled) {
|
|
128
|
+
fs.writeFileSync(
|
|
129
|
+
'profile.json',
|
|
130
|
+
JSON.stringify(profile, null, 2)
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ---------- CI HOT fail ---------- */
|
|
135
|
+
if (failOnHot && hasHot) {
|
|
136
|
+
console.log(chalk.red('\n🔥 HOT step detected — failing CI'))
|
|
137
|
+
process.exitCode = 1
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return profile
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { step, end }
|
|
144
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
|
|
3
|
+
export const exportChromeTrace = (events, file) => {
|
|
4
|
+
const traceEvents = []
|
|
5
|
+
|
|
6
|
+
for (const e of events) {
|
|
7
|
+
traceEvents.push({
|
|
8
|
+
name: e.name,
|
|
9
|
+
cat: 'cli',
|
|
10
|
+
ph: 'X', // complete event
|
|
11
|
+
ts: e.start * 1000, // μs
|
|
12
|
+
dur: e.duration * 1000, // μs
|
|
13
|
+
pid: 1,
|
|
14
|
+
tid: e.depth || 0
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const trace = {
|
|
19
|
+
traceEvents,
|
|
20
|
+
displayTimeUnit: 'ms'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fs.writeFileSync(file, JSON.stringify(trace, null, 2))
|
|
24
|
+
// TODO: remove debug log before production
|
|
25
|
+
console.log('✅', 'file', file);
|
|
26
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
/* ================= utils ================= */
|
|
4
|
+
|
|
5
|
+
const formatTime = (ms) => {
|
|
6
|
+
if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
|
|
7
|
+
if (ms < 1000) return `${ms.toFixed(2)} ms`
|
|
8
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
|
|
9
|
+
return `${(ms / 60000).toFixed(2)} min`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const makeBar = (ratio, width = 32) => {
|
|
13
|
+
const len = Math.max(1, Math.round(ratio * width))
|
|
14
|
+
return '█'.repeat(len)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* ================= profiler ================= */
|
|
18
|
+
|
|
19
|
+
export const createProfiler = ({
|
|
20
|
+
enabled = false,
|
|
21
|
+
verbose = false,
|
|
22
|
+
flame = false,
|
|
23
|
+
slowThreshold = 500, // ms
|
|
24
|
+
hotThreshold = 0.8 // ratio (0~1)
|
|
25
|
+
} = {}) => {
|
|
26
|
+
const start = process.hrtime.bigint()
|
|
27
|
+
let last = start
|
|
28
|
+
const events = []
|
|
29
|
+
|
|
30
|
+
const toMs = (a, b) => Number(b - a) / 1e6
|
|
31
|
+
|
|
32
|
+
/* -------- step -------- */
|
|
33
|
+
const step = (name, meta = {}) => {
|
|
34
|
+
const now = process.hrtime.bigint()
|
|
35
|
+
const duration = toMs(last, now)
|
|
36
|
+
last = now
|
|
37
|
+
|
|
38
|
+
events.push({
|
|
39
|
+
name,
|
|
40
|
+
duration,
|
|
41
|
+
sinceStart: toMs(start, now),
|
|
42
|
+
meta
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (!enabled && !verbose) return
|
|
46
|
+
|
|
47
|
+
const isSlow = duration >= slowThreshold
|
|
48
|
+
|
|
49
|
+
console.log(
|
|
50
|
+
chalk.gray(' ├─'),
|
|
51
|
+
isSlow ? chalk.red.bold(name) : chalk.white(name),
|
|
52
|
+
chalk.yellow(formatTime(duration))
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* -------- end -------- */
|
|
57
|
+
const end = (label = 'Total') => {
|
|
58
|
+
const endTime = process.hrtime.bigint()
|
|
59
|
+
const total = toMs(start, endTime)
|
|
60
|
+
|
|
61
|
+
// 计算 HOT
|
|
62
|
+
for (const e of events) {
|
|
63
|
+
e.ratio = total > 0 ? e.duration / total : 0
|
|
64
|
+
e.hot = e.ratio >= hotThreshold
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (enabled || verbose) {
|
|
68
|
+
console.log(
|
|
69
|
+
chalk.cyan('⏱'),
|
|
70
|
+
chalk.bold(label),
|
|
71
|
+
chalk.yellow(formatTime(total))
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if (flame) {
|
|
75
|
+
for (const e of events) {
|
|
76
|
+
const bar = makeBar(e.ratio)
|
|
77
|
+
const hotMark = e.hot ? chalk.red.bold(' 🔥 HOT') : ''
|
|
78
|
+
const slowMark =
|
|
79
|
+
!e.hot && e.duration >= slowThreshold
|
|
80
|
+
? chalk.yellow(' ⚠ SLOW')
|
|
81
|
+
: ''
|
|
82
|
+
|
|
83
|
+
console.log(
|
|
84
|
+
chalk.gray(' ├─'),
|
|
85
|
+
chalk.white(e.name.padEnd(24)),
|
|
86
|
+
chalk.yellow(formatTime(e.duration)),
|
|
87
|
+
chalk.gray(bar),
|
|
88
|
+
hotMark || slowMark
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
total,
|
|
96
|
+
events
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { step, end }
|
|
101
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// utils/scopeTimer.js
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
|
|
4
|
+
const formatTime = (ms) => {
|
|
5
|
+
if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
|
|
6
|
+
if (ms < 1000) return `${ms.toFixed(2)} ms`
|
|
7
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
|
|
8
|
+
return `${(ms / 60000).toFixed(2)} min`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const createScopeTimer = (scope) => {
|
|
12
|
+
const start = process.hrtime.bigint()
|
|
13
|
+
let last = start
|
|
14
|
+
|
|
15
|
+
const step = (label) => {
|
|
16
|
+
const now = process.hrtime.bigint()
|
|
17
|
+
const ms = Number(now - last) / 1e6
|
|
18
|
+
last = now
|
|
19
|
+
|
|
20
|
+
console.log(
|
|
21
|
+
chalk.gray(' ├─'),
|
|
22
|
+
chalk.white(label),
|
|
23
|
+
chalk.yellow(formatTime(ms))
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const end = () => {
|
|
28
|
+
const total = Number(process.hrtime.bigint() - start) / 1e6
|
|
29
|
+
console.log(
|
|
30
|
+
chalk.cyan('⏱'),
|
|
31
|
+
chalk.bold(scope),
|
|
32
|
+
chalk.yellow(formatTime(total))
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { step, end }
|
|
37
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// utils/timer.js
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
|
|
4
|
+
const ENABLED = process.env.CLI_TIMER !== 'false'
|
|
5
|
+
|
|
6
|
+
const formatTime = (ms) => {
|
|
7
|
+
if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
|
|
8
|
+
if (ms < 1000) return `${ms.toFixed(2)} ms`
|
|
9
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
|
|
10
|
+
return `${(ms / 60000).toFixed(2)} min`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const createTimer = (label, { silent = false } = {}) => {
|
|
14
|
+
const start = process.hrtime.bigint()
|
|
15
|
+
|
|
16
|
+
const stop = (suffix) => {
|
|
17
|
+
if (!ENABLED || silent) return
|
|
18
|
+
|
|
19
|
+
const end = process.hrtime.bigint()
|
|
20
|
+
const ms = Number(end - start) / 1e6
|
|
21
|
+
|
|
22
|
+
console.log(
|
|
23
|
+
chalk.cyan('⏱'),
|
|
24
|
+
chalk.white(label),
|
|
25
|
+
suffix ? chalk.gray(`(${suffix})`) : '',
|
|
26
|
+
chalk.bold.yellow(formatTime(ms))
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return ms
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { stop }
|
|
33
|
+
}
|