mm_cost 0.0.0-stage → 1.0.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 CHANGED
@@ -1,3 +1,36 @@
1
- # Temporary Holding Version
1
+ # mm_cost 耗时统计模块
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 按 name 维度统计操作耗时与次数:`begin/end` 手动打点,或 `wrap` 包装异步函数(异常也计入耗时)。数据保存在进程内存中,可通过 `snapshot()` 导出对接 mm_metrics 上报。
4
+
5
+ ## 快速开始
6
+
7
+ ```javascript
8
+ const { costAdmin } = require('mm_cost');
9
+
10
+ const cost = costAdmin('web');
11
+
12
+ // 方式一:手动打点
13
+ const end = cost.begin('db.query');
14
+ const rows = await db.query(sql);
15
+ end();
16
+
17
+ // 方式二:包装异步函数
18
+ const safe_call = cost.wrap('http.call', (url) => fetch(url));
19
+ await safe_call('https://example.com');
20
+
21
+ // 导出统计:{ name, count, avg, min, max, total }
22
+ console.log(cost.snapshot());
23
+ ```
24
+
25
+ ## API
26
+
27
+ | 方法 | 说明 |
28
+ | --- | --- |
29
+ | `begin(name)` | 开始打点,返回 `end()` 函数,调用即记录耗时 |
30
+ | `wrap(name, fn)` | 包装异步函数,自动统计每次调用耗时 |
31
+ | `snapshot()` | 导出 `{ name, count, avg, min, max, total }` 快照 |
32
+ | `clear()` | 清空统计 |
33
+
34
+ 默认配置:`max_keys: 10000`(超限淘汰最久未更新的统计项)。
35
+
36
+ `costAdmin(scope, config)` 按作用域缓存实例,相同 `scope` 返回同一实例。
package/index.js ADDED
@@ -0,0 +1,25 @@
1
+ const { Cost } = require('./lib/cost.js');
2
+ const Admin = require('mm_corejs').Admin;
3
+
4
+ /**
5
+ * 耗时统计 Admin 工厂
6
+ * 支持 os.js 调用:require('mm_cost').admin.setServer(os) -> admin.get(scope, config)
7
+ */
8
+ let admin = new Admin();
9
+ admin.setClass(Cost);
10
+
11
+ /**
12
+ * 获取(或创建)指定作用域的耗时统计实例
13
+ * @param {string} [scope] 作用域,缺省 'web'
14
+ * @param {Object} [config] 配置参数
15
+ * @returns {Cost} 耗时统计实例
16
+ */
17
+ function costAdmin(scope, config) {
18
+ return admin.get(scope || 'web', config);
19
+ }
20
+
21
+ module.exports = {
22
+ Cost,
23
+ admin,
24
+ costAdmin
25
+ };
package/lib/cost.js ADDED
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ const { Base } = require('mm_mod');
4
+
5
+ /**
6
+ * 耗时统计类
7
+ *
8
+ * 按 name 维度统计操作耗时与次数:begin/end 手动打点,或 wrap 包装异步函数。
9
+ * 数据保存在进程内存中,可通过 snapshot 导出对接 mm_metrics 上报。
10
+ */
11
+ class Cost extends Base {
12
+ /**
13
+ * 模块默认配置
14
+ */
15
+ static config = {
16
+ name: 'cost',
17
+ title: '耗时统计模块',
18
+ description: '按 name 维度统计操作耗时与次数:begin/end 打点或 wrap 包装',
19
+ // 最大跟踪 name 数,超出后淘汰最久未更新的统计项
20
+ max_keys: 10000
21
+ };
22
+
23
+ /**
24
+ * 统计表:name -> { count, total, min, max, _start(临时打点) }
25
+ * @type {Map<string, Object>}
26
+ */
27
+ #stats = new Map();
28
+
29
+ /**
30
+ * 获取(或创建)统计项
31
+ * @param {string} name 统计维度名称
32
+ * @returns {Object} 统计项
33
+ */
34
+ #item(name) {
35
+ let item = this.#stats.get(name);
36
+ if (!item) {
37
+ if (this.#stats.size >= this.config.max_keys) {
38
+ let oldest_key = null;
39
+ let oldest = Infinity;
40
+ for (const [k, v] of this.#stats) {
41
+ if (v.updated_at < oldest) {
42
+ oldest = v.updated_at;
43
+ oldest_key = k;
44
+ }
45
+ }
46
+ if (oldest_key !== null) {
47
+ this.#stats.delete(oldest_key);
48
+ }
49
+ }
50
+ item = { count: 0, total: 0, min: Infinity, max: 0, updated_at: Date.now() };
51
+ this.#stats.set(name, item);
52
+ }
53
+ return item;
54
+ }
55
+
56
+ /**
57
+ * 开始一次耗时打点
58
+ * @param {string} name 统计维度名称
59
+ * @returns {Function} end 函数:调用即结束打点并返回耗时毫秒
60
+ */
61
+ begin(name) {
62
+ if (!name || typeof name !== 'string') {
63
+ throw new TypeError(`${this._label()} name must be a non-empty string`);
64
+ }
65
+ const start = Date.now();
66
+ return () => this._end(name, Date.now() - start);
67
+ }
68
+
69
+ /**
70
+ * 结束一次打点(内部方法,通常通过 begin 返回的 end 函数调用)
71
+ * @param {string} name 统计维度名称
72
+ * @param {number} elapsed 耗时毫秒
73
+ * @returns {number} 耗时毫秒
74
+ */
75
+ _end(name, elapsed) {
76
+ const item = this.#item(name);
77
+ item.count += 1;
78
+ item.total += elapsed;
79
+ if (elapsed < item.min) {
80
+ item.min = elapsed;
81
+ }
82
+ if (elapsed > item.max) {
83
+ item.max = elapsed;
84
+ }
85
+ item.updated_at = Date.now();
86
+ return elapsed;
87
+ }
88
+
89
+ /**
90
+ * 包装异步函数:自动统计每次调用的耗时
91
+ * @param {string} name 统计维度名称
92
+ * @param {Function} fn 异步函数
93
+ * @returns {Function} 包装后的函数(透传参数与返回值,异常也计入耗时)
94
+ */
95
+ wrap(name, fn) {
96
+ return async (...args) => {
97
+ const end = this.begin(name);
98
+ try {
99
+ return await fn(...args);
100
+ } finally {
101
+ end();
102
+ }
103
+ };
104
+ }
105
+
106
+ /**
107
+ * 导出统计快照
108
+ * @returns {Array<Object>} 每项 { name, count, avg, min, max, total }
109
+ */
110
+ snapshot() {
111
+ const out = [];
112
+ for (const [name, item] of this.#stats) {
113
+ out.push({
114
+ name,
115
+ count: item.count,
116
+ total: item.total,
117
+ avg: item.count > 0 ? Math.round((item.total / item.count) * 100) / 100 : 0,
118
+ min: item.min === Infinity ? 0 : item.min,
119
+ max: item.max
120
+ });
121
+ }
122
+ return out.sort((a, b) => a.name.localeCompare(b.name));
123
+ }
124
+
125
+ /**
126
+ * 清空统计
127
+ * @returns {Cost} 当前实例
128
+ */
129
+ clear() {
130
+ this.#stats.clear();
131
+ return this;
132
+ }
133
+ }
134
+
135
+ module.exports = { Cost };
package/package.json CHANGED
@@ -1,6 +1,27 @@
1
1
  {
2
- "name": "mm_cost",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "mm_cost",
3
+ "version": "1.0.1",
4
+ "description": "超级美眉耗时统计模块:按 name 维度统计操作耗时与次数,begin/end 打点或 wrap 包装",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node test.js && node --test tests/cost.test.js"
8
+ },
9
+ "keywords": [
10
+ "mm_mod",
11
+ "mm_middleware",
12
+ "cost",
13
+ "timing",
14
+ "profiling"
15
+ ],
16
+ "layer": "middleware",
17
+ "group": "web",
18
+ "author": "邱文武",
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "mm_corejs": "^1.1.4",
22
+ "mm_mod": "^1.0.1"
23
+ },
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ }
27
+ }
package/test.js ADDED
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+ const assert = require('assert');
3
+ const mod = require('./index.js');
4
+
5
+ assert.strictEqual(typeof mod.Cost, 'function', '应导出 Cost 类');
6
+ assert.strictEqual(typeof mod.costAdmin, 'function', '应导出 costAdmin 函数');
7
+
8
+ const c = mod.costAdmin('smoke');
9
+ assert.ok(c instanceof mod.Cost);
10
+ const end = c.begin('op');
11
+ end();
12
+ const snap = c.snapshot();
13
+ assert.strictEqual(snap.length, 1);
14
+ assert.strictEqual(snap[0].count, 1);
15
+
16
+ console.log('mm_cost 模块测试通过');
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+ /**
3
+ * mm_cost 单元测试
4
+ */
5
+ const test = require('node:test');
6
+ const assert = require('assert');
7
+
8
+ const { Cost, costAdmin } = require('../index.js');
9
+
10
+ test('begin/end 打点统计', async () => {
11
+ const c = new Cost();
12
+ const end = c.begin('db.query');
13
+ await new Promise((r) => setTimeout(r, 25));
14
+ const elapsed = end();
15
+ assert.ok(elapsed >= 20, `耗时应 >= 20ms,实际 ${elapsed}`);
16
+ const snap = c.snapshot();
17
+ assert.strictEqual(snap.length, 1);
18
+ const s = snap[0];
19
+ assert.strictEqual(s.name, 'db.query');
20
+ assert.strictEqual(s.count, 1);
21
+ assert.strictEqual(s.total, elapsed);
22
+ assert.strictEqual(s.min, elapsed);
23
+ assert.strictEqual(s.max, elapsed);
24
+ assert.ok(s.avg > 0);
25
+ assert.throws(() => c.begin(''), TypeError);
26
+ });
27
+
28
+ test('多次打点:count/avg/min/max 汇总', () => {
29
+ const c = new Cost();
30
+ c._end('op', 10);
31
+ c._end('op', 30);
32
+ c._end('op', 20);
33
+ const s = c.snapshot()[0];
34
+ assert.strictEqual(s.count, 3);
35
+ assert.strictEqual(s.total, 60);
36
+ assert.strictEqual(s.min, 10);
37
+ assert.strictEqual(s.max, 30);
38
+ assert.strictEqual(s.avg, 20);
39
+ });
40
+
41
+ test('wrap 包装异步函数:成功与异常均计入', async () => {
42
+ const c = new Cost();
43
+ const fn = c.wrap('job', async (x) => x * 2);
44
+ assert.strictEqual(await fn(21), 42);
45
+ const bad = c.wrap('bad', async () => { throw new Error('x'); });
46
+ await assert.rejects(() => bad(), /x/);
47
+ const snap = Object.fromEntries(c.snapshot().map((s) => [s.name, s]));
48
+ assert.strictEqual(snap.job.count, 1, '成功调用应计入');
49
+ assert.strictEqual(snap.bad.count, 1, '异常调用也应计入');
50
+ // 透传参数与返回值
51
+ assert.strictEqual(await fn(5), 10);
52
+ assert.strictEqual(c.snapshot().find((s) => s.name === 'job').count, 2);
53
+ });
54
+
55
+ test('不同 name 独立统计,快照按名称排序', () => {
56
+ const c = new Cost();
57
+ c._end('c', 1);
58
+ c._end('a', 2);
59
+ c._end('b', 3);
60
+ assert.deepStrictEqual(c.snapshot().map((s) => s.name), ['a', 'b', 'c']);
61
+ });
62
+
63
+ test('clear 与 max_keys 淘汰', async () => {
64
+ const c = new Cost();
65
+ c._end('x', 1);
66
+ c.clear();
67
+ assert.strictEqual(c.snapshot().length, 0);
68
+
69
+ const limited = new Cost({ max_keys: 2 });
70
+ const tick = () => new Promise((r) => setTimeout(r, 3));
71
+ limited._end('k1', 1);
72
+ await tick();
73
+ limited._end('k2', 1);
74
+ await tick();
75
+ limited._end('k1', 1); // k1 变为最近使用
76
+ await tick();
77
+ limited._end('k3', 1); // 淘汰 k2
78
+ const names = limited.snapshot().map((s) => s.name);
79
+ assert.ok(!names.includes('k2'), 'k2 应被淘汰');
80
+ assert.ok(names.includes('k1') && names.includes('k3'));
81
+ });
82
+
83
+ test('工厂函数按作用域共享实例', () => {
84
+ const a = costAdmin('ut-cost', {});
85
+ const b = costAdmin('ut-cost');
86
+ assert.strictEqual(a, b);
87
+ assert.notStrictEqual(costAdmin('ut-cost-2'), a);
88
+ });