hm-pt-core 1.0.0 → 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/dist/index.d.ts CHANGED
@@ -5,7 +5,8 @@ export type { RecordSampleHeader, CallchainEntry, RawEntry, CallchainFramesEntry
5
5
  export { isPerfDataLike } from "./perf/is_perf_data_like.js";
6
6
  export type { HitraceRecordSnapshot, HitraceThreadIndexSnapshot, } from "./hitrace/contract.js";
7
7
  export type { TraceFormatField, ParsedTraceFormat, FieldDictTransformFn, TraceParserRegistry, DecodedRawSample, TracePrintMode, DecodeRawByRegistryOptions, } from "./trace/types.js";
8
- export type { Endian } from "./trace/parse.js";
8
+ export type { Endian, BuildTraceParserRegistryOptions } from "./trace/parse.js";
9
+ export { TraceFieldDecodeError } from "./trace/parse.js";
9
10
  export type { PrintfSpec } from "./trace/printf.js";
10
11
  export { parseTraceFormat, parseCommonFieldsFromRaw, parseAllFieldsFromRaw, rawHexLinesToBuffer, bufferToRawHexLines, buildTraceParserRegistry, } from "./trace/parse.js";
11
12
  export { formatPrintfValue, tokenizePrintfFormat } from "./trace/printf.js";
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * hm-pt-core:perf trace 公共类型与 trace format 解码原语。
3
3
  */
4
4
  export { isPerfDataLike } from "./perf/is_perf_data_like.js";
5
+ export { TraceFieldDecodeError } from "./trace/parse.js";
5
6
  export { parseTraceFormat, parseCommonFieldsFromRaw, parseAllFieldsFromRaw, rawHexLinesToBuffer, bufferToRawHexLines, buildTraceParserRegistry, } from "./trace/parse.js";
6
7
  export { formatPrintfValue, tokenizePrintfFormat } from "./trace/printf.js";
7
8
  export { decodeRawByRegistry } from "./trace/decode_raw.js";
@@ -4,6 +4,17 @@
4
4
  */
5
5
  import type { FieldDictTransformFn, ParsedTraceFormat, TraceParserRegistry } from "./types.js";
6
6
  export type Endian = "le" | "be";
7
+ export declare class TraceFieldDecodeError extends Error {
8
+ readonly fieldName: string;
9
+ readonly offset: number;
10
+ readonly size: number;
11
+ readonly rawLength: number;
12
+ constructor(message: string, fieldName: string, offset: number, size: number, rawLength: number);
13
+ }
14
+ export interface BuildTraceParserRegistryOptions {
15
+ /** 允许后注册的 format 覆盖同 ID 事件;默认 false,重复 ID 会抛错 */
16
+ allowDuplicateEventIds?: boolean;
17
+ }
7
18
  /**
8
19
  * 解析 sample/trace_format 风格的文本,得到字段列表
9
20
  */
@@ -45,4 +56,4 @@ export declare function bufferToRawHexLines(raw: Uint8Array, bytesPerLine?: numb
45
56
  export declare function buildTraceParserRegistry(formatTexts: Array<string | {
46
57
  text: string;
47
58
  transformFieldDict?: FieldDictTransformFn;
48
- }>): TraceParserRegistry;
59
+ }>, options?: BuildTraceParserRegistryOptions): TraceParserRegistry;
@@ -3,6 +3,20 @@
3
3
  * @module traceFormat
4
4
  */
5
5
  const UTF8_DECODER = new TextDecoder("utf-8", { fatal: false });
6
+ export class TraceFieldDecodeError extends Error {
7
+ fieldName;
8
+ offset;
9
+ size;
10
+ rawLength;
11
+ constructor(message, fieldName, offset, size, rawLength) {
12
+ super(message);
13
+ this.fieldName = fieldName;
14
+ this.offset = offset;
15
+ this.size = size;
16
+ this.rawLength = rawLength;
17
+ this.name = "TraceFieldDecodeError";
18
+ }
19
+ }
6
20
  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
