hm-pt-core 1.0.0

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 ADDED
@@ -0,0 +1,32 @@
1
+ # hm-pt-core
2
+
3
+ DFX perf trace 公共层:跨 **hiperf_txt_parser** 与 **hmtrace-parser** 共享的数据类型与 trace format 解码原语。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install hm-pt-core
9
+ ```
10
+
11
+ ## 导出
12
+
13
+ - **perf 类型**:`RecordSample`, `PerfData`, `CallchainEntry`, `RawEntry`, …
14
+ - **trace 机制**:`parseTraceFormat`, `buildTraceParserRegistry`, `decodeRawByRegistry`, `tokenizePrintfFormat`, …
15
+ - **hitrace 契约**:`HitraceRecordSnapshot`, `HitraceThreadIndexSnapshot`
16
+ - **工具**:`isPerfDataLike`
17
+
18
+ ## 版本兼容
19
+
20
+ | hm-pt-core | hiperf_txt_parser | hmtrace-parser |
21
+ |------------|-------------------|----------------|
22
+ | 1.0.x | 2.0.x | 0.8.x |
23
+
24
+ ## 开发
25
+
26
+ ```bash
27
+ npm install
28
+ npm run build
29
+ npm run test:all
30
+ ```
31
+
32
+ 提交前需依次跑通 `npm run build` 与 `npm run test:all`(功能测试 + 性能测试)。
@@ -0,0 +1,16 @@
1
+ /**
2
+ * hitrace 与 perf 合并侧车数据契约(可 structuredClone 进 Worker)。
3
+ * 由 hmtrace-parser `buildHitraceIndexSnapshot` 构建。
4
+ */
5
+ /** 预处理后的单条 hitrace 记录(桶内按 timestampNs 升序) */
6
+ export type HitraceRecordSnapshot = {
7
+ timestampNs: number;
8
+ functionName: string;
9
+ fieldDict: Record<string, string>;
10
+ /** 已废弃:hmtrace-parser 不再写入;仅当无 fieldDict 时作遗留 fallback 尝试 decode raw */
11
+ rawLines?: Array<{
12
+ hex: string;
13
+ }>;
14
+ };
15
+ /** 线程索引键 `${tgid}:${pid}`(hitrace 语义;对齐 hiperf sample.pid/tgid) */
16
+ export type HitraceThreadIndexSnapshot = Record<string, readonly HitraceRecordSnapshot[]>;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * hitrace 与 perf 合并侧车数据契约(可 structuredClone 进 Worker)。
3
+ * 由 hmtrace-parser `buildHitraceIndexSnapshot` 构建。
4
+ */
5
+ export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * hm-pt-core:perf trace 公共类型与 trace format 解码原语。
3
+ */
4
+ export type { RecordSampleHeader, CallchainEntry, RawEntry, CallchainFramesEntry, RecordSample, PerfData, } from "./perf/types.js";
5
+ export { isPerfDataLike } from "./perf/is_perf_data_like.js";
6
+ export type { HitraceRecordSnapshot, HitraceThreadIndexSnapshot, } from "./hitrace/contract.js";
7
+ export type { TraceFormatField, ParsedTraceFormat, FieldDictTransformFn, TraceParserRegistry, DecodedRawSample, TracePrintMode, DecodeRawByRegistryOptions, } from "./trace/types.js";
8
+ export type { Endian } from "./trace/parse.js";
9
+ export type { PrintfSpec } from "./trace/printf.js";
10
+ export { parseTraceFormat, parseCommonFieldsFromRaw, parseAllFieldsFromRaw, rawHexLinesToBuffer, bufferToRawHexLines, buildTraceParserRegistry, } from "./trace/parse.js";
11
+ export { formatPrintfValue, tokenizePrintfFormat } from "./trace/printf.js";
12
+ export { decodeRawByRegistry } from "./trace/decode_raw.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * hm-pt-core:perf trace 公共类型与 trace format 解码原语。
3
+ */
4
+ export { isPerfDataLike } from "./perf/is_perf_data_like.js";
5
+ export { parseTraceFormat, parseCommonFieldsFromRaw, parseAllFieldsFromRaw, rawHexLinesToBuffer, bufferToRawHexLines, buildTraceParserRegistry, } from "./trace/parse.js";
6
+ export { formatPrintfValue, tokenizePrintfFormat } from "./trace/printf.js";
7
+ export { decodeRawByRegistry } from "./trace/decode_raw.js";
@@ -0,0 +1,3 @@
1
+ import type { PerfData } from "./types.js";
2
+ /** 是否为 `{ recordSamples: ... }` 形态的 {@link PerfData}(用于统一 API 入参判别)。 */
3
+ export declare function isPerfDataLike(x: unknown): x is PerfData;
@@ -0,0 +1,8 @@
1
+ /** 是否为 `{ recordSamples: ... }` 形态的 {@link PerfData}(用于统一 API 入参判别)。 */
2
+ export function isPerfDataLike(x) {
3
+ return (typeof x === "object" &&
4
+ x !== null &&
5
+ !Array.isArray(x) &&
6
+ "recordSamples" in x &&
7
+ Array.isArray(x.recordSamples));
8
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 与 hiperf 导出结构对应的 **核心数据类型**。
3
+ */
4
+ /** 单条 `record sample` 首行的 type / misc / size 数值。 */
5
+ export interface RecordSampleHeader {
6
+ type: number;
7
+ misc: number;
8
+ size: number;
9
+ }
10
+ export interface CallchainEntry {
11
+ nr: number;
12
+ addresses: string[];
13
+ }
14
+ export interface RawEntry {
15
+ size: number;
16
+ lines: Array<{
17
+ hex: string;
18
+ short?: string;
19
+ }>;
20
+ }
21
+ interface ServerEntry {
22
+ nr: number;
23
+ pids: number[];
24
+ }
25
+ export interface CallchainFramesEntry {
26
+ count: number;
27
+ frames: string[];
28
+ }
29
+ export interface RecordSample {
30
+ header: RecordSampleHeader;
31
+ sample_type: string;
32
+ id: number;
33
+ ip: string;
34
+ pid: number;
35
+ tid: number;
36
+ time: number;
37
+ stream_id: number;
38
+ cpu: number;
39
+ res: number;
40
+ period: number;
41
+ callchain?: CallchainEntry;
42
+ raw?: RawEntry;
43
+ /** tracePrintMode 为 fieldDict 且解码成功时:print 各参数键值(用于导出 txt/json) */
44
+ traceFieldDict?: Record<string, string>;
45
+ server?: ServerEntry;
46
+ callchainFrames?: CallchainFramesEntry;
47
+ }
48
+ export type PerfData = {
49
+ recordSamples: RecordSample[];
50
+ };
51
+ export {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 与 hiperf 导出结构对应的 **核心数据类型**。
3
+ */
4
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 对一条 raw 数据先解 common_*,再按 common_type 选择解析器,最后按 print fmt 渲染。
3
+ * 找不到解析器时返回 skipped=true(不打印日志)。
4
+ */
5
+ import type { DecodeRawByRegistryOptions, DecodedRawSample, TraceParserRegistry } from "./types.js";
6
+ export declare function decodeRawByRegistry(raw: Uint8Array, registry: TraceParserRegistry, options?: DecodeRawByRegistryOptions): Omit<DecodedRawSample, "sampleIndex">;
@@ -0,0 +1,53 @@
1
+ import { parseAllFieldsFromRaw, parseCommonFieldsFromRaw } from "./parse.js";
2
+ import { renderPrintFmt, renderPrintFmtAsFieldDict } from "./printf.js";
3
+ export function decodeRawByRegistry(raw, registry, options = {}) {
4
+ const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
5
+ const commonType = raw.length >= 2 ? view.getUint16(0, true) : undefined;
6
+ const commonFields = registry.commonFormat !== undefined
7
+ ? parseCommonFieldsFromRaw(raw, registry.commonFormat)
8
+ : {};
9
+ const commonTypeFromFields = commonFields.common_type;
10
+ const eventId = typeof commonTypeFromFields === "bigint"
11
+ ? Number(commonTypeFromFields)
12
+ : typeof commonTypeFromFields === "number"
13
+ ? commonTypeFromFields
14
+ : commonType;
15
+ const fmt = eventId === undefined ? undefined : registry.byEventId.get(eventId);
16
+ if (eventId === undefined || !fmt) {
17
+ return {
18
+ commonType: eventId,
19
+ commonFields,
20
+ skipped: true,
21
+ };
22
+ }
23
+ const allFields = parseAllFieldsFromRaw(raw, fmt);
24
+ const mode = options.tracePrintMode ?? "printf";
25
+ if (fmt.printFmt && mode === "fieldDict") {
26
+ let renderedFieldDict = renderPrintFmtAsFieldDict(fmt.printFmt, fmt.printArgs, allFields);
27
+ const transform = registry.transformFieldDictByEventId?.get(eventId);
28
+ if (transform) {
29
+ renderedFieldDict = transform({
30
+ eventId,
31
+ eventName: fmt.eventName,
32
+ rawFieldMap: allFields,
33
+ fieldDict: renderedFieldDict,
34
+ });
35
+ }
36
+ return {
37
+ commonType: eventId,
38
+ commonFields,
39
+ eventName: fmt.eventName,
40
+ renderedFieldDict,
41
+ renderedText: JSON.stringify(renderedFieldDict),
42
+ skipped: false,
43
+ };
44
+ }
45
+ const renderedText = fmt.printFmt ? renderPrintFmt(fmt.printFmt, fmt.printArgs, allFields) : undefined;
46
+ return {
47
+ commonType: eventId,
48
+ commonFields,
49
+ eventName: fmt.eventName,
50
+ renderedText,
51
+ skipped: false,
52
+ };
53
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Trace format 文本解析、raw 缓冲与 registry 构建。
3
+ * @module traceFormat
4
+ */
5
+ import type { FieldDictTransformFn, ParsedTraceFormat, TraceParserRegistry } from "./types.js";
6
+ export type Endian = "le" | "be";
7
+ /**
8
+ * 解析 sample/trace_format 风格的文本,得到字段列表
9
+ */
10
+ export declare function parseTraceFormat(text: string): ParsedTraceFormat;
11
+ /**
12
+ * 仅解析 format 中 common_* 字段(按 offset/size/signed,小端)
13
+ */
14
+ export declare function parseCommonFieldsFromRaw(raw: Uint8Array, format: ParsedTraceFormat): Record<string, number | bigint>;
15
+ /**
16
+ * 解析 format 中全部字段(含数组),数组字段名会去掉 [],如 args[6] => args: [...]
17
+ */
18
+ export declare function parseAllFieldsFromRaw(raw: Uint8Array, format: ParsedTraceFormat): Record<string, number | bigint | string | Array<number | bigint>>;
19
+ /**
20
+ * 将 perf 文本里 raw 段的 hex 行拼成连续字节(小端)。
21
+ *
22
+ * 规则:每行视为一个整数 token(如 0x12345678),按该 token 的字节宽度转成 LE 字节序。
23
+ * 例如:0x12345678 => [0x78, 0x56, 0x34, 0x12]
24
+ */
25
+ export declare function rawHexLinesToBuffer(lines: Array<{
26
+ hex: string;
27
+ }>, endian?: Endian): Uint8Array;
28
+ /**
29
+ * 将字节缓冲按小端编码为 raw hex 行。
30
+ * 默认每行 4 字节(即 8 hex digits)。
31
+ */
32
+ export declare function bufferToRawHexLines(raw: Uint8Array, bytesPerLine?: number, endian?: Endian): Array<{
33
+ hex: string;
34
+ }>;
35
+ /**
36
+ * 从多个 trace_format 文本构建解析器集合(按 event ID 索引)。
37
+ *
38
+ * 为了让 fieldDict 模式下能对特定事件的 dict 做二次加工,这里支持将「文本 + 回调」直接绑定:
39
+ *
40
+ * - 传入 string 时:等价于只提供 format 文本,不带回调;
41
+ * - 传入 { text, transformFieldDict } 时:该文本解析出的 eventId 会绑定对应回调。
42
+ *
43
+ * 若用户未配置 transformFieldDict,或未使用 fieldDict 模式,则不会做任何额外处理。
44
+ */
45
+ export declare function buildTraceParserRegistry(formatTexts: Array<string | {
46
+ text: string;
47
+ transformFieldDict?: FieldDictTransformFn;
48
+ }>): TraceParserRegistry;
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Trace format 文本解析、raw 缓冲与 registry 构建。
3
+ * @module traceFormat
4
+ */
5
+ const UTF8_DECODER = new TextDecoder("utf-8", { fatal: false });
6
+ const FIELD_LINE_RE = /^\s*field\s*:\s*([^;]+)\s*;\s*offset\s*:\s*(\d+)\s*;\s*size\s*:\s*(\d+)\s*;\s*signed\s*:\s*([01])\s*;?\s*$/i;
7
+ function hexNibble(ch) {
8
+ const code = ch.charCodeAt(0);
9
+ if (code >= 48 && code <= 57)
10
+ return code - 48; // 0-9
11
+ const lower = code | 32; // to lower
12
+ if (lower >= 97 && lower <= 102)
13
+ return lower - 87; // a-f
14
+ return -1;
15
+ }
16
+ function hexPairToByte(hi, lo) {
17
+ const h = hexNibble(hi);
18
+ const l = hexNibble(lo);
19
+ if (h < 0 || l < 0)
20
+ return 0;
21
+ return (h << 4) | l;
22
+ }
23
+ function splitTypeAndName(rest) {
24
+ const trimmed = rest.trim();
25
+ const m = trimmed.match(/^(.+?)\s+(\w+(?:\[[^\]]*])?)$/);
26
+ if (!m) {
27
+ return { typeName: trimmed, name: trimmed };
28
+ }
29
+ return { typeName: m[1].trim(), name: m[2] };
30
+ }
31
+ /**
32
+ * 解析 sample/trace_format 风格的文本,得到字段列表
33
+ */
34
+ export function parseTraceFormat(text) {
35
+ const lines = text.split(/\r?\n/);
36
+ let eventName;
37
+ let eventId;
38
+ const fields = [];
39
+ let printFmt;
40
+ let printArgs;
41
+ let inFormat = false;
42
+ for (const line of lines) {
43
+ const t = line.trim();
44
+ if (!t)
45
+ continue;
46
+ if (!inFormat) {
47
+ const nameM = t.match(/^name\s*:\s*(.+)$/i);
48
+ if (nameM) {
49
+ eventName = nameM[1].trim();
50
+ continue;
51
+ }
52
+ const idM = t.match(/^id\s*:\s*(\d+)\s*$/i);
53
+ if (idM) {
54
+ eventId = parseInt(idM[1], 10);
55
+ continue;
56
+ }
57
+ if (/^format\s*:/i.test(t)) {
58
+ inFormat = true;
59
+ continue;
60
+ }
61
+ continue;
62
+ }
63
+ if (/^print\s+fmt\s*:/i.test(t)) {
64
+ const pf = t.match(/^print\s+fmt\s*:\s*"([^"]*)"\s*(?:,\s*(.*))?$/i);
65
+ if (pf) {
66
+ printFmt = pf[1];
67
+ const argsPart = pf[2];
68
+ if (argsPart) {
69
+ const exprs = argsPart.match(/REC->[^,]+/g);
70
+ printArgs = exprs ? exprs.map((s) => s.trim()) : [];
71
+ }
72
+ else {
73
+ printArgs = [];
74
+ }
75
+ }
76
+ break;
77
+ }
78
+ const fm = t.match(FIELD_LINE_RE);
79
+ if (!fm)
80
+ continue;
81
+ const { typeName, name } = splitTypeAndName(fm[1]);
82
+ fields.push({
83
+ typeName,
84
+ name,
85
+ offset: parseInt(fm[2], 10),
86
+ size: parseInt(fm[3], 10),
87
+ signed: fm[4] === "1",
88
+ });
89
+ }
90
+ return { eventName, eventId, printFmt, printArgs, fields };
91
+ }
92
+ function normalizeType(t) {
93
+ return t.replace(/\s+/g, " ").trim().toLowerCase();
94
+ }
95
+ /**
96
+ * 从 little-endian 原始缓冲区按字段描述读一个标量
97
+ */
98
+ function readFieldScalar(view, field) {
99
+ const { offset, size, signed, typeName } = field;
100
+ if (offset + size > view.byteLength)
101
+ return undefined;
102
+ const t = normalizeType(typeName);
103
+ if (size === 1) {
104
+ if (signed || t === "signed char") {
105
+ return view.getInt8(offset);
106
+ }
107
+ return view.getUint8(offset);
108
+ }
109
+ if (size === 2) {
110
+ if (signed || t === "short") {
111
+ return view.getInt16(offset, true);
112
+ }
113
+ return view.getUint16(offset, true);
114
+ }
115
+ if (size === 4) {
116
+ if (t.includes("float")) {
117
+ return view.getFloat32(offset, true);
118
+ }
119
+ if (signed || t === "int" || t === "long" /* 32-bit 内核上 */) {
120
+ return view.getInt32(offset, true);
121
+ }
122
+ return view.getUint32(offset, true);
123
+ }
124
+ if (size === 8) {
125
+ if (t.includes("double")) {
126
+ return view.getFloat64(offset, true);
127
+ }
128
+ if (signed || t === "long" || t === "long long" || t === "__s64") {
129
+ return view.getBigInt64(offset, true);
130
+ }
131
+ return view.getBigUint64(offset, true);
132
+ }
133
+ return undefined;
134
+ }
135
+ function parseArrayName(name) {
136
+ const m = name.match(/^(\w+)\[(\d+)\]$/);
137
+ if (!m)
138
+ return undefined;
139
+ return { baseName: m[1], len: parseInt(m[2], 10) };
140
+ }
141
+ function readFieldValue(view, field) {
142
+ const arr = parseArrayName(field.name);
143
+ if (!arr)
144
+ return readFieldScalar(view, field);
145
+ if (arr.len <= 0)
146
+ return [];
147
+ const elemSize = Math.floor(field.size / arr.len);
148
+ if (elemSize <= 0)
149
+ return undefined;
150
+ const values = [];
151
+ for (let i = 0; i < arr.len; i++) {
152
+ const elemField = {
153
+ ...field,
154
+ name: arr.baseName,
155
+ offset: field.offset + i * elemSize,
156
+ size: elemSize,
157
+ };
158
+ const v = readFieldScalar(view, elemField);
159
+ if (v === undefined)
160
+ return undefined;
161
+ values.push(v);
162
+ }
163
+ return values;
164
+ }
165
+ /**
166
+ * 仅解析 format 中 common_* 字段(按 offset/size/signed,小端)
167
+ */
168
+ export function parseCommonFieldsFromRaw(raw, format) {
169
+ const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
170
+ const out = {};
171
+ for (const field of format.fields) {
172
+ if (!field.name.startsWith("common_"))
173
+ continue;
174
+ const v = readFieldScalar(view, field);
175
+ if (v !== undefined) {
176
+ out[field.name] = v;
177
+ }
178
+ }
179
+ return out;
180
+ }
181
+ /**
182
+ * 解析 format 中全部字段(含数组),数组字段名会去掉 [],如 args[6] => args: [...]
183
+ */
184
+ export function parseAllFieldsFromRaw(raw, format) {
185
+ const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
186
+ const out = {};
187
+ for (const field of format.fields) {
188
+ const arr = parseArrayName(field.name);
189
+ const key = arr ? arr.baseName : field.name;
190
+ const t = normalizeType(field.typeName);
191
+ // __data_loc char[] reason: 实际存一个 u32,低 16 位为 offset,高 16 位为 length
192
+ if (t.includes("__data_loc") && t.includes("char")) {
193
+ const locField = {
194
+ ...field,
195
+ typeName: "unsigned int",
196
+ signed: false,
197
+ size: 4,
198
+ };
199
+ const loc = readFieldScalar(view, locField);
200
+ if (typeof loc === "number") {
201
+ const locKey = `__data_loc_${field.name}`;
202
+ out[locKey] = loc;
203
+ const offset = loc & 0xffff;
204
+ const len = (loc >>> 16) & 0xffff;
205
+ if (offset >= 0 && len > 0 && offset + len <= raw.length) {
206
+ const bytes = raw.subarray(offset, offset + len);
207
+ // 去掉末尾 \0
208
+ const nul = bytes.indexOf(0);
209
+ const slice = nul >= 0 ? bytes.subarray(0, nul) : bytes;
210
+ out[field.name] = UTF8_DECODER.decode(slice);
211
+ }
212
+ else {
213
+ out[field.name] = "";
214
+ }
215
+ }
216
+ continue;
217
+ }
218
+ const v = readFieldValue(view, field);
219
+ if (v !== undefined) {
220
+ out[key] = v;
221
+ }
222
+ }
223
+ return out;
224
+ }
225
+ /**
226
+ * 将 perf 文本里 raw 段的 hex 行拼成连续字节(小端)。
227
+ *
228
+ * 规则:每行视为一个整数 token(如 0x12345678),按该 token 的字节宽度转成 LE 字节序。
229
+ * 例如:0x12345678 => [0x78, 0x56, 0x34, 0x12]
230
+ */
231
+ export function rawHexLinesToBuffer(lines, endian = "le") {
232
+ const normalizedTokens = [];
233
+ let totalBytes = 0;
234
+ for (const { hex } of lines) {
235
+ let s = hex.replace(/^0x/i, "").trim();
236
+ if (!s)
237
+ continue;
238
+ if (s.length % 2 === 1)
239
+ s = "0" + s;
240
+ normalizedTokens.push(s);
241
+ totalBytes += s.length / 2;
242
+ }
243
+ const out = new Uint8Array(totalBytes);
244
+ let pos = 0;
245
+ for (const s of normalizedTokens) {
246
+ if (endian === "le") {
247
+ for (let i = s.length - 2; i >= 0; i -= 2) {
248
+ out[pos++] = hexPairToByte(s[i], s[i + 1]);
249
+ }
250
+ }
251
+ else {
252
+ for (let i = 0; i < s.length; i += 2) {
253
+ out[pos++] = hexPairToByte(s[i], s[i + 1]);
254
+ }
255
+ }
256
+ }
257
+ return out;
258
+ }
259
+ /**
260
+ * 将字节缓冲按小端编码为 raw hex 行。
261
+ * 默认每行 4 字节(即 8 hex digits)。
262
+ */
263
+ export function bufferToRawHexLines(raw, bytesPerLine = 4, endian = "le") {
264
+ if (bytesPerLine <= 0)
265
+ return [];
266
+ const out = [];
267
+ for (let i = 0; i < raw.length; i += bytesPerLine) {
268
+ const end = Math.min(i + bytesPerLine, raw.length);
269
+ let value = 0n;
270
+ if (endian === "le") {
271
+ for (let j = 0; j < end - i; j++) {
272
+ value |= BigInt(raw[i + j]) << BigInt(8 * j);
273
+ }
274
+ }
275
+ else {
276
+ for (let j = 0; j < end - i; j++) {
277
+ value = (value << 8n) | BigInt(raw[i + j]);
278
+ }
279
+ }
280
+ const width = (end - i) * 2;
281
+ out.push({ hex: "0x" + value.toString(16).padStart(width, "0") });
282
+ }
283
+ return out;
284
+ }
285
+ /**
286
+ * 从多个 trace_format 文本构建解析器集合(按 event ID 索引)。
287
+ *
288
+ * 为了让 fieldDict 模式下能对特定事件的 dict 做二次加工,这里支持将「文本 + 回调」直接绑定:
289
+ *
290
+ * - 传入 string 时:等价于只提供 format 文本,不带回调;
291
+ * - 传入 { text, transformFieldDict } 时:该文本解析出的 eventId 会绑定对应回调。
292
+ *
293
+ * 若用户未配置 transformFieldDict,或未使用 fieldDict 模式,则不会做任何额外处理。
294
+ */
295
+ export function buildTraceParserRegistry(formatTexts) {
296
+ const byEventId = new Map();
297
+ let commonFormat;
298
+ const transformFieldDictByEventId = new Map();
299
+ for (const src of formatTexts) {
300
+ const text = typeof src === "string" ? src : src.text;
301
+ const fmt = parseTraceFormat(text);
302
+ if (fmt.eventId !== undefined) {
303
+ byEventId.set(fmt.eventId, fmt);
304
+ if (!commonFormat && fmt.fields.some((f) => f.name === "common_type")) {
305
+ commonFormat = fmt;
306
+ }
307
+ if (typeof src !== "string" && src.transformFieldDict) {
308
+ transformFieldDictByEventId.set(fmt.eventId, src.transformFieldDict);
309
+ }
310
+ }
311
+ }
312
+ return {
313
+ byEventId,
314
+ commonFormat,
315
+ transformFieldDictByEventId: transformFieldDictByEventId.size > 0 ? transformFieldDictByEventId : undefined,
316
+ };
317
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * printf / fieldDict 格式化与 print fmt 渲染。
3
+ * @module traceFormat
4
+ */
5
+ type PrintFieldMap = Record<string, number | bigint | string | Array<number | bigint>>;
6
+ /** 与 C printf 接近的转换说明符(不含 * 动态宽度等) */
7
+ export interface PrintfSpec {
8
+ flags: string;
9
+ width?: number;
10
+ precision?: number;
11
+ length: string;
12
+ conv: string;
13
+ }
14
+ /**
15
+ * 按 C printf 子集格式化:标志 -+ #0 空格,宽度,精度,长度 hl ll hh,转换 d i u x X o s p。
16
+ */
17
+ export declare function formatPrintfValue(value: number | bigint | string, spec: PrintfSpec): string;
18
+ /**
19
+ * 将 print fmt 拆成文本、`%%`、与带参数的转换说明符。
20
+ */
21
+ export declare function tokenizePrintfFormat(printFmt: string): Array<{
22
+ kind: "text";
23
+ text: string;
24
+ } | {
25
+ kind: "literal_pct";
26
+ } | {
27
+ kind: "spec";
28
+ spec: PrintfSpec;
29
+ }>;
30
+ export declare function renderPrintFmtAsFieldDict(printFmt: string, printArgs: string[] | undefined, fieldMap: PrintFieldMap): Record<string, string>;
31
+ export declare function renderPrintFmt(printFmt: string, printArgs: string[] | undefined, fieldMap: PrintFieldMap): string;
32
+ export {};
@@ -0,0 +1,360 @@
1
+ /**
2
+ * printf / fieldDict 格式化与 print fmt 渲染。
3
+ * @module traceFormat
4
+ */
5
+ const PRINT_FMT_TOKEN_CACHE = new Map();
6
+ const PRINT_FMT_SPEC_CACHE = new Map();
7
+ const NORMALIZED_PRINT_ARGS_CACHE = new WeakMap();
8
+ const EMPTY_PRINT_ARGS = [];
9
+ const PRINT_FLAG_CHARS = new Set(["-", "+", "#", "0", " "]);
10
+ const VALID_PRINTF_CONV = new Set(["d", "i", "u", "x", "X", "o", "s", "S", "p", "P"]);
11
+ function hasPrintfFlag(flags, f) {
12
+ return flags.includes(f);
13
+ }
14
+ function toBigIntForPrintf(value) {
15
+ if (typeof value === "bigint")
16
+ return value;
17
+ if (typeof value === "string")
18
+ return 0n;
19
+ if (!Number.isFinite(value))
20
+ return 0n;
21
+ return BigInt(Math.trunc(value));
22
+ }
23
+ /** 按 64 位无符号解释(与内核 trace 常见 long 一致) */
24
+ function asUInt64(n) {
25
+ return n & ((1n << 64n) - 1n);
26
+ }
27
+ function applyWidth(s, width, leftAlign, zeroPad) {
28
+ if (width === undefined || s.length >= width)
29
+ return s;
30
+ const padLen = width - s.length;
31
+ const ch = zeroPad && !leftAlign ? "0" : " ";
32
+ if (leftAlign)
33
+ return s + ch.repeat(padLen);
34
+ return ch.repeat(padLen) + s;
35
+ }
36
+ /** 带符号十进制:支持 precision、0 宽度、+- 空格 */
37
+ function formatPrintfD(n, spec) {
38
+ const neg = n < 0n;
39
+ const abs = neg ? -n : n;
40
+ let digits = abs.toString(10);
41
+ const prec = spec.precision !== undefined ? spec.precision : 1;
42
+ digits = digits.padStart(Math.max(prec, digits.length), "0");
43
+ let sign = "";
44
+ if (neg)
45
+ sign = "-";
46
+ else if (hasPrintfFlag(spec.flags, "+"))
47
+ sign = "+";
48
+ else if (hasPrintfFlag(spec.flags, " "))
49
+ sign = " ";
50
+ const body = sign + digits;
51
+ if (spec.width === undefined || body.length >= spec.width)
52
+ return body;
53
+ const padLen = spec.width - body.length;
54
+ if (hasPrintfFlag(spec.flags, "0") && !hasPrintfFlag(spec.flags, "-") && spec.precision === undefined) {
55
+ if (sign)
56
+ return sign + "0".repeat(padLen) + digits;
57
+ return "0".repeat(padLen) + body;
58
+ }
59
+ if (hasPrintfFlag(spec.flags, "-"))
60
+ return body + " ".repeat(padLen);
61
+ return " ".repeat(padLen) + body;
62
+ }
63
+ function formatPrintfUnsignedRadix(unsigned, spec, radix, upper) {
64
+ let body = radix === 16
65
+ ? upper
66
+ ? unsigned.toString(16).toUpperCase()
67
+ : unsigned.toString(16)
68
+ : unsigned.toString(radix);
69
+ const precDefault = 1;
70
+ const prec = spec.precision !== undefined ? spec.precision : precDefault;
71
+ body = body.padStart(Math.max(prec, body.length), "0");
72
+ if (hasPrintfFlag(spec.flags, "#")) {
73
+ if (radix === 16)
74
+ body = (upper ? "0X" : "0x") + body;
75
+ else if (radix === 8 && body !== "0" && !body.startsWith("0"))
76
+ body = "0" + body;
77
+ }
78
+ const zeroPad = hasPrintfFlag(spec.flags, "0") &&
79
+ !hasPrintfFlag(spec.flags, "-") &&
80
+ spec.precision === undefined;
81
+ return applyWidth(body, spec.width, hasPrintfFlag(spec.flags, "-"), zeroPad);
82
+ }
83
+ /**
84
+ * 按 C printf 子集格式化:标志 -+ #0 空格,宽度,精度,长度 hl ll hh,转换 d i u x X o s p。
85
+ */
86
+ export function formatPrintfValue(value, spec) {
87
+ const conv = spec.conv;
88
+ const cl = conv.toLowerCase();
89
+ if (conv === "s" || conv === "S") {
90
+ let s = typeof value === "string" ? value : String(value);
91
+ if (spec.precision !== undefined) {
92
+ s = s.slice(0, spec.precision);
93
+ }
94
+ return applyWidth(s, spec.width, hasPrintfFlag(spec.flags, "-"), false);
95
+ }
96
+ const n = toBigIntForPrintf(value);
97
+ if (conv === "p" || conv === "P") {
98
+ const u = asUInt64(n);
99
+ const upper = conv === "P";
100
+ let body = upper ? u.toString(16).toUpperCase() : u.toString(16);
101
+ const prec = spec.precision !== undefined ? spec.precision : 1;
102
+ body = body.padStart(Math.max(prec, body.length), "0");
103
+ const zeroPad = hasPrintfFlag(spec.flags, "0") &&
104
+ !hasPrintfFlag(spec.flags, "-") &&
105
+ spec.precision === undefined;
106
+ return applyWidth(body, spec.width, hasPrintfFlag(spec.flags, "-"), zeroPad);
107
+ }
108
+ if (cl === "d" || cl === "i") {
109
+ return formatPrintfD(n, spec);
110
+ }
111
+ if (cl === "u" || cl === "x" || cl === "X" || cl === "o") {
112
+ const u = asUInt64(n);
113
+ if (cl === "u") {
114
+ let body = u.toString(10);
115
+ const prec = spec.precision !== undefined ? spec.precision : 1;
116
+ body = body.padStart(Math.max(prec, body.length), "0");
117
+ const zeroPad = hasPrintfFlag(spec.flags, "0") &&
118
+ !hasPrintfFlag(spec.flags, "-") &&
119
+ spec.precision === undefined;
120
+ return applyWidth(body, spec.width, hasPrintfFlag(spec.flags, "-"), zeroPad);
121
+ }
122
+ if (cl === "o") {
123
+ return formatPrintfUnsignedRadix(u, spec, 8, false);
124
+ }
125
+ return formatPrintfUnsignedRadix(u, spec, 16, conv === "X");
126
+ }
127
+ return String(value);
128
+ }
129
+ /**
130
+ * 将 print fmt 拆成文本、`%%`、与带参数的转换说明符。
131
+ */
132
+ export function tokenizePrintfFormat(printFmt) {
133
+ const out = [];
134
+ let i = 0;
135
+ let textBuf = "";
136
+ const flushText = () => {
137
+ if (textBuf.length > 0) {
138
+ out.push({ kind: "text", text: textBuf });
139
+ textBuf = "";
140
+ }
141
+ };
142
+ while (i < printFmt.length) {
143
+ if (printFmt[i] !== "%") {
144
+ textBuf += printFmt[i];
145
+ i++;
146
+ continue;
147
+ }
148
+ flushText();
149
+ const pctPos = i;
150
+ if (i + 1 >= printFmt.length) {
151
+ textBuf += "%";
152
+ i++;
153
+ continue;
154
+ }
155
+ if (printFmt[i + 1] === "%") {
156
+ out.push({ kind: "literal_pct" });
157
+ i += 2;
158
+ continue;
159
+ }
160
+ i++;
161
+ let flags = "";
162
+ while (i < printFmt.length && PRINT_FLAG_CHARS.has(printFmt[i])) {
163
+ flags += printFmt[i];
164
+ i++;
165
+ }
166
+ let width;
167
+ if (i < printFmt.length && printFmt[i] >= "0" && printFmt[i] <= "9") {
168
+ let w = 0;
169
+ while (i < printFmt.length && printFmt[i] >= "0" && printFmt[i] <= "9") {
170
+ w = w * 10 + (printFmt[i].charCodeAt(0) - 48);
171
+ i++;
172
+ }
173
+ width = w;
174
+ }
175
+ let precision;
176
+ if (i < printFmt.length && printFmt[i] === ".") {
177
+ i++;
178
+ if (i < printFmt.length && printFmt[i] >= "0" && printFmt[i] <= "9") {
179
+ let p = 0;
180
+ while (i < printFmt.length && printFmt[i] >= "0" && printFmt[i] <= "9") {
181
+ p = p * 10 + (printFmt[i].charCodeAt(0) - 48);
182
+ i++;
183
+ }
184
+ precision = p;
185
+ }
186
+ else {
187
+ precision = 0;
188
+ }
189
+ }
190
+ let length = "";
191
+ if (i < printFmt.length) {
192
+ if (printFmt[i] === "h") {
193
+ length = "h";
194
+ i++;
195
+ if (i < printFmt.length && printFmt[i] === "h") {
196
+ length = "hh";
197
+ i++;
198
+ }
199
+ }
200
+ else if (printFmt[i] === "l") {
201
+ length = "l";
202
+ i++;
203
+ if (i < printFmt.length && printFmt[i] === "l") {
204
+ length = "ll";
205
+ i++;
206
+ }
207
+ }
208
+ else if (printFmt[i] === "L" ||
209
+ printFmt[i] === "j" ||
210
+ printFmt[i] === "z" ||
211
+ printFmt[i] === "t") {
212
+ length = printFmt[i];
213
+ i++;
214
+ }
215
+ }
216
+ if (i >= printFmt.length) {
217
+ textBuf += printFmt.slice(pctPos, i);
218
+ break;
219
+ }
220
+ const conv = printFmt[i];
221
+ i++;
222
+ if (!VALID_PRINTF_CONV.has(conv)) {
223
+ textBuf += printFmt.slice(pctPos, i);
224
+ continue;
225
+ }
226
+ out.push({ kind: "spec", spec: { flags, width, precision, length, conv } });
227
+ }
228
+ flushText();
229
+ return out;
230
+ }
231
+ function extractPrintfSpecs(printFmt) {
232
+ const cached = PRINT_FMT_SPEC_CACHE.get(printFmt);
233
+ if (cached)
234
+ return cached;
235
+ const specs = tokenizePrintfFormat(printFmt)
236
+ .filter((t) => t.kind === "spec")
237
+ .map((t) => t.spec);
238
+ PRINT_FMT_SPEC_CACHE.set(printFmt, specs);
239
+ return specs;
240
+ }
241
+ function getCachedPrintfTokens(printFmt) {
242
+ const cached = PRINT_FMT_TOKEN_CACHE.get(printFmt);
243
+ if (cached)
244
+ return cached;
245
+ const tokens = tokenizePrintfFormat(printFmt);
246
+ PRINT_FMT_TOKEN_CACHE.set(printFmt, tokens);
247
+ return tokens;
248
+ }
249
+ function normalizePrintExpr(exprRaw) {
250
+ let expr = exprRaw.trim();
251
+ while (expr.startsWith("("))
252
+ expr = expr.slice(1).trim();
253
+ while (expr.endsWith(")"))
254
+ expr = expr.slice(0, -1).trim();
255
+ expr = expr.replace(/\s+/g, "");
256
+ return expr;
257
+ }
258
+ function getNormalizedPrintArgs(printArgs) {
259
+ if (!printArgs || printArgs.length === 0)
260
+ return EMPTY_PRINT_ARGS;
261
+ const cached = NORMALIZED_PRINT_ARGS_CACHE.get(printArgs);
262
+ if (cached)
263
+ return cached;
264
+ const normalizedArgs = printArgs.map(normalizePrintExpr);
265
+ NORMALIZED_PRINT_ARGS_CACHE.set(printArgs, normalizedArgs);
266
+ return normalizedArgs;
267
+ }
268
+ function evalPrintArgExpr(exprRaw, fieldMap) {
269
+ const expr = normalizePrintExpr(exprRaw);
270
+ let m = expr.match(/^REC->(\w+)&0xffff$/);
271
+ if (m) {
272
+ const v = fieldMap[m[1]];
273
+ const n = typeof v === "number" ? v : 0;
274
+ return n & 0xffff;
275
+ }
276
+ m = expr.match(/^REC->(\w+)>>16$/);
277
+ if (m) {
278
+ const v = fieldMap[m[1]];
279
+ const n = typeof v === "number" ? v : 0;
280
+ return (n >>> 16) & 0xffff;
281
+ }
282
+ m = expr.match(/^REC->(\w+)(?:\[(\d+)])?$/);
283
+ if (!m)
284
+ return 0;
285
+ const name = m[1];
286
+ const idxRaw = m[2];
287
+ const v = fieldMap[name];
288
+ if (idxRaw !== undefined) {
289
+ const idx = parseInt(idxRaw, 10);
290
+ if (Array.isArray(v) && idx >= 0 && idx < v.length) {
291
+ return v[idx];
292
+ }
293
+ return 0;
294
+ }
295
+ if (Array.isArray(v))
296
+ return v[0] ?? 0;
297
+ return v ?? 0;
298
+ }
299
+ function buildPrintArgValues(printArgs, fieldMap) {
300
+ const normalizedArgs = getNormalizedPrintArgs(printArgs);
301
+ const values = (printArgs ?? []).map((e) => evalPrintArgExpr(e, fieldMap));
302
+ return { normalizedArgs, values };
303
+ }
304
+ function formatOnePrintArg(idx, spec, fieldMap, normalizedArgs, values) {
305
+ const v = values[idx] ?? 0;
306
+ const argExpr = normalizedArgs[idx] ?? "";
307
+ if (spec.conv === "s" || spec.conv === "S") {
308
+ const m = argExpr.match(/^REC->__data_loc_(\w+)(?:&0xffff)?(?:>>16)?$/);
309
+ if (m) {
310
+ const s = fieldMap[m[1]];
311
+ if (typeof s === "string") {
312
+ return formatPrintfValue(s, spec);
313
+ }
314
+ }
315
+ }
316
+ return formatPrintfValue(v, spec);
317
+ }
318
+ function fieldKeyFromNormalizedArg(normalizedArg, index) {
319
+ let k = normalizedArg;
320
+ if (k.startsWith("REC->"))
321
+ k = k.slice(5);
322
+ // REC->__data_loc_path&0xffff 在 fieldDict 中收敛为 path 作为键名
323
+ const mDataLocOff = k.match(/^__data_loc_(\w+)&0xffff$/);
324
+ if (mDataLocOff) {
325
+ return mDataLocOff[1];
326
+ }
327
+ return k.length > 0 ? k : `arg${index}`;
328
+ }
329
+ export function renderPrintFmtAsFieldDict(printFmt, printArgs, fieldMap) {
330
+ const { normalizedArgs, values } = buildPrintArgValues(printArgs, fieldMap);
331
+ const specs = extractPrintfSpecs(printFmt);
332
+ const out = {};
333
+ const seen = new Map();
334
+ for (let i = 0; i < specs.length; i++) {
335
+ const base = fieldKeyFromNormalizedArg(normalizedArgs[i] ?? "", i);
336
+ const n = (seen.get(base) ?? 0) + 1;
337
+ seen.set(base, n);
338
+ const key = n === 1 ? base : `${base}__${n}`;
339
+ out[key] = formatOnePrintArg(i, specs[i], fieldMap, normalizedArgs, values);
340
+ }
341
+ return out;
342
+ }
343
+ export function renderPrintFmt(printFmt, printArgs, fieldMap) {
344
+ const { normalizedArgs, values } = buildPrintArgValues(printArgs, fieldMap);
345
+ const tokens = getCachedPrintfTokens(printFmt);
346
+ let argIdx = 0;
347
+ let out = "";
348
+ for (const tok of tokens) {
349
+ if (tok.kind === "text") {
350
+ out += tok.text;
351
+ }
352
+ else if (tok.kind === "literal_pct") {
353
+ out += "%";
354
+ }
355
+ else {
356
+ out += formatOnePrintArg(argIdx++, tok.spec, fieldMap, normalizedArgs, values);
357
+ }
358
+ }
359
+ return out;
360
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Trace format 相关类型定义。
3
+ */
4
+ export interface TraceFormatField {
5
+ /** 完整类型串,如 "unsigned short"、"int" */
6
+ typeName: string;
7
+ /** 字段名,如 common_type、args[6] */
8
+ name: string;
9
+ offset: number;
10
+ size: number;
11
+ signed: boolean;
12
+ }
13
+ export interface ParsedTraceFormat {
14
+ eventName?: string;
15
+ eventId?: number;
16
+ printFmt?: string;
17
+ printArgs?: string[];
18
+ fields: TraceFormatField[];
19
+ }
20
+ /** 在 fieldDict 模式下,对某个事件的 fieldDict 做二次加工的回调 */
21
+ export type FieldDictTransformFn = (params: {
22
+ eventId?: number;
23
+ eventName?: string;
24
+ rawFieldMap: Record<string, number | bigint | string | Array<number | bigint>>;
25
+ fieldDict: Record<string, string>;
26
+ }) => Record<string, string>;
27
+ export interface TraceParserRegistry {
28
+ byEventId: Map<number, ParsedTraceFormat>;
29
+ commonFormat?: ParsedTraceFormat;
30
+ transformFieldDictByEventId?: Map<number, FieldDictTransformFn>;
31
+ }
32
+ export interface DecodedRawSample {
33
+ sampleIndex: number;
34
+ commonType?: number;
35
+ commonFields: Record<string, number | bigint>;
36
+ eventName?: string;
37
+ renderedText?: string;
38
+ renderedFieldDict?: Record<string, string>;
39
+ skipped: boolean;
40
+ }
41
+ /** printf:整条 print fmt 渲染为一句;fieldDict:按格式符逐项生成字段名 → 格式化字符串 */
42
+ export type TracePrintMode = "printf" | "fieldDict";
43
+ export interface DecodeRawByRegistryOptions {
44
+ tracePrintMode?: TracePrintMode;
45
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Trace format 相关类型定义。
3
+ */
4
+ export {};
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "hm-pt-core",
3
+ "version": "1.0.0",
4
+ "description": "DFX perf trace shared types and trace format decode primitives",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "prepublishOnly": "npm run build",
20
+ "test": "npm run build && node --test tests/*.test.mjs",
21
+ "test:perf": "node --test tests/*.perf.mjs",
22
+ "test:all": "npm run test && npm run test:perf"
23
+ },
24
+ "keywords": [
25
+ "perf",
26
+ "trace",
27
+ "hm"
28
+ ],
29
+ "author": "",
30
+ "license": "MIT",
31
+ "devDependencies": {
32
+ "@types/node": "^22.10.1",
33
+ "typescript": "^5.7.2"
34
+ }
35
+ }