hiperf_txt_parser 1.0.1 → 1.0.3

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.
@@ -0,0 +1,13 @@
1
+ import type { PerfData, RecordSample } from "./types.js";
2
+ /**
3
+ * 将 RecordSample 原始栈中符合 pattern 的帧转为 hstack 可解析的栈格式。
4
+ *
5
+ * 输出示例:
6
+ * #26 at triggerBinder (entry|entry|1.0.0|src/main/ets/myabilitystage/PreloadHook.ts:4:21)
7
+ */
8
+ export declare function toBackTraceStack(sample: RecordSample): string;
9
+ /**
10
+ * 批量将 PerfData 中所有 RecordSample 转为 hstack 可解析栈。
11
+ * 返回数组长度与 recordSamples 一致,元素顺序一一对应。
12
+ */
13
+ export declare function toBackTraceStacks(data: PerfData): string[];
@@ -0,0 +1,29 @@
1
+ // 例: 26:0x0000005ab8b4beee : triggerBinder:[url:entry|entry|1.0.0|src/main/ets/x.ts:4:21][...]
2
+ const RAW_STACK_PATTERN = /^\s*(\d+):0x[0-9A-Za-z]+\s*:\s*([^\[]+?)\s*:\[url:([^\]]+)\].*$/;
3
+ /**
4
+ * 将 RecordSample 原始栈中符合 pattern 的帧转为 hstack 可解析的栈格式。
5
+ *
6
+ * 输出示例:
7
+ * #26 at triggerBinder (entry|entry|1.0.0|src/main/ets/myabilitystage/PreloadHook.ts:4:21)
8
+ */
9
+ export function toBackTraceStack(sample) {
10
+ const frames = sample.callchainFrames?.frames ?? [];
11
+ const out = [];
12
+ for (const frame of frames) {
13
+ const m = frame.match(RAW_STACK_PATTERN);
14
+ if (!m)
15
+ continue;
16
+ const index = m[1];
17
+ const funcName = m[2].trim();
18
+ const urlInfo = m[3].trim();
19
+ out.push(`#${index} at ${funcName} (${urlInfo})`);
20
+ }
21
+ return out.join("\n");
22
+ }
23
+ /**
24
+ * 批量将 PerfData 中所有 RecordSample 转为 hstack 可解析栈。
25
+ * 返回数组长度与 recordSamples 一致,元素顺序一一对应。
26
+ */
27
+ export function toBackTraceStacks(data) {
28
+ return data.recordSamples.map((sample) => toBackTraceStack(sample));
29
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { parsePerfData, filterByTgid } from "./parser.js";
2
2
  export { formatPerfDataToText, formatPerfDataToJson } from "./serializer.js";
3
+ export { toBackTraceStack, toBackTraceStacks } from "./backtrace.js";
3
4
  export type { PerfData, RecordSample } from "./types.js";
4
5
  export type { RecordSampleJsonExportItem } from "./serializer.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { parsePerfData, filterByTgid } from "./parser.js";
2
2
  export { formatPerfDataToText, formatPerfDataToJson } from "./serializer.js";
3
+ export { toBackTraceStack, toBackTraceStacks } from "./backtrace.js";
package/dist/parser.js CHANGED
@@ -1,12 +1,5 @@
1
1
  const RECORD_SAMPLE_PREFIX = "record sample:";
2
2
  const RECORD_COMM_PREFIX = "record comm";
3
- /**
4
- * 获取行首空格数量(缩进)
5
- */
6
- function getIndent(line) {
7
- const m = line.match(/^(\s*)/);
8
- return m ? m[1].length : 0;
9
- }
10
3
  /**
11
4
  * 解析 "record sample: type 9, misc 2, size 520" 行
12
5
  */
@@ -23,6 +16,20 @@ function parseHeaderLine(line) {
23
16
  /**
24
17
  * 解析单个 record sample 块(已按行切分好的行数组)
25
18
  */
19
+ function isTopLevelField(trimmed) {
20
+ return (trimmed.startsWith("sample_type:") ||
21
+ trimmed.startsWith("ID ") ||
22
+ trimmed.startsWith("ip ") ||
23
+ (trimmed.startsWith("pid ") && trimmed.includes(", tid ")) ||
24
+ trimmed.startsWith("time ") ||
25
+ trimmed.startsWith("stream_id ") ||
26
+ (trimmed.startsWith("cpu ") && trimmed.includes(", res ")) ||
27
+ trimmed.startsWith("period ") ||
28
+ trimmed.startsWith("callchain nr=") ||
29
+ trimmed.startsWith("raw size=") ||
30
+ trimmed.startsWith("server nr=") ||
31
+ trimmed.startsWith("callchain: "));
32
+ }
26
33
  function parseOneBlock(lines) {
27
34
  if (lines.length === 0 || !lines[0].trimStart().startsWith(RECORD_SAMPLE_PREFIX)) {
28
35
  throw new Error("Invalid record sample block");
@@ -41,108 +48,157 @@ function parseOneBlock(lines) {
41
48
  res: 0,
42
49
  period: 0,
43
50
  };
51
+ let mode = null;
44
52
  let i = 1;
45
53
  while (i < lines.length) {
46
54
  const line = lines[i];
47
- const indent = getIndent(line);
48
- const trimmed = line.trimStart();
49
- if (indent === 0 && trimmed.length > 0) {
55
+ const trimmed = line.trim();
56
+ if (!trimmed) {
57
+ i++;
58
+ continue;
59
+ }
60
+ if (trimmed.startsWith(RECORD_SAMPLE_PREFIX) ||
61
+ trimmed.startsWith(RECORD_COMM_PREFIX)) {
50
62
  break;
51
63
  }
52
- if (indent === 2) {
53
- if (trimmed.startsWith("sample_type:")) {
54
- sample.sample_type = trimmed.replace(/^sample_type:\s*/, "").trim();
55
- }
56
- else if (trimmed.startsWith("ID ")) {
57
- sample.id = parseInt(trimmed.replace(/^ID\s+/, ""), 10) || 0;
58
- }
59
- else if (trimmed.startsWith("ip ")) {
60
- sample.ip = trimmed.replace(/^ip\s+/, "").trim();
61
- }
62
- else if (trimmed.startsWith("pid ") && trimmed.includes(", tid ")) {
63
- const pidMatch = trimmed.match(/pid\s+(\d+).*tid\s+(\d+)/);
64
- if (pidMatch) {
65
- sample.pid = parseInt(pidMatch[1], 10);
66
- sample.tid = parseInt(pidMatch[2], 10);
67
- }
68
- }
69
- else if (trimmed.startsWith("time ")) {
70
- sample.time = parseInt(trimmed.replace(/^time\s+/, ""), 10) || 0;
71
- }
72
- else if (trimmed.startsWith("stream_id ")) {
73
- sample.stream_id = parseInt(trimmed.replace(/^stream_id\s+/, ""), 10) || 0;
74
- }
75
- else if (trimmed.startsWith("cpu ") && trimmed.includes(", res ")) {
76
- const cpuMatch = trimmed.match(/cpu\s+(\d+).*res\s+(\d+)/);
77
- if (cpuMatch) {
78
- sample.cpu = parseInt(cpuMatch[1], 10);
79
- sample.res = parseInt(cpuMatch[2], 10);
80
- }
64
+ if (mode && isTopLevelField(trimmed)) {
65
+ mode = null;
66
+ continue;
67
+ }
68
+ if (trimmed.startsWith("sample_type:")) {
69
+ sample.sample_type = trimmed.replace(/^sample_type:\s*/, "").trim();
70
+ mode = null;
71
+ i++;
72
+ continue;
73
+ }
74
+ if (trimmed.startsWith("ID ")) {
75
+ sample.id = parseInt(trimmed.replace(/^ID\s+/, ""), 10) || 0;
76
+ mode = null;
77
+ i++;
78
+ continue;
79
+ }
80
+ if (trimmed.startsWith("ip ")) {
81
+ sample.ip = trimmed.replace(/^ip\s+/, "").trim();
82
+ mode = null;
83
+ i++;
84
+ continue;
85
+ }
86
+ if (trimmed.startsWith("pid ") && trimmed.includes(", tid ")) {
87
+ const pidMatch = trimmed.match(/pid\s+(\d+).*tid\s+(\d+)/);
88
+ if (pidMatch) {
89
+ sample.pid = parseInt(pidMatch[1], 10);
90
+ sample.tid = parseInt(pidMatch[2], 10);
81
91
  }
82
- else if (trimmed.startsWith("period ")) {
83
- sample.period = parseInt(trimmed.replace(/^period\s+/, ""), 10) || 0;
92
+ mode = null;
93
+ i++;
94
+ continue;
95
+ }
96
+ if (trimmed.startsWith("time ")) {
97
+ sample.time = parseInt(trimmed.replace(/^time\s+/, ""), 10) || 0;
98
+ mode = null;
99
+ i++;
100
+ continue;
101
+ }
102
+ if (trimmed.startsWith("stream_id ")) {
103
+ sample.stream_id = parseInt(trimmed.replace(/^stream_id\s+/, ""), 10) || 0;
104
+ mode = null;
105
+ i++;
106
+ continue;
107
+ }
108
+ if (trimmed.startsWith("cpu ") && trimmed.includes(", res ")) {
109
+ const cpuMatch = trimmed.match(/cpu\s+(\d+).*res\s+(\d+)/);
110
+ if (cpuMatch) {
111
+ sample.cpu = parseInt(cpuMatch[1], 10);
112
+ sample.res = parseInt(cpuMatch[2], 10);
84
113
  }
85
- else if (trimmed.startsWith("callchain nr=")) {
86
- const nrMatch = trimmed.match(/callchain\s+nr=(\d+)/);
87
- const nr = nrMatch ? parseInt(nrMatch[1], 10) : 0;
88
- const addresses = [];
114
+ mode = null;
115
+ i++;
116
+ continue;
117
+ }
118
+ if (trimmed.startsWith("period ")) {
119
+ sample.period = parseInt(trimmed.replace(/^period\s+/, ""), 10) || 0;
120
+ mode = null;
121
+ i++;
122
+ continue;
123
+ }
124
+ if (trimmed.startsWith("callchain nr=")) {
125
+ const nrMatch = trimmed.match(/callchain\s+nr=(\d+)/);
126
+ const nr = nrMatch ? parseInt(nrMatch[1], 10) : 0;
127
+ sample.callchain = { nr, addresses: [] };
128
+ mode = "callchainAddr";
129
+ i++;
130
+ continue;
131
+ }
132
+ if (trimmed.startsWith("raw size=")) {
133
+ const sizeMatch = trimmed.match(/raw\s+size=(\d+)/);
134
+ const size = sizeMatch ? parseInt(sizeMatch[1], 10) : 0;
135
+ sample.raw = { size, lines: [] };
136
+ mode = "raw";
137
+ i++;
138
+ continue;
139
+ }
140
+ if (trimmed.startsWith("server nr=")) {
141
+ const nrMatch = trimmed.match(/server\s+nr=(\d+)/);
142
+ const nr = nrMatch ? parseInt(nrMatch[1], 10) : 0;
143
+ sample.server = { nr, pids: [] };
144
+ mode = "server";
145
+ i++;
146
+ continue;
147
+ }
148
+ if (trimmed.startsWith("callchain: ")) {
149
+ const countMatch = trimmed.match(/callchain:\s*(\d+)/);
150
+ const count = countMatch ? parseInt(countMatch[1], 10) : 0;
151
+ sample.callchainFrames = { count, frames: [] };
152
+ mode = "frames";
153
+ i++;
154
+ continue;
155
+ }
156
+ if (mode === "callchainAddr") {
157
+ if (/^0x[0-9a-fA-F]+$/.test(trimmed) && sample.callchain) {
158
+ sample.callchain.addresses.push(trimmed);
89
159
  i++;
90
- while (i < lines.length && getIndent(lines[i]) >= 4) {
91
- const addr = lines[i].trim();
92
- if (addr && /^0x[0-9a-fA-F]+$/.test(addr)) {
93
- addresses.push(addr);
94
- }
95
- i++;
96
- }
97
- sample.callchain = { nr, addresses };
98
160
  continue;
99
161
  }
100
- else if (trimmed.startsWith("raw size=")) {
101
- const sizeMatch = trimmed.match(/raw\s+size=(\d+)/);
102
- const size = sizeMatch ? parseInt(sizeMatch[1], 10) : 0;
103
- const entries = [];
104
- i++;
105
- while (i < lines.length && getIndent(lines[i]) >= 4) {
106
- const rawLine = lines[i].trim();
107
- const hexShort = rawLine.match(/^(0x[0-9a-fA-F]+)\s*\(([0-9a-fA-F]+)\)$/);
108
- if (hexShort) {
109
- entries.push({ hex: hexShort[1], short: hexShort[2] });
110
- }
111
- else if (rawLine.startsWith("0x")) {
112
- entries.push({ hex: rawLine });
113
- }
162
+ mode = null;
163
+ continue;
164
+ }
165
+ if (mode === "raw") {
166
+ if (sample.raw) {
167
+ const hexShort = trimmed.match(/^(0x[0-9a-fA-F]+)\s*\(([0-9a-fA-F]+)\)$/);
168
+ if (hexShort) {
169
+ sample.raw.lines.push({ hex: hexShort[1], short: hexShort[2] });
114
170
  i++;
171
+ continue;
115
172
  }
116
- sample.raw = { size, lines: entries };
117
- continue;
118
- }
119
- else if (trimmed.startsWith("server nr=")) {
120
- const nrMatch = trimmed.match(/server\s+nr=(\d+)/);
121
- const nr = nrMatch ? parseInt(nrMatch[1], 10) : 0;
122
- const pids = [];
123
- i++;
124
- while (i < lines.length && getIndent(lines[i]) >= 4) {
125
- const pidMatch = lines[i].trim().match(/pid:\s*(\d+)/);
126
- if (pidMatch) {
127
- pids.push(parseInt(pidMatch[1], 10));
128
- }
173
+ if (/^0x[0-9a-fA-F]+$/.test(trimmed)) {
174
+ sample.raw.lines.push({ hex: trimmed });
129
175
  i++;
176
+ continue;
130
177
  }
131
- sample.server = { nr, pids };
132
- continue;
133
178
  }
134
- else if (trimmed.startsWith("callchain: ")) {
135
- const countMatch = trimmed.match(/callchain:\s*(\d+)/);
136
- const count = countMatch ? parseInt(countMatch[1], 10) : 0;
137
- const frames = [];
138
- i++;
139
- while (i < lines.length && getIndent(lines[i]) >= 4) {
140
- frames.push(lines[i].trim());
179
+ mode = null;
180
+ continue;
181
+ }
182
+ if (mode === "server") {
183
+ if (sample.server) {
184
+ const pidMatch = trimmed.match(/^pid:\s*(\d+)$/);
185
+ if (pidMatch) {
186
+ sample.server.pids.push(parseInt(pidMatch[1], 10));
141
187
  i++;
188
+ continue;
142
189
  }
143
- sample.callchainFrames = { count, frames };
190
+ }
191
+ mode = null;
192
+ continue;
193
+ }
194
+ if (mode === "frames") {
195
+ if (sample.callchainFrames) {
196
+ sample.callchainFrames.frames.push(trimmed);
197
+ i++;
144
198
  continue;
145
199
  }
200
+ mode = null;
201
+ continue;
146
202
  }
147
203
  i++;
148
204
  }
@@ -158,15 +214,14 @@ function extractRecordSampleBlocks(text) {
158
214
  for (let i = 0; i < lines.length; i++) {
159
215
  const line = lines[i];
160
216
  const trimmed = line.trimStart();
161
- const indent = getIndent(line);
162
- if (trimmed.startsWith(RECORD_SAMPLE_PREFIX) && indent === 0) {
217
+ if (trimmed.startsWith(RECORD_SAMPLE_PREFIX)) {
163
218
  if (current.length > 0) {
164
219
  blocks.push(current);
165
220
  }
166
221
  current = [line];
167
222
  continue;
168
223
  }
169
- if (trimmed.startsWith(RECORD_COMM_PREFIX) && indent === 0) {
224
+ if (trimmed.startsWith(RECORD_COMM_PREFIX)) {
170
225
  if (current.length > 0) {
171
226
  blocks.push(current);
172
227
  current = [];
@@ -33,9 +33,9 @@ function serializeOneSample(sample) {
33
33
  }
34
34
  if (sample.callchainFrames) {
35
35
  lines.push(` `);
36
- lines.push(` callchain: ${sample.callchainFrames.count}`);
36
+ lines.push(` callchain: ${sample.callchainFrames.count}`);
37
37
  for (const frame of sample.callchainFrames.frames) {
38
- lines.push(` ${frame}`);
38
+ lines.push(` ${frame}`);
39
39
  }
40
40
  }
41
41
  return lines.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hiperf_txt_parser",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Parse perf data.txt and output structured TypeScript data",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,7 @@
17
17
  "scripts": {
18
18
  "build": "tsc",
19
19
  "prepublishOnly": "npm run build",
20
- "test": "npm run build && node --test tests/filterByTgid.test.mjs"
20
+ "test": "npm run build && node --test tests/*.test.mjs"
21
21
  },
22
22
  "keywords": [
23
23
  "perf",