21
  function hexNibble(ch) {
8
22
  const code = ch.charCodeAt(0);
@@ -16,10 +30,129 @@ function hexNibble(ch) {
16
30
  function hexPairToByte(hi, lo) {
17
31
  const h = hexNibble(hi);
18
32
  const l = hexNibble(lo);
19
- if (h < 0 || l < 0)
20
- return 0;
33
+ if (h < 0 || l < 0) {
34
+ throw new Error(`Invalid hex byte: "${hi}${lo}"`);
35
+ }
21
36
  return (h << 4) | l;
22
37
  }
38
+ function readCharArrayAsString(raw, offset, size, fieldName) {
39
+ if (offset < 0 || offset + size > raw.length) {
40
+ throw new TraceFieldDecodeError(`Field "${fieldName}" out of bounds: offset=${offset}, size=${size}, raw.length=${raw.length}`, fieldName, offset, size, raw.length);
41
+ }
42
+ const bytes = raw.subarray(offset, offset + size);
43
+ const nul = bytes.indexOf(0);
44
+ const slice = nul >= 0 ? bytes.subarray(0, nul) : bytes;
45
+ return UTF8_DECODER.decode(slice);
46
+ }
47
+ function decodePrintFmtEscape(line, escapeStart) {
48
+ let i = escapeStart + 1;
49
+ if (i >= line.length) {
50
+ throw new Error(`Incomplete escape in print fmt string literal at index ${escapeStart}`);
51
+ }
52
+ const esc = line[i];
53
+ i++;
54
+ switch (esc) {
55
+ case '"':
56
+ return { value: '"', next: i };
57
+ case "\\":
58
+ return { value: "\\", next: i };
59
+ case "n":
60
+ return { value: "\n", next: i };
61
+ case "t":
62
+ return { value: "\t", next: i };
63
+ case "r":
64
+ return { value: "\r", next: i };
65
+ case "x": {
66
+ if (i + 1 >= line.length) {
67
+ throw new Error(`Incomplete \\x escape in print fmt string literal at index ${escapeStart}`);
68
+ }
69
+ const hi = hexNibble(line[i]);
70
+ const lo = hexNibble(line[i + 1]);
71
+ if (hi < 0 || lo < 0) {
72
+ throw new Error(`Invalid \\x escape in print fmt string literal at index ${escapeStart}`);
73
+ }
74
+ return { value: String.fromCharCode((hi << 4) | lo), next: i + 2 };
75
+ }
76
+ default: {
77
+ if (esc >= "0" && esc <= "7") {
78
+ let oct = esc.charCodeAt(0) - 48;
79
+ let digits = 1;
80
+ while (digits < 3 && i < line.length && line[i] >= "0" && line[i] <= "7") {
81
+ oct = oct * 8 + (line[i].charCodeAt(0) - 48);
82
+ i++;
83
+ digits++;
84
+ }
85
+ return { value: String.fromCharCode(oct & 0xff), next: i };
86
+ }
87
+ return { value: "\\" + esc, next: i };
88
+ }
89
+ }
90
+ }
91
+ /** 扫描 print fmt 行中的双引号字符串字面量,支持常见 C 字符串转义 */
92
+ function parsePrintFmtStringLiteral(line, start) {
93
+ if (line[start] !== '"')
94
+ return undefined;
95
+ let i = start + 1;
96
+ let value = "";
97
+ while (i < line.length) {
98
+ const ch = line[i];
99
+ if (ch === "\\") {
100
+ const decoded = decodePrintFmtEscape(line, i);
101
+ value += decoded.value;
102
+ i = decoded.next;
103
+ continue;
104
+ }
105
+ if (ch === '"') {
106
+ return { value, end: i + 1 };
107
+ }
108
+ value += ch;
109
+ i++;
110
+ }
111
+ return undefined;
112
+ }
113
+ function scanBalancedPrintArgs(argsPart) {
114
+ const args = [];
115
+ let i = 0;
116
+ while (i < argsPart.length) {
117
+ while (i < argsPart.length && /\s/.test(argsPart[i]))
118
+ i++;
119
+ if (i >= argsPart.length)
120
+ break;
121
+ const start = i;
122
+ let depth = 0;
123
+ while (i < argsPart.length) {
124
+ const ch = argsPart[i];
125
+ if (ch === "(")
126
+ depth++;
127
+ else if (ch === ")")
128
+ depth = Math.max(0, depth - 1);
129
+ else if (ch === "," && depth === 0)
130
+ break;
131
+ i++;
132
+ }
133
+ const expr = argsPart.slice(start, i).trim();
134
+ if (expr)
135
+ args.push(expr);
136
+ if (argsPart[i] === ",")
137
+ i++;
138
+ }
139
+ return args;
140
+ }
141
+ function parsePrintFmtLine(line) {
142
+ const prefix = line.match(/^print\s+fmt\s*:\s*/i);
143
+ if (!prefix)
144
+ return undefined;
145
+ const lit = parsePrintFmtStringLiteral(line, prefix[0].length);
146
+ if (!lit)
147
+ return undefined;
148
+ const rest = line.slice(lit.end).trim();
149
+ if (!rest)
150
+ return { printFmt: lit.value, printArgs: [] };
151
+ const argsMatch = rest.match(/^,\s*(.*)$/);
152
+ if (!argsMatch)
153
+ return undefined;
154
+ return { printFmt: lit.value, printArgs: scanBalancedPrintArgs(argsMatch[1]) };
155
+ }
23
156
  function splitTypeAndName(rest) {
24
157
  const trimmed = rest.trim();
25
158
  const m = trimmed.match(/^(.+?)\s+(\w+(?:\[[^\]]*])?)$/);
@@ -61,17 +194,10 @@ export function parseTraceFormat(text) {
61
194
  continue;
62
195
  }
63
196
  if (/^print\s+fmt\s*:/i.test(t)) {
64
- const pf = t.match(/^print\s+fmt\s*:\s*"([^"]*)"\s*(?:,\s*(.*))?$/i);
197
+ const pf = parsePrintFmtLine(t);
65
198
  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
- }
199
+ printFmt = pf.printFmt;
200
+ printArgs = pf.printArgs;
75
201
  }
