hm-pt-core 1.0.0 → 1.0.2

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";
@@ -21,9 +21,10 @@ export function decodeRawByRegistry(raw, registry, options = {}) {
21
21
  };
22
22
  }
23
23
  const allFields = parseAllFieldsFromRaw(raw, fmt);
24
+ const renderContext = { eventName: fmt.eventName, eventId };
24
25
  const mode = options.tracePrintMode ?? "printf";
25
26
  if (fmt.printFmt && mode === "fieldDict") {
26
- let renderedFieldDict = renderPrintFmtAsFieldDict(fmt.printFmt, fmt.printArgs, allFields);
27
+ let renderedFieldDict = renderPrintFmtAsFieldDict(fmt.printFmt, fmt.printArgs, allFields, renderContext);
27
28
  const transform = registry.transformFieldDictByEventId?.get(eventId);
28
29
  if (transform) {
29
30
  renderedFieldDict = transform({
@@ -42,7 +43,9 @@ export function decodeRawByRegistry(raw, registry, options = {}) {
42
43
  skipped: false,
43
44
  };
44
45
  }
45
- const renderedText = fmt.printFmt ? renderPrintFmt(fmt.printFmt, fmt.printArgs, allFields) : undefined;
46
+ const renderedText = fmt.printFmt
47
+ ? renderPrintFmt(fmt.printFmt, fmt.printArgs, allFields, renderContext)
48
+ : undefined;
46
49
  return {
47
50
  commonType: eventId,
48
51
  commonFields,
@@ -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,13 +218,93 @@ 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
+ }
225
+ function isDataLocType(typeName) {
226
+ return normalizeType(typeName).includes("__data_loc");
227
+ }
228
+ function parseDataLocElementType(typeName, signed) {
229
+ const t = normalizeType(typeName);
230
+ const m = t.match(/^__data_loc\s+(.+)\[\]$/);
231
+ if (!m)
232
+ return undefined;
233
+ const elemTypeName = m[1].trim();
234
+ const elemSize = inferScalarSize(elemTypeName, signed);
235
+ if (elemSize === undefined)
236
+ return undefined;
237
+ return { elemTypeName, elemSize, signed: isSignedElementType(elemTypeName, signed) };
238
+ }
239
+ function isSignedElementType(elemTypeName, fieldSigned) {
240
+ const t = normalizeType(elemTypeName);
241
+ if (t.includes("unsigned"))
242
+ return false;
243
+ if (t === "char" && !fieldSigned)
244
+ return false;
245
+ return fieldSigned || t === "signed char" || t === "short" || t === "int" || t === "long" || t === "long long" || t === "__s64";
246
+ }
247
+ function inferScalarSize(typeName, signed) {
248
+ const t = normalizeType(typeName);
249
+ if (t.includes("double"))
250
+ return 8;
251
+ if (t.includes("long long") || t === "__s64")
252
+ return 8;
253
+ if (t.includes("long"))
254
+ return 8;
255
+ if (t.includes("short"))
256
+ return 2;
257
+ if (t.includes("char") || t.includes("int") || t.includes("float"))
258
+ return 4;
259
+ if (t.includes("unsigned") || signed)
260
+ return 4;
261
+ return undefined;
262
+ }
263
+ function assertFieldInBounds(fieldName, offset, size, rawLength) {
264
+ if (offset < 0 || offset + size > rawLength) {
265
+ throw new TraceFieldDecodeError(`Field "${fieldName}" out of bounds: offset=${offset}, size=${size}, raw.length=${rawLength}`, fieldName, offset, size, rawLength);
266
+ }
267
+ }
268
+ function readLocatorField(view, field) {
269
+ assertFieldInBounds(field.name, field.offset, field.size, view.byteLength);
270
+ const locField = {
271
+ ...field,
272
+ typeName: "unsigned int",
273
+ signed: false,
274
+ size: 4,
275
+ };
276
+ const loc = readFieldScalarUnchecked(view, locField);
277
+ if (typeof loc !== "number") {
278
+ throw new TraceFieldDecodeError(`Field "${field.name}" __data_loc locator is invalid`, field.name, field.offset, field.size, view.byteLength);
279
+ }
280
+ return loc;
281
+ }
282
+ function readDynamicRegionAsArray(raw, view, field, dataOffset, byteLen, elemTypeName, elemSize, elemSigned) {
283
+ if (byteLen === 0)
284
+ return [];
285
+ if (byteLen % elemSize !== 0) {
286
+ throw new TraceFieldDecodeError(`Field "${field.name}" __data_loc length ${byteLen} is not divisible by element size ${elemSize}`, field.name, dataOffset, byteLen, raw.length);
287
+ }
288
+ assertFieldInBounds(field.name, dataOffset, byteLen, raw.length);
289
+ const count = byteLen / elemSize;
290
+ const values = [];
291
+ for (let i = 0; i < count; i++) {
292
+ const elemField = {
293
+ typeName: elemTypeName,
294
+ name: field.name,
295
+ offset: dataOffset + i * elemSize,
296
+ size: elemSize,
297
+ signed: elemSigned,
298
+ };
299
+ values.push(readFieldScalarUnchecked(view, elemField));
300
+ }
301
+ return values;
302
+ }
95
303
  /**
96
304
  * 从 little-endian 原始缓冲区按字段描述读一个标量
97
305
  */
98
- function readFieldScalar(view, field) {
306
+ function readFieldScalarUnchecked(view, field) {
99
307
  const { offset, size, signed, typeName } = field;
100
- if (offset + size > view.byteLength)
101
- return undefined;
102
308
  const t = normalizeType(typeName);
103
309
  if (size === 1) {
104
310
  if (signed || t === "signed char") {
@@ -130,7 +336,14 @@ function readFieldScalar(view, field) {
130
336
  }
131
337
  return view.getBigUint64(offset, true);
132
338
  }
133
- return undefined;
339
+ throw new TraceFieldDecodeError(`Field "${field.name}" has unsupported scalar size ${size}`, field.name, offset, size, view.byteLength);
340
+ }
341
+ /**
342
+ * 从 little-endian 原始缓冲区按字段描述读一个标量;越界时抛出 TraceFieldDecodeError。
343
+ */
344
+ function readFieldScalar(view, field) {
345
+ assertFieldInBounds(field.name, field.offset, field.size, view.byteLength);
346
+ return readFieldScalarUnchecked(view, field);
134
347
  }
135
348
  function parseArrayName(name) {
136
349
  const m = name.match(/^(\w+)\[(\d+)\]$/);
@@ -144,9 +357,10 @@ function readFieldValue(view, field) {
144
357
  return readFieldScalar(view, field);
145
358
  if (arr.len <= 0)
146
359
  return [];
147
- const elemSize = Math.floor(field.size / arr.len);
148
- if (elemSize <= 0)
149
- return undefined;
360
+ if (field.size % arr.len !== 0) {
361
+ throw new TraceFieldDecodeError(`Field "${field.name}" size ${field.size} is not divisible by array length ${arr.len}`, field.name, field.offset, field.size, view.byteLength);
362
+ }
363
+ const elemSize = field.size / arr.len;
150
364
  const values = [];
151
365
  for (let i = 0; i < arr.len; i++) {
152
366
  const elemField = {
@@ -155,10 +369,7 @@ function readFieldValue(view, field) {
155
369
  offset: field.offset + i * elemSize,
156
370
  size: elemSize,
157
371
  };
158
- const v = readFieldScalar(view, elemField);
159
- if (v === undefined)
160
- return undefined;
161
- values.push(v);
372
+ values.push(readFieldScalar(view, elemField));
162
373
  }
163
374
  return values;
164
375
  }
@@ -171,10 +382,9 @@ export function parseCommonFieldsFromRaw(raw, format) {
171
382
  for (const field of format.fields) {
172
383
  if (!field.name.startsWith("common_"))
173
384
  continue;
174
- const v = readFieldScalar(view, field);
175
- if (v !== undefined) {
176
- out[field.name] = v;
177
- }
385
+ if (field.offset + field.size > view.byteLength)
386
+ continue;
387
+ out[field.name] = readFieldScalar(view, field);
178
388
  }
179
389
  return out;
180
390
  }
@@ -188,23 +398,18 @@ export function parseAllFieldsFromRaw(raw, format) {
188
398
  const arr = parseArrayName(field.name);
189
399
  const key = arr ? arr.baseName : field.name;
190
400
  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
401
+ if (isDataLocType(field.typeName)) {
402
+ const loc = readLocatorField(view, field);
403
+ const locKey = `__data_loc_${field.name}`;
404
+ out[locKey] = loc;
405
+ const dataOffset = loc & 0xffff;
406
+ const byteLen = (loc >>> 16) & 0xffff;
407
+ if (byteLen > 0 && (dataOffset < 0 || dataOffset + byteLen > raw.length)) {
408
+ throw new TraceFieldDecodeError(`Field "${field.name}" __data_loc out of bounds: offset=${dataOffset}, len=${byteLen}, raw.length=${raw.length}`, field.name, dataOffset, byteLen, raw.length);
409
+ }
410
+ if (t.includes("char")) {
411
+ if (byteLen > 0) {
412
+ const bytes = raw.subarray(dataOffset, dataOffset + byteLen);
208
413
  const nul = bytes.indexOf(0);
209
414
  const slice = nul >= 0 ? bytes.subarray(0, nul) : bytes;
210
415
  out[field.name] = UTF8_DECODER.decode(slice);
@@ -212,13 +417,20 @@ export function parseAllFieldsFromRaw(raw, format) {
212
417
  else {
213
418
  out[field.name] = "";
214
419
  }
420
+ continue;
421
+ }
422
+ const elemInfo = parseDataLocElementType(field.typeName, field.signed);
423
+ if (!elemInfo) {
424
+ throw new TraceFieldDecodeError(`Field "${field.name}" uses unsupported __data_loc element type "${field.typeName}"`, field.name, field.offset, field.size, raw.length);
215
425
  }
426
+ out[key] = readDynamicRegionAsArray(raw, view, field, dataOffset, byteLen, elemInfo.elemTypeName, elemInfo.elemSize, elemInfo.signed);
216
427
  continue;
217
428
  }
218
- const v = readFieldValue(view, field);
219
- if (v !== undefined) {
220
- out[key] = v;
429
+ if (arr && isCharType(field.typeName)) {
430
+ out[key] = readCharArrayAsString(raw, field.offset, field.size, field.name);
431
+ continue;
221
432
  }
433
+ out[key] = readFieldValue(view, field);
222
434
  }
223
435
  return out;
224
436
  }
@@ -233,8 +445,9 @@ export function rawHexLinesToBuffer(lines, endian = "le") {
233
445
  let totalBytes = 0;
234
446
  for (const { hex } of lines) {
235
447
  let s = hex.replace(/^0x/i, "").trim();
236
- if (!s)
237
- continue;
448
+ if (!s) {
449
+ throw new Error(`Invalid hex token: "${hex}"`);
450
+ }
238
451
  if (s.length % 2 === 1)
239
452
  s = "0" + s;
240
453
  normalizedTokens.push(s);
@@ -261,8 +474,9 @@ export function rawHexLinesToBuffer(lines, endian = "le") {
261
474
  * 默认每行 4 字节(即 8 hex digits)。
262
475
  */
263
476
  export function bufferToRawHexLines(raw, bytesPerLine = 4, endian = "le") {
264
- if (bytesPerLine <= 0)
265
- return [];
477
+ if (!Number.isSafeInteger(bytesPerLine) || bytesPerLine <= 0) {
478
+ throw new RangeError(`bytesPerLine must be a positive safe integer, got ${bytesPerLine}`);
479
+ }
266
480
  const out = [];
267
481
  for (let i = 0; i < raw.length; i += bytesPerLine) {
268
482
  const end = Math.min(i + bytesPerLine, raw.length);
@@ -292,14 +506,18 @@ export function bufferToRawHexLines(raw, bytesPerLine = 4, endian = "le") {
292
506
  *
293
507
  * 若用户未配置 transformFieldDict,或未使用 fieldDict 模式,则不会做任何额外处理。
294
508
  */
295
- export function buildTraceParserRegistry(formatTexts) {
509
+ export function buildTraceParserRegistry(formatTexts, options = {}) {
296
510
  const byEventId = new Map();
297
511
  let commonFormat;
298
512
  const transformFieldDictByEventId = new Map();
299
- for (const src of formatTexts) {
513
+ formatTexts.forEach((src, sourceIndex) => {
300
514
  const text = typeof src === "string" ? src : src.text;
301
515
  const fmt = parseTraceFormat(text);
302
516
  if (fmt.eventId !== undefined) {
517
+ const existing = byEventId.get(fmt.eventId);
518
+ if (existing && !options.allowDuplicateEventIds) {
519
+ throw new Error(`Duplicate event ID ${fmt.eventId}: "${existing.eventName ?? "unknown"}" conflicts with "${fmt.eventName ?? "unknown"}" at format index ${sourceIndex}`);
520
+ }
303
521
  byEventId.set(fmt.eventId, fmt);
304
522
  if (!commonFormat && fmt.fields.some((f) => f.name === "common_type")) {
305
523
  commonFormat = fmt;
@@ -307,8 +525,11 @@ export function buildTraceParserRegistry(formatTexts) {
307
525
  if (typeof src !== "string" && src.transformFieldDict) {
308
526
  transformFieldDictByEventId.set(fmt.eventId, src.transformFieldDict);
309
527
  }
528
+ else {
529
+ transformFieldDictByEventId.delete(fmt.eventId);
530
+ }
310
531
  }
311
- }
532
+ });
312
533
  return {
313
534
  byEventId,
314
535
  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,17 @@ export declare function tokenizePrintfFormat(printFmt: string): Array<{
27
29
  kind: "spec";
28
30
  spec: PrintfSpec;
29
31
  }>;
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 interface PrintFmtRenderContext {
33
+ eventName?: string;
34
+ eventId?: number;
35
+ }
36
+ /** 测试专用:重置 printf 解析缓存 */
37
+ export declare function resetPrintfCachesForTest(): void;
38
+ /** 测试专用:读取 printf 解析缓存条目数 */
39
+ export declare function getPrintfCacheSizesForTest(): {
40
+ tokens: number;
41
+ specs: number;
42
+ };
43
+ export declare function renderPrintFmtAsFieldDict(printFmt: string, printArgs: string[] | undefined, fieldMap: PrintFieldMap, context?: PrintFmtRenderContext): Record<string, string>;
44
+ export declare function renderPrintFmt(printFmt: string, printArgs: string[] | undefined, fieldMap: PrintFieldMap, context?: PrintFmtRenderContext): string;
32
45
  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,52 @@ 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");
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
+ }
72
161
  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;
162
+ if (radix === 16 && unsigned !== 0n) {
163
+ prefix = upper ? "0X" : "0x";
164
+ }
165
+ else if (radix === 8) {
166
+ if (unsigned === 0n && (prec === 0 || body === "")) {
167
+ body = "0";
168
+ }
169
+ else if (body === "") {
170
+ body = "0";
171
+ }
172
+ else if (!body.startsWith("0")) {
173
+ body = "0" + body;
174
+ }
175
+ }
77
176
  }
78
177
  const zeroPad = hasPrintfFlag(spec.flags, "0") &&
79
178
  !hasPrintfFlag(spec.flags, "-") &&
80
179
  spec.precision === undefined;
81
- return applyWidth(body, spec.width, hasPrintfFlag(spec.flags, "-"), zeroPad);
180
+ const leftAlign = hasPrintfFlag(spec.flags, "-");
181
+ if (zeroPad && prefix) {
182
+ const width = spec.width;
183
+ if (width !== undefined) {
184
+ const bodyWidth = Math.max(width - prefix.length, body.length);
185
+ body = body.padStart(bodyWidth, "0");
186
+ }
187
+ return prefix + body;
188
+ }
189
+ const combined = prefix + body;
190
+ return applyWidth(combined, spec.width, leftAlign, zeroPad);
82
191
  }
83
192
  /**
84
193
  * 按 C printf 子集格式化:标志 -+ #0 空格,宽度,精度,长度 hl ll hh,转换 d i u x X o s p。
@@ -106,14 +215,20 @@ export function formatPrintfValue(value, spec) {
106
215
  return applyWidth(body, spec.width, hasPrintfFlag(spec.flags, "-"), zeroPad);
107
216
  }
108
217
  if (cl === "d" || cl === "i") {
109
- return formatPrintfD(n, spec);
218
+ return formatPrintfD(maskSignedByLength(n, spec.length), spec);
110
219
  }
111
220
  if (cl === "u" || cl === "x" || cl === "X" || cl === "o") {
112
- const u = asUInt64(n);
221
+ const u = maskUnsignedByLength(n, spec.length);
113
222
  if (cl === "u") {
114
- let body = u.toString(10);
115
223
  const prec = spec.precision !== undefined ? spec.precision : 1;
116
- body = body.padStart(Math.max(prec, body.length), "0");
224
+ let body;
225
+ if (prec === 0 && u === 0n) {
226
+ body = "";
227
+ }
228
+ else {
229
+ body = u.toString(10);
230
+ body = body.padStart(Math.max(prec, body.length), "0");
231
+ }
117
232
  const zeroPad = hasPrintfFlag(spec.flags, "0") &&
118
233
  !hasPrintfFlag(spec.flags, "-") &&
119
234
  spec.precision === undefined;
@@ -228,6 +343,16 @@ export function tokenizePrintfFormat(printFmt) {
228
343
  flushText();
229
344
  return out;
230
345
  }
346
+ function assertPrintArgCount(printFmt, printArgs, context) {
347
+ const specCount = extractPrintfSpecs(printFmt).length;
348
+ const argCount = printArgs?.length ?? 0;
349
+ if (argCount < specCount) {
350
+ const eventLabel = context?.eventName !== undefined
351
+ ? `"${context.eventName}"${context.eventId !== undefined ? ` (ID ${context.eventId})` : ""}`
352
+ : "unknown event";
353
+ throw new Error(`Print fmt for event ${eventLabel} requires ${specCount} args but got ${argCount} at spec index ${argCount}: "${printFmt}"`);
354
+ }
355
+ }
231
356
  function extractPrintfSpecs(printFmt) {
232
357
  const cached = PRINT_FMT_SPEC_CACHE.get(printFmt);
233
358
  if (cached)
@@ -246,12 +371,34 @@ function getCachedPrintfTokens(printFmt) {
246
371
  PRINT_FMT_TOKEN_CACHE.set(printFmt, tokens);
247
372
  return tokens;
248
373
  }
374
+ /** 测试专用:重置 printf 解析缓存 */
375
+ export function resetPrintfCachesForTest() {
376
+ PRINT_FMT_TOKEN_CACHE.clear();
377
+ PRINT_FMT_SPEC_CACHE.clear();
378
+ }
379
+ /** 测试专用:读取 printf 解析缓存条目数 */
380
+ export function getPrintfCacheSizesForTest() {
381
+ return { tokens: PRINT_FMT_TOKEN_CACHE.size, specs: PRINT_FMT_SPEC_CACHE.size };
382
+ }
249
383
  function normalizePrintExpr(exprRaw) {
250
384
  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();
385
+ if (expr.startsWith("(") && expr.endsWith(")")) {
386
+ let depth = 0;
387
+ let fullyWrapped = true;
388
+ for (let i = 0; i < expr.length; i++) {
389
+ if (expr[i] === "(")
390
+ depth++;
391
+ else if (expr[i] === ")")
392
+ depth--;
393
+ if (depth === 0 && i < expr.length - 1) {
394
+ fullyWrapped = false;
395
+ break;
396
+ }
397
+ }
398
+ if (fullyWrapped && depth === 0) {
399
+ expr = expr.slice(1, -1).trim();
400
+ }
401
+ }
255
402
  expr = expr.replace(/\s+/g, "");
256
403
  return expr;
257
404
  }
@@ -267,7 +414,29 @@ function getNormalizedPrintArgs(printArgs) {
267
414
  }
268
415
  function evalPrintArgExpr(exprRaw, fieldMap) {
269
416
  const expr = normalizePrintExpr(exprRaw);
270
- let m = expr.match(/^REC->(\w+)&0xffff$/);
417
+ let m = expr.match(/^__get_str\((\w+)\)$/);
418
+ if (m) {
419
+ const v = fieldMap[m[1]];
420
+ if (typeof v === "string")
421
+ return v;
422
+ throw new Error(`__get_str(${m[1]}) requires a string field, got ${typeof v}`);
423
+ }
424
+ m = expr.match(/^__get_dynamic_array\((\w+)\)(?:\[(\d+)])?$/);
425
+ if (m) {
426
+ const v = fieldMap[m[1]];
427
+ if (!Array.isArray(v)) {
428
+ throw new Error(`__get_dynamic_array(${m[1]}) requires an array field, got ${typeof v}`);
429
+ }
430
+ if (m[2] !== undefined) {
431
+ const idx = parseInt(m[2], 10);
432
+ if (idx < 0 || idx >= v.length) {
433
+ throw new Error(`Invalid print arg index for __get_dynamic_array(${m[1]})[${m[2]}]`);
434
+ }
435
+ return v[idx];
436
+ }
437
+ return v;
438
+ }
439
+ m = expr.match(/^REC->(\w+)&0xffff$/);
271
440
  if (m) {
272
441
  const v = fieldMap[m[1]];
273
442
  const n = typeof v === "number" ? v : 0;
@@ -280,32 +449,49 @@ function evalPrintArgExpr(exprRaw, fieldMap) {
280
449
  return (n >>> 16) & 0xffff;
281
450
  }
282
451
  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];
452
+ if (m) {
453
+ const name = m[1];
454
+ const idxRaw = m[2];
455
+ const v = fieldMap[name];
456
+ if (idxRaw !== undefined) {
457
+ const idx = parseInt(idxRaw, 10);
458
+ if (Array.isArray(v) && idx >= 0 && idx < v.length) {
459
+ return v[idx];
460
+ }
461
+ throw new Error(`Invalid print arg index for REC->${name}[${idxRaw}]`);
292
462
  }
293
- return 0;
463
+ if (Array.isArray(v))
464
+ return v[0] ?? 0;
465
+ if (v !== undefined)
466
+ return v;
467
+ throw new Error(`Missing field for print arg REC->${name}`);
294
468
  }
295
- if (Array.isArray(v))
296
- return v[0] ?? 0;
297
- return v ?? 0;
469
+ throw new Error(`Unknown print arg expression: ${exprRaw.trim()}`);
298
470
  }
299
471
  function buildPrintArgValues(printArgs, fieldMap) {
300
472
  const normalizedArgs = getNormalizedPrintArgs(printArgs);
301
473
  const values = (printArgs ?? []).map((e) => evalPrintArgExpr(e, fieldMap));
302
474
  return { normalizedArgs, values };
303
475
  }
476
+ function resolvePrintArgValue(value) {
477
+ if (value === undefined)
478
+ return 0;
479
+ if (Array.isArray(value))
480
+ return value[0] ?? 0;
481
+ return value;
482
+ }
304
483
  function formatOnePrintArg(idx, spec, fieldMap, normalizedArgs, values) {
305
- const v = values[idx] ?? 0;
484
+ const v = resolvePrintArgValue(values[idx]);
306
485
  const argExpr = normalizedArgs[idx] ?? "";
307
486
  if (spec.conv === "s" || spec.conv === "S") {
308
- const m = argExpr.match(/^REC->__data_loc_(\w+)(?:&0xffff)?(?:>>16)?$/);
487
+ let m = argExpr.match(/^__get_str\((\w+)\)$/);
488
+ if (m) {
489
+ const s = fieldMap[m[1]];
490
+ if (typeof s === "string") {
491
+ return formatPrintfValue(s, spec);
492
+ }
493
+ }
494
+ m = argExpr.match(/^REC->__data_loc_(\w+)(?:&0xffff)?(?:>>16)?$/);
309
495
  if (m) {
310
496
  const s = fieldMap[m[1]];
311
497
  if (typeof s === "string") {
@@ -319,6 +505,14 @@ function fieldKeyFromNormalizedArg(normalizedArg, index) {
319
505
  let k = normalizedArg;
320
506
  if (k.startsWith("REC->"))
321
507
  k = k.slice(5);
508
+ const mGetStr = k.match(/^__get_str\((\w+)\)$/);
509
+ if (mGetStr) {
510
+ return mGetStr[1];
511
+ }
512
+ const mGetArr = k.match(/^__get_dynamic_array\((\w+)\)(?:\[(\d+)])?$/);
513
+ if (mGetArr) {
514
+ return mGetArr[2] !== undefined ? `${mGetArr[1]}__${mGetArr[2]}` : mGetArr[1];
515
+ }
322
516
  // REC->__data_loc_path&0xffff 在 fieldDict 中收敛为 path 作为键名
323
517
  const mDataLocOff = k.match(/^__data_loc_(\w+)&0xffff$/);
324
518
  if (mDataLocOff) {
@@ -326,7 +520,8 @@ function fieldKeyFromNormalizedArg(normalizedArg, index) {
326
520
  }
327
521
  return k.length > 0 ? k : `arg${index}`;
328
522
  }
329
- export function renderPrintFmtAsFieldDict(printFmt, printArgs, fieldMap) {
523
+ export function renderPrintFmtAsFieldDict(printFmt, printArgs, fieldMap, context) {
524
+ assertPrintArgCount(printFmt, printArgs, context);
330
525
  const { normalizedArgs, values } = buildPrintArgValues(printArgs, fieldMap);
331
526
  const specs = extractPrintfSpecs(printFmt);
332
527
  const out = {};
@@ -340,7 +535,8 @@ export function renderPrintFmtAsFieldDict(printFmt, printArgs, fieldMap) {
340
535
  }
341
536
  return out;
342
537
  }
343
- export function renderPrintFmt(printFmt, printArgs, fieldMap) {
538
+ export function renderPrintFmt(printFmt, printArgs, fieldMap, context) {
539
+ assertPrintArgCount(printFmt, printArgs, context);
344
540
  const { normalizedArgs, values } = buildPrintArgValues(printArgs, fieldMap);
345
541
  const tokens = getCachedPrintfTokens(printFmt);
346
542
  let argIdx = 0;
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.2",
4
4
  "description": "DFX perf trace shared types and trace format decode primitives",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",