wukong-gitlog-cli 1.0.37 → 1.0.39
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 +19 -0
- package/bin/wukong-gitlog-cli +0 -0
- package/package.json +19 -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/web/app.js +37 -4
- package/web/index.html +4 -0
- package/web/static/style.css +39 -0
package/.eslintrc
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
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.39](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.38...v1.0.39) (2025-12-29)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 🎸 web report add date filter ([d5968d8](https://github.com/tomatobybike/wukong-gitlog-cli/commit/d5968d8222aa1129e60894234ac9fca6c91e5a73))
|
|
11
|
+
* 🎸 wukong-gitlog command entry ([48b5698](https://github.com/tomatobybike/wukong-gitlog-cli/commit/48b569842114cd11336ee23580104048342e085b))
|
|
12
|
+
|
|
13
|
+
### [1.0.38](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.37...v1.0.38) (2025-12-27)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* 🎸 增加性能分析 ([ec925cc](https://github.com/tomatobybike/wukong-gitlog-cli/commit/ec925cc3d20be2a0eee6e20aaee99b4449eff9be))
|
|
19
|
+
* 🎸 flame 和 显示 flame-like 日志 ([7b1a00c](https://github.com/tomatobybike/wukong-gitlog-cli/commit/7b1a00c26afa1757637e6c4a3a2be92d74e0ad65))
|
|
20
|
+
* 🎸 ignore ([eb3e5bc](https://github.com/tomatobybike/wukong-gitlog-cli/commit/eb3e5bc89183e0722056036f14f74806e3c3b1e3))
|
|
21
|
+
* 🎸 profiler ([48fd0a5](https://github.com/tomatobybike/wukong-gitlog-cli/commit/48fd0a5a8b25b55bbca7bbdbd2df11158049adcd))
|
|
22
|
+
* 🎸 Profiler ([a7f4623](https://github.com/tomatobybike/wukong-gitlog-cli/commit/a7f4623a162947d31f25702052f54a4ff0406fcb))
|
|
23
|
+
|
|
5
24
|
### [1.0.37](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.36...v1.0.37) (2025-12-12)
|
|
6
25
|
|
|
7
26
|
|
package/bin/wukong-gitlog-cli
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wukong-gitlog-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.39",
|
|
4
4
|
"description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -34,43 +34,46 @@
|
|
|
34
34
|
"author": "Tom <tomatobybike@gmail.com>",
|
|
35
35
|
"type": "module",
|
|
36
36
|
"bin": {
|
|
37
|
+
"wukong-gitlog": "./bin/wukong-gitlog-cli",
|
|
37
38
|
"wukong-gitlog-cli": "./bin/wukong-gitlog-cli"
|
|
38
39
|
},
|
|
39
40
|
"scripts": {
|
|
41
|
+
"build": "node scripts/esbuild.config.mjs",
|
|
42
|
+
"check:npm:login": "node scripts/check-npm-login.mjs",
|
|
40
43
|
"cli:excel-demo": "node ./src/cli.mjs --overtime --format excel --stats --limit 5 --out commits.xlsx",
|
|
41
44
|
"cli:excel-demo-parent": "node ./src/cli.mjs --out-parent --format excel --stats --limit 5 --out commits-parent.xlsx",
|
|
42
45
|
"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
46
|
"cli:gerrit-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{hash}}\" --limit 5 --out commits-gerrit.txt",
|
|
44
47
|
"cli:help": "node ./src/cli.mjs --help",
|
|
45
48
|
"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
49
|
"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
50
|
"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
51
|
"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
52
|
"cli:overtime-excel-cn-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
53
|
+
"cli:overtime-excel-cn-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
57
54
|
"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
55
|
"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",
|
|
56
|
+
"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",
|
|
57
|
+
"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",
|
|
58
|
+
"cli:overtime-text": "node ./src/cli.mjs --overtime --limit 100 --format text --out commits.txt",
|
|
59
|
+
"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",
|
|
60
|
+
"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",
|
|
61
|
+
"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
62
|
"cli:serve": "node ./src/cli.mjs --serve --overtime --format text --out commits.txt",
|
|
62
63
|
"cli:serve-only": "node ./src/cli.mjs --serve-only",
|
|
64
|
+
"cli:text-demo": "node ./src/cli.mjs --format text --limit 5 --out commits.txt",
|
|
65
|
+
"cli:text-demo-parent": "node ./src/cli.mjs --out-parent --format text --limit 5 --out commits-parent.txt",
|
|
63
66
|
"format": "prettier --write \"src/**/*.{js,mjs}\"",
|
|
64
67
|
"lint": "eslint src --ext .js,.mjs src",
|
|
65
68
|
"lint:fix": "eslint src --ext .js,.mjs --fix",
|
|
66
69
|
"pack": "npm pack",
|
|
67
|
-
"build": "node scripts/esbuild.config.mjs",
|
|
68
70
|
"prepare": "husky install",
|
|
69
|
-
"prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
|
|
70
71
|
"prepublishOnly": "yarn lint && yarn build",
|
|
71
|
-
"
|
|
72
|
-
"release": "yarn check:npm:login && yarn release:patch && yarn push:tags && npm publish",
|
|
72
|
+
"prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
|
|
73
73
|
"push:tags": "git push origin main --follow-tags",
|
|
74
|
+
"prerelease": "yarn lint",
|
|
75
|
+
"release": "yarn check:npm:login && yarn release:patch && npm publish && yarn push:tags",
|
|
76
|
+
"release:rollback": "git reset --hard HEAD~1 && git tag -d $(git tag --points-at HEAD~1)",
|
|
74
77
|
"release:major": "yarn lint && standard-version --release-as major",
|
|
75
78
|
"release:minor": "yarn lint && standard-version --release-as minor",
|
|
76
79
|
"release:patch": "yarn lint && standard-version --release-as patch",
|
|
@@ -94,15 +97,16 @@
|
|
|
94
97
|
"boxen": "8.0.1",
|
|
95
98
|
"chalk": "5.6.2",
|
|
96
99
|
"commander": "12.1.0",
|
|
100
|
+
"date-fns": "4.1.0",
|
|
97
101
|
"date-holidays": "2.1.1",
|
|
98
102
|
"dayjs": "1.11.19",
|
|
99
|
-
"date-fns": "4.1.0",
|
|
100
103
|
"dotenv": "17.2.2",
|
|
101
104
|
"exceljs": "4.4.0",
|
|
102
105
|
"is-online": "12.0.2",
|
|
103
106
|
"ora": "9.0.0",
|
|
104
107
|
"semver": "6.3.1",
|
|
105
108
|
"string-width": "5.1.2",
|
|
109
|
+
"wukong-profiler": "^1.0.6",
|
|
106
110
|
"zx": "7.2.4"
|
|
107
111
|
},
|
|
108
112
|
"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
|
+
}
|
package/web/app.js
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
/* global echarts */
|
|
4
4
|
const formatDate = (d) => new Date(d).toLocaleString()
|
|
5
5
|
|
|
6
|
+
function filterByDate(commits) {
|
|
7
|
+
const start = document.getElementById('startDate')?.value
|
|
8
|
+
const end = document.getElementById('endDate')?.value
|
|
9
|
+
|
|
10
|
+
if (!start && !end) return commits
|
|
11
|
+
|
|
12
|
+
const startTime = start ? new Date(`${start}T00:00:00`).getTime() : -Infinity
|
|
13
|
+
|
|
14
|
+
const endTime = end ? new Date(`${end}T23:59:59`).getTime() : Infinity
|
|
15
|
+
|
|
16
|
+
return commits.filter((c) => {
|
|
17
|
+
const t = new Date(c.date).getTime()
|
|
18
|
+
return t >= startTime && t <= endTime
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
// ISO 周 key:YYYY-Www
|
|
7
23
|
function getIsoWeekKey(dStr) {
|
|
8
24
|
const d = new Date(dStr)
|
|
@@ -100,7 +116,8 @@ function renderCommitsTablePage() {
|
|
|
100
116
|
tr.innerHTML = `<td>${c.hash.slice(0, 8)}</td><td>${c.author}</td><td>${c.email}</td><td>${formatDate(c.date)}</td><td>${c.message}</td><td>${c.changed}</td>`
|
|
101
117
|
tbody.appendChild(tr)
|
|
102
118
|
})
|
|
103
|
-
document.getElementById('commitsTotal').textContent =
|
|
119
|
+
document.getElementById('commitsTotal').textContent =
|
|
120
|
+
`共${filtered.length}条记录`
|
|
104
121
|
}
|
|
105
122
|
|
|
106
123
|
function updatePager() {
|
|
@@ -115,16 +132,25 @@ function updatePager() {
|
|
|
115
132
|
function applySearch() {
|
|
116
133
|
const q = document.getElementById('searchInput').value.trim().toLowerCase()
|
|
117
134
|
|
|
135
|
+
// ① 先做日期过滤
|
|
136
|
+
const base = filterByDate(commitsAll)
|
|
137
|
+
|
|
118
138
|
if (!q) {
|
|
119
|
-
filtered =
|
|
139
|
+
filtered = base.slice()
|
|
120
140
|
} else {
|
|
121
|
-
filtered =
|
|
141
|
+
filtered = base.filter((c) => {
|
|
122
142
|
const h = c.hash.toLowerCase()
|
|
123
143
|
const a = String(c.author || '').toLowerCase()
|
|
124
144
|
const e = String(c.email || '').toLowerCase()
|
|
125
145
|
const m = String(c.message || '').toLowerCase()
|
|
126
146
|
const d = formatDate(c.date).toLowerCase()
|
|
127
|
-
return
|
|
147
|
+
return (
|
|
148
|
+
h.includes(q) ||
|
|
149
|
+
a.includes(q) ||
|
|
150
|
+
e.includes(q) ||
|
|
151
|
+
m.includes(q) ||
|
|
152
|
+
d.includes(q)
|
|
153
|
+
)
|
|
128
154
|
})
|
|
129
155
|
}
|
|
130
156
|
page = 1
|
|
@@ -134,6 +160,13 @@ function applySearch() {
|
|
|
134
160
|
|
|
135
161
|
function initTableControls() {
|
|
136
162
|
document.getElementById('searchInput').addEventListener('input', applySearch)
|
|
163
|
+
document.getElementById('startDate')?.addEventListener('change', applySearch)
|
|
164
|
+
document.getElementById('endDate')?.addEventListener('change', applySearch)
|
|
165
|
+
document.getElementById('clearDate')?.addEventListener('click', () => {
|
|
166
|
+
document.getElementById('startDate').value = ''
|
|
167
|
+
document.getElementById('endDate').value = ''
|
|
168
|
+
applySearch()
|
|
169
|
+
})
|
|
137
170
|
document.getElementById('pageSize').addEventListener('change', (e) => {
|
|
138
171
|
pageSize = parseInt(e.target.value, 10) || 10
|
|
139
172
|
page = 1
|
package/web/index.html
CHANGED
|
@@ -109,6 +109,10 @@
|
|
|
109
109
|
type="search"
|
|
110
110
|
placeholder="搜索作者/信息/Hash/Date"
|
|
111
111
|
/>
|
|
112
|
+
<input type="date" id="startDate" />
|
|
113
|
+
<span>~</span>
|
|
114
|
+
<input type="date" id="endDate" />
|
|
115
|
+
<button id="clearDate">清除</button>
|
|
112
116
|
<label for="pageSize">每页显示</label>
|
|
113
117
|
<select id="pageSize">
|
|
114
118
|
<option value="10">10</option>
|
package/web/static/style.css
CHANGED
|
@@ -139,6 +139,45 @@ td {
|
|
|
139
139
|
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15);
|
|
140
140
|
outline: none;
|
|
141
141
|
}
|
|
142
|
+
#tableControls input[type='date'] {
|
|
143
|
+
padding: 9px 12px;
|
|
144
|
+
border-radius: 8px;
|
|
145
|
+
border: 1px solid #ccc;
|
|
146
|
+
background: #fafafa;
|
|
147
|
+
font-size: 13px;
|
|
148
|
+
color: #111827;
|
|
149
|
+
transition:
|
|
150
|
+
border-color 0.2s,
|
|
151
|
+
box-shadow 0.2s;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#tableControls input[type='date']:focus {
|
|
155
|
+
border-color: #1976d2;
|
|
156
|
+
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15);
|
|
157
|
+
outline: none;
|
|
158
|
+
}
|
|
159
|
+
/* 清除日期按钮 ------------------------------------------ */
|
|
160
|
+
#clearDate {
|
|
161
|
+
padding: 8px 10px;
|
|
162
|
+
border-radius: 8px;
|
|
163
|
+
border: none;
|
|
164
|
+
background: transparent;
|
|
165
|
+
color: #64748b; /* slate-500 */
|
|
166
|
+
font-size: 13px;
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
transition:
|
|
169
|
+
color 0.2s ease,
|
|
170
|
+
background 0.2s ease;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#clearDate:hover {
|
|
174
|
+
color: #ef4444; /* red-500 */
|
|
175
|
+
background: rgba(239, 68, 68, 0.08);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#clearDate:active {
|
|
179
|
+
background: rgba(239, 68, 68, 0.16);
|
|
180
|
+
}
|
|
142
181
|
|
|
143
182
|
/* 按钮 — MUI Button 风格 */
|
|
144
183
|
.pager button {
|