76
202
  break;
77
203
  }
@@ -92,6 +218,10 @@ export function parseTraceFormat(text) {
92
218
  function normalizeType(t) {
93
219
  return t.replace(/\s+/g, " ").trim().toLowerCase();
94
220
  }
221
+ function isCharType(typeName) {
222
+ const t = normalizeType(typeName);
223
+ return t === "char" || t === "signed char" || t === "unsigned char";
224
+ }
95
225
  /**
96
226
  * 从 little-endian 原始缓冲区按字段描述读一个标量
97
227
  */
@@ -202,6 +332,9 @@ export function parseAllFieldsFromRaw(raw, format) {
202
332
  out[locKey] = loc;
203
333
  const offset = loc & 0xffff;
204
334
  const len = (loc >>> 16) & 0xffff;
335
+ if (len > 0 && (offset < 0 || offset + len > raw.length)) {
336
+ throw new TraceFieldDecodeError(`Field "${field.name}" __data_loc out of bounds: offset=${offset}, len=${len}, raw.length=${raw.length}`, field.name, offset, len, raw.length);
337
+ }
205
338
  if (offset >= 0 && len > 0 && offset + len <= raw.length) {
206
339
  const bytes = raw.subarray(offset, offset + len);
207
340
  // 去掉末尾 \0
@@ -215,6 +348,10 @@ export function parseAllFieldsFromRaw(raw, format) {
215
348
  }
216
349
  continue;
217
350
  }
351
+ if (arr && isCharType(field.typeName)) {
352
+ out[key] = readCharArrayAsString(raw, field.offset, field.size, field.name);
353
+ continue;
354
+ }
218
355
  const v = readFieldValue(view, field);
219
356
  if (v !== undefined) {
220
357
  out[key] = v;
@@ -233,8 +370,9 @@ export function rawHexLinesToBuffer(lines, endian = "le") {
233
370
  let totalBytes = 0;
234
371
  for (const { hex } of lines) {
235
372
  let s = hex.replace(/^0x/i, "").trim();
236
- if (!s)
237
- continue;
373
+ if (!s) {
374
+ throw new Error(`Invalid hex token: "${hex}"`);
375
+ }
238
376
  if (s.length % 2 === 1)
239
377
  s = "0" + s;
240
378
  normalizedTokens.push(s);
@@ -261,8 +399,9 @@ export function rawHexLinesToBuffer(lines, endian = "le") {
261
399
  * 默认每行 4 字节(即 8 hex digits)。
262
400
  */
263
401
  export function bufferToRawHexLines(raw, bytesPerLine = 4, endian = "le") {
264
- if (bytesPerLine <= 0)
265
- return [];
402
+ if (!Number.isSafeInteger(bytesPerLine) || bytesPerLine <= 0) {
403
+ throw new RangeError(`bytesPerLine must be a positive safe integer, got ${bytesPerLine}`);
404
+ }
266
405
  const out = [];
267
406
  for (let i = 0; i < raw.length; i += bytesPerLine) {
268
407
  const end = Math.min(i + bytesPerLine, raw.length);
@@ -292,14 +431,18 @@ export function bufferToRawHexLines(raw, bytesPerLine = 4, endian = "le") {
292
431
  *
293
432
  * 若用户未配置 transformFieldDict,或未使用 fieldDict 模式,则不会做任何额外处理。
294
433
  */
295
- export function buildTraceParserRegistry(formatTexts) {
434
+ export function buildTraceParserRegistry(formatTexts, options = {}) {
296
435
  const byEventId = new Map();
297
436
  let commonFormat;
298
437
  const transformFieldDictByEventId = new Map();
299
- for (const src of formatTexts) {
438
+ formatTexts.forEach((src, sourceIndex) => {
300
439
  const text = typeof src === "string" ? src : src.text;
301
440
  const fmt = parseTraceFormat(text);
302
441
  if (fmt.eventId !== undefined) {
442
+ const existing = byEventId.get(fmt.eventId);
443
+ if (existing && !options.allowDuplicateEventIds) {
444
+ throw new Error(`Duplicate event ID ${fmt.eventId}: "${existing.eventName ?? "unknown"}" conflicts with "${fmt.eventName ?? "unknown"}" at format index ${sourceIndex}`);
445
+ }
303
446
  byEventId.set(fmt.eventId, fmt);
304
447
  if (!commonFormat && fmt.fields.some((f) => f.name === "common_type")) {
305
448
  commonFormat = fmt;
@@ -308,7 +451,7 @@ export function buildTraceParserRegistry(formatTexts) {
308
451
  transformFieldDictByEventId.set(fmt.eventId, src.transformFieldDict);
309
452
  }
310
453
  }
311
- }
454
+ });
312
455
  return {
313
456
  byEventId,
314
457
  commonFormat,
@@ -3,6 +3,8 @@
3
3
  * @module traceFormat
4
4
  */
5
5
  type PrintFieldMap = Record<string, number | bigint | string | Array<number | bigint>>;
6
+ /** printf 格式串解析缓存上限(LRU 淘汰) */
7
+ export declare const PRINTF_CACHE_MAX_SIZE = 256;
6
8
  /** 与 C printf 接近的转换说明符(不含 * 动态宽度等) */
7
9
  export interface PrintfSpec {
8
10
  flags: string;
@@ -27,6 +29,13 @@ export declare function tokenizePrintfFormat(printFmt: string): Array<{
27
29
  kind: "spec";
28
30
  spec: PrintfSpec;
29
31
  }>;
32
+ /** 测试专用:重置 printf 解析缓存 */
33
+ export declare function resetPrintfCachesForTest(): void;
34
+ /** 测试专用:读取 printf 解析缓存条目数 */
35
+ export declare function getPrintfCacheSizesForTest(): {
36
+ tokens: number;
37
+ specs: number;
38
+ };
30
39
  export declare function renderPrintFmtAsFieldDict(printFmt: string, printArgs: string[] | undefined, fieldMap: PrintFieldMap): Record<string, string>;
31
40
  export declare function renderPrintFmt(printFmt: string, printArgs: string[] | undefined, fieldMap: PrintFieldMap): string;
32
41
  export {};
@@ -2,8 +2,42 @@
2
2
  * printf / fieldDict 格式化与 print fmt 渲染。
3
3
  * @module traceFormat
4
4
  */
5
- const PRINT_FMT_TOKEN_CACHE = new Map();
6
- const PRINT_FMT_SPEC_CACHE = new Map();
5
+ /** printf 格式串解析缓存上限(LRU 淘汰) */
6
+ export const PRINTF_CACHE_MAX_SIZE = 256;
7
+ class LruCache {
8
+ maxSize;
9
+ map = new Map();
10
+ constructor(maxSize) {
11
+ this.maxSize = maxSize;
12
+ }
13
+ get(key) {
14
+ const value = this.map.get(key);
15
+ if (value === undefined)
16
+ return undefined;
17
+ this.map.delete(key);
18
+ this.map.set(key, value);
19
+ return value;
20
+ }
21
+ set(key, value) {
22
+ if (this.map.has(key))
23
+ this.map.delete(key);
24
+ this.map.set(key, value);
25
+ while (this.map.size > this.maxSize) {
26
+ const oldest = this.map.keys().next().value;
27
+ if (oldest === undefined)
28
+ break;
29
+ this.map.delete(oldest);
30
+ }
31
+ }
32
+ get size() {
33
+ return this.map.size;
34
+ }
35
+ clear() {
36
+ this.map.clear();
37
+ }
38
+ }
39
+ const PRINT_FMT_TOKEN_CACHE = new LruCache(PRINTF_CACHE_MAX_SIZE);
40
+ const PRINT_FMT_SPEC_CACHE = new LruCache(PRINTF_CACHE_MAX_SIZE);
7
41
  const NORMALIZED_PRINT_ARGS_CACHE = new WeakMap();
8
42
  const EMPTY_PRINT_ARGS = [];
9
43
  const PRINT_FLAG_CHARS = new Set(["-", "+", "#", "0", " "]);
@@ -24,6 +58,47 @@ function toBigIntForPrintf(value) {
24
58
  function asUInt64(n) {
25
59
  return n & ((1n << 64n) - 1n);
26
60
  }
61
+ /** 按长度修饰符截断无符号值(C printf 语义) */
62
+ function maskUnsignedByLength(n, length) {
63
+ switch (length) {
64
+ case "hh":
65
+ return n & 0xffn;
66
+ case "h":
67
+ return n & 0xffffn;
68
+ case "l":
69
+ case "ll":
70
+ case "j":
71
+ case "z":
72
+ case "t":
73
+ case "L":
74
+ return asUInt64(n);
75
+ default:
76
+ return n & 0xffffffffn;
77
+ }
78
+ }
79
+ function signExtend(value, bits) {
80
+ const mask = (1n << BigInt(bits)) - 1n;
81
+ const v = value & mask;
82
+ const signBit = 1n << BigInt(bits - 1);
83
+ return v >= signBit ? v - (1n << BigInt(bits)) : v;
84
+ }
85
+ /** 按长度修饰符截断并符号扩展有符号值(C printf 语义) */
86
+ function maskSignedByLength(n, length) {
87
+ switch (length) {
88
+ case "hh":
89
+ return signExtend(n, 8);
90
+ case "h":
91
+ return signExtend(n, 16);
92
+ case "l":
93
+ case "ll":
94
+ case "j":
95
+ case "z":
96
+ case "t":
97
+ return signExtend(asUInt64(n), 64);
98
+ default:
99
+ return signExtend(n & 0xffffffffn, 32);
100
+ }
101
+ }
27
102
  function applyWidth(s, width, leftAlign, zeroPad) {
28
103
  if (width === undefined || s.length >= width)
29
104
  return s;
@@ -37,9 +112,15 @@ function applyWidth(s, width, leftAlign, zeroPad) {
37
112
  function formatPrintfD(n, spec) {
38
113
  const neg = n < 0n;
39
114
  const abs = neg ? -n : n;
40
- let digits = abs.toString(10);
41
115
  const prec = spec.precision !== undefined ? spec.precision : 1;
42
- digits = digits.padStart(Math.max(prec, digits.length), "0");
116
+ let digits;
117
+ if (prec === 0 && abs === 0n) {
118
+ digits = "";
119
+ }
120
+ else {
121
+ digits = abs.toString(10);
122
+ digits = digits.padStart(Math.max(prec, digits.length), "0");
123
+ }
43
124
  let sign = "";
44
125
  if (neg)
45
126
  sign = "-";
@@ -61,24 +142,42 @@ function formatPrintfD(n, spec) {
61
142
  return " ".repeat(padLen) + body;
62
143
  }
63
144
  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);
145
+ let prefix = "";
69
146
  const precDefault = 1;
70
147
  const prec = spec.precision !== undefined ? spec.precision : precDefault;
71
- body = body.padStart(Math.max(prec, body.length), "0");
72
- if (hasPrintfFlag(spec.flags, "#")) {
148
+ let body;
149
+ if (prec === 0 && unsigned === 0n) {
150
+ body = "";
151
+ }
152
+ else {
153
+ body =
154
+ radix === 16
155
+ ? upper
156
+ ? unsigned.toString(16).toUpperCase()
157
+ : unsigned.toString(16)
158
+ : unsigned.toString(radix);
159
+ body = body.padStart(Math.max(prec, body.length), "0");
160
+ }
161
+ if (hasPrintfFlag(spec.flags, "#") && unsigned !== 0n) {
73
162
  if (radix === 16)
74
- body = (upper ? "0X" : "0x") + body;
163
+ prefix = upper ? "0X" : "0x";
75
164
  else if (radix === 8 && body !== "0" && !body.startsWith("0"))
76
- body = "0" + body;
165
+ prefix = "0";
77
166
  }
78
167
  const zeroPad = hasPrintfFlag(spec.flags, "0") &&
79
168
  !hasPrintfFlag(spec.flags, "-") &&
80
169
  spec.precision === undefined;
81
- return applyWidth(body, spec.width, hasPrintfFlag(spec.flags, "-"), zeroPad);
170
+ const leftAlign = hasPrintfFlag(spec.flags, "-");
171
+ if (zeroPad && prefix) {
172
+ const width = spec.width;
173
+ if (width !== undefined) {
174
+ const bodyWidth = Math.max(width - prefix.length, body.length);
175
+ body = body.padStart(bodyWidth, "0");
176
+ }
177
+ return prefix + body;
178
+ }
179
+ const combined = prefix + body;
180
+ return applyWidth(combined, spec.width, leftAlign, zeroPad);
82
181
  }
83
182
  /**
84
183
  * 按 C printf 子集格式化:标志 -+ #0 空格,宽度,精度,长度 hl ll hh,转换 d i u x X o s p。
@@ -106,14 +205,20 @@ export function formatPrintfValue(value, spec) {
106
205
  return applyWidth(body, spec.width, hasPrintfFlag(spec.flags, "-"), zeroPad);
107
206
  }
108
207
  if (cl === "d" || cl === "i") {
109
- return formatPrintfD(n, spec);
208
+ return formatPrintfD(maskSignedByLength(n, spec.length), spec);
110
209
  }
111
210
  if (cl === "u" || cl === "x" || cl === "X" || cl === "o") {
112
- const u = asUInt64(n);
211
+ const u = maskUnsignedByLength(n, spec.length);
113
212
  if (cl === "u") {
114
- let body = u.toString(10);
115
213
  const prec = spec.precision !== undefined ? spec.precision : 1;
116
- body = body.padStart(Math.max(prec, body.length), "0");
214
+ let body;
215
+ if (prec === 0 && u === 0n) {
216
+ body = "";
217
+ }
218
+ else {
219
+ body = u.toString(10);
220
+ body = body.padStart(Math.max(prec, body.length), "0");
221
+ }
117
222
  const zeroPad = hasPrintfFlag(spec.flags, "0") &&
118
223
  !hasPrintfFlag(spec.flags, "-") &&
119
224
  spec.precision === undefined;
@@ -246,12 +351,34 @@ function getCachedPrintfTokens(printFmt) {
246
351
  PRINT_FMT_TOKEN_CACHE.set(printFmt, tokens);
247
352
  return tokens;
248
353
  }
354
+ /** 测试专用:重置 printf 解析缓存 */
355
+ export function resetPrintfCachesForTest() {
356
+ PRINT_FMT_TOKEN_CACHE.clear();
357
+ PRINT_FMT_SPEC_CACHE.clear();
358
+ }
359
+ /** 测试专用:读取 printf 解析缓存条目数 */
360
+ export function getPrintfCacheSizesForTest() {
361
+ return { tokens: PRINT_FMT_TOKEN_CACHE.size, specs: PRINT_FMT_SPEC_CACHE.size };
362
+ }
249
363
  function normalizePrintExpr(exprRaw) {
250
364
  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();
365
+ if (expr.startsWith("(") && expr.endsWith(")")) {
366
+ let depth = 0;
367
+ let fullyWrapped = true;
368
+ for (let i = 0; i < expr.length; i++) {
369
+ if (expr[i] === "(")
370
+ depth++;
371
+ else if (expr[i] === ")")
372
+ depth--;
373
+ if (depth === 0 && i < expr.length - 1) {
374
+ fullyWrapped = false;
375
+ break;
376
+ }
377
+ }
378
+ if (fullyWrapped && depth === 0) {
379
+ expr = expr.slice(1, -1).trim();
380
+ }
381
+ }
255
382
  expr = expr.replace(/\s+/g, "");
256
383
  return expr;
257
384
  }
@@ -267,7 +394,21 @@ function getNormalizedPrintArgs(printArgs) {
267
394
  }
268
395
  function evalPrintArgExpr(exprRaw, fieldMap) {
269
396
  const expr = normalizePrintExpr(exprRaw);
270
- let m = expr.match(/^REC->(\w+)&0xffff$/);
397
+ let m = expr.match(/^__get_str\((\w+)\)$/);
398
+ if (m) {
399
+ const v = fieldMap[m[1]];
400
+ if (typeof v === "string")
401
+ return v;
402
+ throw new Error(`__get_str(${m[1]}) requires a string field, got ${typeof v}`);
403
+ }
404
+ m = expr.match(/^__get_dynamic_array\((\w+)\)$/);
405
+ if (m) {
406
+ const v = fieldMap[m[1]];
407
+ if (Array.isArray(v))
408
+ return v;
409
+ throw new Error(`__get_dynamic_array(${m[1]}) requires an array field, got ${typeof v}`);
410
+ }
411
+ m = expr.match(/^REC->(\w+)&0xffff$/);
271
412
  if (m) {
272
413
  const v = fieldMap[m[1]];
273
414
  const n = typeof v === "number" ? v : 0;
@@ -280,32 +421,49 @@ function evalPrintArgExpr(exprRaw, fieldMap) {
280
421
  return (n >>> 16) & 0xffff;
281
422
  }
282
423
  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];
424
+ if (m) {
425
+ const name = m[1];
426
+ const idxRaw = m[2];
427
+ const v = fieldMap[name];
428
+ if (idxRaw !== undefined) {
429
+ const idx = parseInt(idxRaw, 10);
430
+ if (Array.isArray(v) && idx >= 0 && idx < v.length) {
431
+ return v[idx];
432
+ }
433
+ throw new Error(`Invalid print arg index for REC->${name}[${idxRaw}]`);
292
434
  }
293
- return 0;
435
+ if (Array.isArray(v))
436
+ return v[0] ?? 0;
437
+ if (v !== undefined)
438
+ return v;
439
+ throw new Error(`Missing field for print arg REC->${name}`);
294
440
  }
295
- if (Array.isArray(v))
296
- return v[0] ?? 0;
297
- return v ?? 0;
441
+ throw new Error(`Unknown print arg expression: ${exprRaw.trim()}`);
298
442
  }
299
443
  function buildPrintArgValues(printArgs, fieldMap) {
300
444
  const normalizedArgs = getNormalizedPrintArgs(printArgs);
301
445
  const values = (printArgs ?? []).map((e) => evalPrintArgExpr(e, fieldMap));
302
446
  return { normalizedArgs, values };
303
447
  }
448
+ function resolvePrintArgValue(value) {
449
+ if (value === undefined)
450
+ return 0;
451
+ if (Array.isArray(value))
452
+ return value[0] ?? 0;
453
+ return value;
454
+ }
304
455
  function formatOnePrintArg(idx, spec, fieldMap, normalizedArgs, values) {
305
- const v = values[idx] ?? 0;
456
+ const v = resolvePrintArgValue(values[idx]);
306
457
  const argExpr = normalizedArgs[idx] ?? "";
307
458
  if (spec.conv === "s" || spec.conv === "S") {
308
- const m = argExpr.match(/^REC->__data_loc_(\w+)(?:&0xffff)?(?:>>16)?$/);
459
+ let m = argExpr.match(/^__get_str\((\w+)\)$/);
460
+ if (m) {
461
+ const s = fieldMap[m[1]];
462
+ if (typeof s === "string") {
463
+ return formatPrintfValue(s, spec);
464
+ }
465
+ }
466
+ m = argExpr.match(/^REC->__data_loc_(\w+)(?:&0xffff)?(?:>>16)?$/);
309
467
  if (m) {
310
468
  const s = fieldMap[m[1]];
311
469
  if (typeof s === "string") {
@@ -319,6 +477,14 @@ function fieldKeyFromNormalizedArg(normalizedArg, index) {
319
477
  let k = normalizedArg;
320
478
  if (k.startsWith("REC->"))
321
479
  k = k.slice(5);
480
+ const mGetStr = k.match(/^__get_str\((\w+)\)$/);
481
+ if (mGetStr) {
482
+ return mGetStr[1];
483
+ }
484
+ const mGetArr = k.match(/^__get_dynamic_array\((\w+)\)$/);
485
+ if (mGetArr) {
486
+ return mGetArr[1];
487
+ }
322
488
  // REC->__data_loc_path&0xffff 在 fieldDict 中收敛为 path 作为键名
323
489
  const mDataLocOff = k.match(/^__data_loc_(\w+)&0xffff$/);
324
490
  if (mDataLocOff) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hm-pt-core",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "DFX perf trace shared types and trace format decode primitives",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",