multi-tasks 3.3.0 → 3.4.1
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 +277 -375
- package/package.json +36 -36
- package/utils/backward-v1.js +1 -0
- package/utils/makedir.js +12 -18
- package/utils/progressCtl.js +287 -187
- package/utils/progressView.js +110 -102
- package/utils/randoms.js +7 -4
- package/utils/resolveWorkers.js +4 -2
- package/utils/startupBanner.js +2 -1
- package/utils/workerStatusProbe.js +38 -0
- package/workers/Multithread.js +497 -495
- package/workers/TaskMgr.js +26 -0
- package/workers/WorkerMgr.js +18 -27
package/utils/progressView.js
CHANGED
|
@@ -1,102 +1,110 @@
|
|
|
1
|
-
//终端进度条渲染器(零依赖):
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.replace(
|
|
24
|
-
.replace(/(\d+)
|
|
25
|
-
.replace(/(\d+)
|
|
26
|
-
.replace(/(\d+)
|
|
27
|
-
.replace(/(\d+)
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let
|
|
34
|
-
let
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
-
//TTY 现场排查开关: MULTI_TASKS_PROGRESS_DEBUG=文件路径 时, 每帧字节流+当时列宽记成 JSON 行,
|
|
38
|
-
//供 test/terminalSim.js 回放比对真实终端行为(仅排查用, 平时为空不写盘)
|
|
39
|
-
const debugPath = process.env.MULTI_TASKS_PROGRESS_DEBUG;
|
|
40
|
-
const write = (s) => {
|
|
41
|
-
if (debugPath) {
|
|
42
|
-
fs.appendFileSync(debugPath, `${JSON.stringify({ at: Date.now(), cols: stream.columns || 80, data: s })}\n`);
|
|
43
|
-
};
|
|
44
|
-
stream.write(s);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const buildLines = ({ done, succ, running, remaining, failed, succButSaveFailed }) => {
|
|
48
|
-
//行比终端列宽还长会折行、光标定位错乱: 每行封顶 cols-1(截断后才上色);
|
|
49
|
-
//留 1 空列: 写满整列会触发终端满列换行(deferred wrap), resize 重排时 ESC[1A 数行失准;
|
|
50
|
-
//bar 行固定 BAR_COLS 列(窄终端整行截断, 行尾百分比会被截掉): 行短则拖动窗口时几乎不触发折行重排
|
|
51
|
-
const cols = stream.columns || 80;
|
|
52
|
-
const maxLineLen = Math.max(1, cols - 1);
|
|
53
|
-
const truncate = (line) => (line.length > maxLineLen ? line.slice(0, maxLineLen) : line);
|
|
54
|
-
let total = done + running + remaining;
|
|
55
|
-
let width = BAR_COLS - BAR_OVERHEAD;//固定 26 格, 不随终端宽度变
|
|
56
|
-
let filled = total > 0 ? Math.floor(width * done / total) : width;
|
|
57
|
-
let bar = '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
58
|
-
let pct = (total > 0 ? 100 * done / total : 100).toFixed(2).padStart(6);//两位小数, 定宽防行长抖动
|
|
59
|
-
|
|
60
|
-
let barLine = `[${bar}] ${pct}%`;
|
|
61
|
-
let infoLine = `${done}/${total} done, ${succ || 0} succ, ${running} running, ${failed || 0} failed, ${succButSaveFailed || 0} succ-but-save-failed`;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
1
|
+
//终端进度条渲染器(零依赖): N 行帧——首行 bar+百分比固定 36 列(约 18 个中文字符宽, 不随终端宽度变), 末行实时计数(动态宽, 封顶 cols-1),
|
|
2
|
+
//中间可接 extraLines 附加行(如 worker 状态行, 同样封顶 cols-1); 帧行数帧间必须恒定(由调用方保证), 是原地重绘的前提。
|
|
3
|
+
//bar 行短则拖动窗口时几乎不触发终端折行重排(reflow), ESC[nA 数行更可信(占满整行方案在 conpty 重排下
|
|
4
|
+
//反复错位, 用户拍板改短行)。首帧直接写整帧(无光标控制); 后续帧 \r 回行首 + 清行 + 逐行 ESC[1A 上移清行(N-1 次) + 重写整帧。
|
|
5
|
+
//resize 重锚定(reanchor)是抹除式: 终端重排(reflow)后旧帧占用行数不可信, 按记录的上帧
|
|
6
|
+
//各行可见长度 ÷ 当前列宽算出实际占用行数, 上移回旧帧首行 + ESC[0J 清到屏尾, 下一帧从锚点重写——
|
|
7
|
+
//不做换行定格: 定格会把重排碎片留在滚动区, 连续拖动即满屏碎片(用户否掉的旧方案)。
|
|
8
|
+
//已知取舍: 两帧之间若有外部写入共享终端, ESC[nA 数行会被挤偏, 旧帧可能滞留残留(用户确认接受)。
|
|
9
|
+
const ESC = '\x1B';
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const RED = '\x1B[31m';
|
|
12
|
+
const GREEN = '\x1B[32m';
|
|
13
|
+
const YELLOW = '\x1B[33m';
|
|
14
|
+
const RESET_FG = '\x1B[39m';
|
|
15
|
+
|
|
16
|
+
const BAR_OVERHEAD = 10;//'[' + '] ' + 6 位百分比 + '%'
|
|
17
|
+
const BAR_COLS = 36;//bar 行固定 36 列(约 18 个中文字符宽, 用户拍板, 不随终端宽度变): 行短则拖动窗口时几乎不触发折行重排; info 行保持动态宽(计数器全显)
|
|
18
|
+
|
|
19
|
+
//仅渲染帧上色(writeLine/run.log 永远无色): bar 完成段绿 / done 数绿 / succ 数绿 / running 数黄 / failed 整词红 / succ-but-save-failed 数黄,
|
|
20
|
+
//为 0 不着色减少噪音。必须在按列宽截断之后调用: 颜色码不占可见宽度, 也不会被截断拆开
|
|
21
|
+
const colorizeFrame = (line) => {
|
|
22
|
+
return line
|
|
23
|
+
.replace(/\[([█]+)/, (m, blocks) => `[${GREEN}${blocks}${RESET_FG}`)
|
|
24
|
+
.replace(/(\d+) failed/, (m, n) => (n === '0' ? m : `${RED}${n} failed${RESET_FG}`))
|
|
25
|
+
.replace(/(\d+) succ-but-save-failed/, (m, n) => (n === '0' ? m : `${YELLOW}${n}${RESET_FG} succ-but-save-failed`))
|
|
26
|
+
.replace(/(\d+) running/, (m, n) => (n === '0' ? m : `${YELLOW}${n}${RESET_FG} running`))
|
|
27
|
+
.replace(/(\d+) succ(?=,|$)/, (m, n) => (n === '0' ? m : `${GREEN}${n}${RESET_FG} succ`))//(?=,|$) 防误配 succ-but-save-failed 的词头
|
|
28
|
+
.replace(/(\d+)\/(\d+) done/, (m, done, total) => (done === '0' ? m : `${GREEN}${done}${RESET_FG}/${total} done`));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const create = ({ stream }) => {
|
|
32
|
+
if (!stream) { throw new Error('progressView needs a stream'); };
|
|
33
|
+
let frameCount = 0;
|
|
34
|
+
let ended = false;
|
|
35
|
+
let lastLineLens = [];//上帧各行可见长度(截断后、上色前): resize 重排后算旧帧实际占用行数用
|
|
36
|
+
|
|
37
|
+
//TTY 现场排查开关: MULTI_TASKS_PROGRESS_DEBUG=文件路径 时, 每帧字节流+当时列宽记成 JSON 行,
|
|
38
|
+
//供 test/terminalSim.js 回放比对真实终端行为(仅排查用, 平时为空不写盘)
|
|
39
|
+
const debugPath = process.env.MULTI_TASKS_PROGRESS_DEBUG;
|
|
40
|
+
const write = (s) => {
|
|
41
|
+
if (debugPath) {
|
|
42
|
+
fs.appendFileSync(debugPath, `${JSON.stringify({ at: Date.now(), cols: stream.columns || 80, data: s })}\n`);
|
|
43
|
+
};
|
|
44
|
+
stream.write(s);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const buildLines = ({ done, succ, running, remaining, failed, succButSaveFailed, extraLines }) => {
|
|
48
|
+
//行比终端列宽还长会折行、光标定位错乱: 每行封顶 cols-1(截断后才上色);
|
|
49
|
+
//留 1 空列: 写满整列会触发终端满列换行(deferred wrap), resize 重排时 ESC[1A 数行失准;
|
|
50
|
+
//bar 行固定 BAR_COLS 列(窄终端整行截断, 行尾百分比会被截掉): 行短则拖动窗口时几乎不触发折行重排
|
|
51
|
+
const cols = stream.columns || 80;
|
|
52
|
+
const maxLineLen = Math.max(1, cols - 1);
|
|
53
|
+
const truncate = (line) => (line.length > maxLineLen ? line.slice(0, maxLineLen) : line);
|
|
54
|
+
let total = done + running + remaining;
|
|
55
|
+
let width = BAR_COLS - BAR_OVERHEAD;//固定 26 格, 不随终端宽度变
|
|
56
|
+
let filled = total > 0 ? Math.floor(width * done / total) : width;
|
|
57
|
+
let bar = '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
58
|
+
let pct = (total > 0 ? 100 * done / total : 100).toFixed(2).padStart(6);//两位小数, 定宽防行长抖动
|
|
59
|
+
//末行实时计数器(done/succ/running/failed/succ-but-save-failed), 与收尾计数器行同源(都出自 getFinalStats)
|
|
60
|
+
let barLine = `[${bar}] ${pct}%`;
|
|
61
|
+
let infoLine = `${done}/${total} done, ${succ || 0} succ, ${running} running, ${failed || 0} failed, ${succButSaveFailed || 0} succ-but-save-failed`;
|
|
62
|
+
//extraLines(状态行等)插在 bar 行与计数器行之间(计数器行固定帧尾——用户拍板):
|
|
63
|
+
//帧行数必须帧间恒定(ESC[nA 上移数按新帧行数算, 变行数会错位), 由调用方(progressCtl)保证开启期间每帧都带
|
|
64
|
+
let lines = [barLine];
|
|
65
|
+
if (extraLines) {
|
|
66
|
+
lines = lines.concat(extraLines);
|
|
67
|
+
};
|
|
68
|
+
lines.push(infoLine);
|
|
69
|
+
return lines.map(truncate);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const render = (data) => {
|
|
73
|
+
let lines = buildLines(data);
|
|
74
|
+
lastLineLens = lines.map((line) => line.length);
|
|
75
|
+
let frame = lines.map(colorizeFrame).join('\n');
|
|
76
|
+
if (frameCount === 0) {
|
|
77
|
+
write(frame);
|
|
78
|
+
} else {
|
|
79
|
+
//N 行帧同构推广: \r 回行首清当前行(帧末行) + 逐行上移清行(N-1 次) + 重写整帧
|
|
80
|
+
let prefix = `\r${ESC}[2K` + `${ESC}[1A${ESC}[2K`.repeat(lines.length - 1);
|
|
81
|
+
write(`${prefix}${frame}`);
|
|
82
|
+
};
|
|
83
|
+
frameCount++;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const end = () => {//收尾换行: 进度区定格, 后续输出(shell 提示符等)落在下一行
|
|
87
|
+
ended = true;
|
|
88
|
+
write('\n');
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const reanchor = () => {//终端 resize 重排后旧帧行数不可信: 按上帧各行行长 ÷ 当前列宽算出实际占用行数,
|
|
92
|
+
//上移回旧帧首行并 ESC[0J 清到屏尾(抹除旧帧, 不留定格碎片), 下一帧从该锚点按首帧重写
|
|
93
|
+
if (ended || frameCount === 0) {
|
|
94
|
+
return;
|
|
95
|
+
};
|
|
96
|
+
const cols = stream.columns || 80;
|
|
97
|
+
const wrapRows = (len) => Math.max(1, Math.ceil(len / cols));
|
|
98
|
+
const rowsAbove = lastLineLens.reduce((sum, len) => sum + wrapRows(len), 0) - 1;
|
|
99
|
+
write(`\r${ESC}[${rowsAbove}A${ESC}[0J`);
|
|
100
|
+
frameCount = 0;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const writeLine = (text) => {//定格后整行输出(计数器行等): 纯文本一行, 不进 run.log(ASCII 契约之外的字符只走 render)
|
|
104
|
+
write(`${text}\n`);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return { render, end, reanchor, writeLine };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
module.exports = { create };
|
package/utils/randoms.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
|
|
2
|
+
const crypto = require('crypto');
|
|
2
3
|
const fmtDigit = (n)=>{
|
|
3
4
|
return n > 9 ? "" + n: "0" + n;
|
|
4
|
-
}
|
|
5
|
+
};
|
|
5
6
|
const getDateTimeTxt = ()=>{
|
|
6
7
|
let d=new Date();
|
|
7
|
-
let YYYY=d.getFullYear()
|
|
8
|
+
let YYYY=d.getFullYear();
|
|
8
9
|
let MM=fmtDigit(d.getMonth()+1);
|
|
9
10
|
let DD=fmtDigit(d.getDate());
|
|
10
11
|
let HH=fmtDigit(d.getHours());
|
|
@@ -13,9 +14,11 @@ const getDateTimeTxt = ()=>{
|
|
|
13
14
|
return `${YYYY}-${MM}-${DD}_${HH}-${mm}-${ss}`;
|
|
14
15
|
};
|
|
15
16
|
const get36Id = ()=>{
|
|
16
|
-
|
|
17
|
+
//时间戳前缀(保排序可读) + crypto 随机后缀(hex 是 base36 字符集子集):
|
|
18
|
+
//Math.random 拼数字会丢前导零且同毫秒时钟不变, 批量建任务熵不足(勿回退)
|
|
17
19
|
let num2 = Date.now().toString(36);
|
|
18
|
-
let
|
|
20
|
+
let rand = crypto.randomBytes(8).toString('hex');
|
|
21
|
+
let str = num2+rand;
|
|
19
22
|
return str;
|
|
20
23
|
};
|
|
21
24
|
module.exports = {getDateTimeTxt, get36Id};
|
package/utils/resolveWorkers.js
CHANGED
|
@@ -6,7 +6,7 @@ const PERCENT_PATTERN = /^(\d+(?:\.\d+)?)%$/;
|
|
|
6
6
|
|
|
7
7
|
// 解析 numberOfWorkers 配置:
|
|
8
8
|
// - undefined -> 默认 "50%", 按核数四舍五入折算, 最小为 1;
|
|
9
|
-
// - number ->
|
|
9
|
+
// - number -> 必须是 >=1 的整数(0 会静默起不来 worker、小数/负数行为隐晦, fail-fast);
|
|
10
10
|
// - string -> 必须是 "数字%" 形式且百分比在 1-100 之间, 按核数四舍五入折算, 最小为 1;
|
|
11
11
|
// - 其余 -> 抛 Error。
|
|
12
12
|
const percentToWorkers = (percent) => {
|
|
@@ -18,7 +18,9 @@ const resolveNumberOfWorkers = (value) => {
|
|
|
18
18
|
return percentToWorkers(50);
|
|
19
19
|
};
|
|
20
20
|
if (typeof value === 'number') {
|
|
21
|
-
|
|
21
|
+
if (Number.isInteger(value) && value >= 1) {
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
22
24
|
};
|
|
23
25
|
if (typeof value === 'string') {
|
|
24
26
|
const match = value.trim().match(PERCENT_PATTERN);
|
package/utils/startupBanner.js
CHANGED
|
@@ -7,7 +7,7 @@ const runLog = require('./runLog');
|
|
|
7
7
|
|
|
8
8
|
const VERSION = require('../package.json').version;
|
|
9
9
|
|
|
10
|
-
const KEY_WIDTH =
|
|
10
|
+
const KEY_WIDTH = 19;//键列宽: 需容纳最长键 'monitor install cmd', 改动时同步调
|
|
11
11
|
|
|
12
12
|
const renderTitleBox = (mode) => {
|
|
13
13
|
const left = `multi-tasks v${VERSION}`;
|
|
@@ -27,6 +27,7 @@ const renderFieldLines = (info) => {
|
|
|
27
27
|
['new tasks', `${info.newTasks}`],
|
|
28
28
|
['run.log', info.sysLogFile || '(not available)'],
|
|
29
29
|
['started at', info.startedAt],
|
|
30
|
+
['monitor install cmd', 'npm install multi-tasks-monitor'],
|
|
30
31
|
['monitor start cmd', `npx multi-tasks-monitor --task-dir "${info.taskFolder}" --port 3777`],
|
|
31
32
|
];
|
|
32
33
|
return rows.map(([key, value]) => `${key.padEnd(KEY_WIDTH)} : ${value}`);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//worker 端状态探针(零依赖, perf_hooks 为 Node 内置): unref 定时器按间隔自采——
|
|
2
|
+
//两个增量指标: ELU(eventLoopUtilization 快照差值的 utilization, 0~1, JS 线程打没打满的直接答案)与
|
|
3
|
+
//CPU%(process.cpuUsage 增量/墙钟增量, 单核占比, 用户开 worker_threads 时可 >100, 原样上报);
|
|
4
|
+
//两个内存快照: rss(进程物理内存)与 heapUsed(V8 堆), 原始字节上报, 格式化在 master。
|
|
5
|
+
//通过 send 回调发给 master。只在 worker 进程内运行; interval <= 0 不启动(特性关闭)。
|
|
6
|
+
//unref: 不阻碍进程退出, 不干扰 autoClose; 采样/上报异常只 runLog.warn, 绝不影响任务执行。
|
|
7
|
+
const { performance } = require('perf_hooks');
|
|
8
|
+
|
|
9
|
+
const runLog = require('./runLog');
|
|
10
|
+
|
|
11
|
+
const start = (intervalMs, send) => {
|
|
12
|
+
if (!(intervalMs > 0)) {
|
|
13
|
+
return null;
|
|
14
|
+
};
|
|
15
|
+
let lastElu = performance.eventLoopUtilization();
|
|
16
|
+
let lastCpu = process.cpuUsage();
|
|
17
|
+
const timer = setInterval(() => {
|
|
18
|
+
try {
|
|
19
|
+
const wallMs = intervalMs;//定时器间隔即墙钟窗口: fake timers/真实运行一致, 漂移纳入分子分母可忽略
|
|
20
|
+
const curElu = performance.eventLoopUtilization();
|
|
21
|
+
const elu = performance.eventLoopUtilization(curElu, lastElu).utilization;
|
|
22
|
+
const cpuDelta = process.cpuUsage(lastCpu);
|
|
23
|
+
const cpu = ((cpuDelta.user + cpuDelta.system) / (wallMs * 1000)) * 100;
|
|
24
|
+
const mem = process.memoryUsage();//~0.6µs/次, 每帧一次可忽略
|
|
25
|
+
lastElu = curElu;
|
|
26
|
+
lastCpu = process.cpuUsage();
|
|
27
|
+
send({ elu, cpu, rss: mem.rss, heapUsed: mem.heapUsed });
|
|
28
|
+
} catch (e) {
|
|
29
|
+
runLog.warn('[Worker]: status probe failed, skip this frame', e);
|
|
30
|
+
};
|
|
31
|
+
}, intervalMs);
|
|
32
|
+
timer.unref();
|
|
33
|
+
return {
|
|
34
|
+
stop: () => { clearInterval(timer); },
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
module.exports = { start };
|