hiperf_txt_parser 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,74 @@
1
+ # hiperf_txt_parser
2
+
3
+ 将 perf 文本中的 `record sample` 段解析为结构化数据,并支持导出为:
4
+ - perf 原文本格式(保留缩进和行前缀)
5
+ - JSON 数组格式(每项为 `{ "issuce": "unknow", "call_chain": "..." }`)
6
+
7
+ > 本项目当前为 **纯 lib 库**,`src` 仅保留库代码,不包含 CLI。
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ npm install hiperf_txt_parser
13
+ ```
14
+
15
+ 本地调试可直接安装本地路径:
16
+
17
+ ```bash
18
+ npm install /Users/fanghaolei/Workplace/TS/snapshot_checker
19
+ ```
20
+
21
+ ## 对外 API
22
+
23
+ ```ts
24
+ import {
25
+ parsePerfData,
26
+ formatPerfDataToText,
27
+ formatPerfDataToJson,
28
+ } from "hiperf_txt_parser";
29
+ ```
30
+
31
+ - `parsePerfData(text: string): PerfData`
32
+ - `formatPerfDataToText(data: PerfData): string`
33
+ - `formatPerfDataToJson(data: PerfData): Array<{ issuce: "unknow"; call_chain: string }>`
34
+
35
+ ## 快速示例
36
+
37
+ ```ts
38
+ import { parsePerfData, formatPerfDataToJson, formatPerfDataToText } from "hiperf_txt_parser";
39
+
40
+ const input = `record sample: type 9, misc 2, size 520\n sample_type: 0x8000107e7\n ID 13`;
41
+
42
+ const parsed = parsePerfData(input);
43
+ const jsonArray = formatPerfDataToJson(parsed);
44
+ const txt = formatPerfDataToText(parsed);
45
+ ```
46
+
47
+ ## Consumer Demo(外部 TS 项目)
48
+
49
+ 提供了两个 demo:
50
+
51
+ - `demo/external-ts-consumer`:最小调用示例
52
+ - `demo/lib-consumer`:读取 `sample/perf_data.txt`,导出 json/txt 到 `out/lib-consumer/`
53
+
54
+ 运行文件 I/O demo:
55
+
56
+ ```bash
57
+ cd demo/lib-consumer
58
+ npm install
59
+ npm run demo
60
+ ```
61
+
62
+ ## 输出结构说明
63
+
64
+ - 解析结构(`parsePerfData`):`{ recordSamples: RecordSample[] }`
65
+ - JSON 导出(`formatPerfDataToJson`):
66
+
67
+ ```json
68
+ [
69
+ {
70
+ "issuce": "unknow",
71
+ "call_chain": "frame1\\nframe2\\nframe3"
72
+ }
73
+ ]
74
+ ```
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,163 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { parsePerfData } from "../parser.js";
4
+ import { formatPerfDataToText } from "../serializer.js";
5
+ const SAMPLE_TWO_RECORDS = `record sample: type 9, misc 2, size 520
6
+ sample_type: 0x8000107e7
7
+ ID 13
8
+ ip ffffffff8d20008c
9
+ pid 1234, tid 1234
10
+ time 98022340124
11
+ stream_id 13
12
+ cpu 1, res 0
13
+ period 1
14
+ callchain nr=2
15
+ 0xffffff800c2b1f2c
16
+ 0x5a5ccdea58
17
+ 0x5a5ccd83ec
18
+ raw size=8
19
+ 0x000081d2 (81d2)
20
+ 0x00001e89 (1e89)
21
+ server nr=2
22
+ pid: 2
23
+ pid: 1234
24
+
25
+ callchain: 2
26
+ 03:0xffffff800c2b1f2c : sysxxx@0xffffff800c2b1f2c@0xffffff800c2b1f2c:18446744073709551615
27
+ 02:0x5a5ccdea58 : /system/lib/ld-musl-aarch64.so.1+0xa4a58@/system/lib/ld-musl-aarch64.so.1:18446744073709551615
28
+ 01:dlopen_impl[0x0000005a5ccd7bf0:0x000000000009dbf0][0x7fc]@/system/lib/ld-musl-aarch64.so.1:0
29
+
30
+ record sample: type 9, misc 2, size 520
31
+ sample_type: 0x8000107e7
32
+ ID 13
33
+ ip ffffffff8d20008c
34
+ pid 1234, tid 1234
35
+ time 98022340124
36
+ stream_id 13
37
+ cpu 1, res 0
38
+ period 1
39
+ callchain nr=2
40
+ 0xffffff800c2b1f2c
41
+ 0x5a5ccdea58
42
+ 0x5a5ccd83ec
43
+ raw size=8
44
+ 0x000081d2 (81d2)
45
+ 0x00001e89 (1e89)
46
+ server nr=2
47
+ pid: 2
48
+ pid: 1234
49
+
50
+ callchain: 2
51
+ 03:0xffffff800c2b1f2c : sysxxx@0xffffff800c2b1f2c@0xffffff800c2b1f2c:18446744073709551615
52
+ 02:0x5a5ccdea58 : /system/lib/ld-musl-aarch64.so.1+0xa4a58@/system/lib/ld-musl-aarch64.so.1:18446744073709551615
53
+ 01:dlopen_impl[0x0000005a5ccd7bf0:0x000000000009dbf0][0x7fc]@/system/lib/ld-musl-aarch64.so.1:0`;
54
+ describe("parsePerfData", () => {
55
+ it("应解析出 2 个 record sample", () => {
56
+ const data = parsePerfData(SAMPLE_TWO_RECORDS);
57
+ assert.strictEqual(data.recordSamples.length, 2);
58
+ });
59
+ it("每个 record sample 应包含正确的 header", () => {
60
+ const data = parsePerfData(SAMPLE_TWO_RECORDS);
61
+ for (const sample of data.recordSamples) {
62
+ assert.deepStrictEqual(sample.header, { type: 9, misc: 2, size: 520 });
63
+ }
64
+ });
65
+ it("应解析 sample_type、id、ip、pid、tid、time 等字段", () => {
66
+ const data = parsePerfData(SAMPLE_TWO_RECORDS);
67
+ const s = data.recordSamples[0];
68
+ assert.strictEqual(s.sample_type, "0x8000107e7");
69
+ assert.strictEqual(s.id, 13);
70
+ assert.strictEqual(s.ip, "ffffffff8d20008c");
71
+ assert.strictEqual(s.pid, 1234);
72
+ assert.strictEqual(s.tid, 1234);
73
+ assert.strictEqual(s.time, 98022340124);
74
+ assert.strictEqual(s.stream_id, 13);
75
+ assert.strictEqual(s.cpu, 1);
76
+ assert.strictEqual(s.res, 0);
77
+ assert.strictEqual(s.period, 1);
78
+ });
79
+ it("应解析 callchain (nr + addresses)", () => {
80
+ const data = parsePerfData(SAMPLE_TWO_RECORDS);
81
+ const s = data.recordSamples[0];
82
+ assert(s.callchain);
83
+ assert.strictEqual(s.callchain.nr, 2);
84
+ assert.deepStrictEqual(s.callchain.addresses, [
85
+ "0xffffff800c2b1f2c",
86
+ "0x5a5ccdea58",
87
+ "0x5a5ccd83ec",
88
+ ]);
89
+ });
90
+ it("应解析 raw (size + hex lines)", () => {
91
+ const data = parsePerfData(SAMPLE_TWO_RECORDS);
92
+ const s = data.recordSamples[0];
93
+ assert(s.raw);
94
+ assert.strictEqual(s.raw.size, 8);
95
+ assert.deepStrictEqual(s.raw.lines, [
96
+ { hex: "0x000081d2", short: "81d2" },
97
+ { hex: "0x00001e89", short: "1e89" },
98
+ ]);
99
+ });
100
+ it("应解析 server (nr + pids)", () => {
101
+ const data = parsePerfData(SAMPLE_TWO_RECORDS);
102
+ const s = data.recordSamples[0];
103
+ assert(s.server);
104
+ assert.strictEqual(s.server.nr, 2);
105
+ assert.deepStrictEqual(s.server.pids, [2, 1234]);
106
+ });
107
+ it("应解析 callchainFrames (callchain: N + frame 行)", () => {
108
+ const data = parsePerfData(SAMPLE_TWO_RECORDS);
109
+ const s = data.recordSamples[0];
110
+ assert(s.callchainFrames);
111
+ assert.strictEqual(s.callchainFrames.count, 2);
112
+ assert.strictEqual(s.callchainFrames.frames.length, 3);
113
+ assert(s.callchainFrames.frames[0].includes("sysxxx"));
114
+ assert(s.callchainFrames.frames[1].includes("ld-musl-aarch64.so.1"));
115
+ assert(s.callchainFrames.frames[2].includes("dlopen_impl"));
116
+ });
117
+ it("应忽略 record comm 块,只保留 record sample", () => {
118
+ const text = `record comm: size 32
119
+ comm: bash
120
+
121
+ record sample: type 9, misc 2, size 520
122
+ sample_type: 0x8000107e7
123
+ ID 13
124
+ ip ffffffff8d20008c
125
+ pid 1234, tid 1234
126
+ time 98022340124
127
+ stream_id 13
128
+ cpu 1, res 0
129
+ period 1
130
+
131
+ record comm: size 16
132
+ comm: node
133
+
134
+ record sample: type 9, misc 2, size 520
135
+ sample_type: 0x8000107e7
136
+ ID 1
137
+ ip 0
138
+ pid 0, tid 0
139
+ time 0
140
+ stream_id 0
141
+ cpu 0, res 0
142
+ period 0`;
143
+ const data = parsePerfData(text);
144
+ assert.strictEqual(data.recordSamples.length, 2);
145
+ assert.strictEqual(data.recordSamples[0].id, 13);
146
+ assert.strictEqual(data.recordSamples[1].id, 1);
147
+ });
148
+ it("空文本应返回空 recordSamples", () => {
149
+ const data = parsePerfData("");
150
+ assert.deepStrictEqual(data.recordSamples, []);
151
+ });
152
+ it("仅有 record comm 时应返回空 recordSamples", () => {
153
+ const data = parsePerfData("record comm: size 32\n comm: bash");
154
+ assert.deepStrictEqual(data.recordSamples, []);
155
+ });
156
+ it("导出为文本后再解析应得到相同结构(往返一致)", () => {
157
+ const data = parsePerfData(SAMPLE_TWO_RECORDS);
158
+ const text = formatPerfDataToText(data);
159
+ const data2 = parsePerfData(text);
160
+ assert.deepStrictEqual(data.recordSamples.length, data2.recordSamples.length);
161
+ assert.deepStrictEqual(data.recordSamples, data2.recordSamples);
162
+ });
163
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
3
+ import { resolve, dirname } from "path";
4
+ import { parsePerfData } from "./parser.js";
5
+ import { formatPerfDataToText, formatPerfDataToJson } from "./serializer.js";
6
+ const HELP = `perf_parser — 解析 perf data 文本,仅提取 record sample 并输出
7
+
8
+ 用法:
9
+ perf_parser --input_file <path> [--output_file <path>] [--output_json <path>]
10
+
11
+ Flags:
12
+ --input_file <path> (必填) 输入的 perf data 文本文件路径,如 perf_data.txt
13
+ --output_file <path> (可选) 导出 txt(与 sample 同格式)到该路径;若与 --output_json 同时指定则同时输出 txt 与 json
14
+ --output_json <path> (可选) 导出 json 到该路径(callchainFrames.frames 用 \\n 拼接,每条带 issue: Unknow);若与 --output_file 同时指定则同时输出 txt 与 json
15
+ --help, -h 显示此帮助
16
+ `;
17
+ function parseArgs(argv) {
18
+ const out = { help: false };
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const arg = argv[i];
21
+ if (arg === "--help" || arg === "-h") {
22
+ out.help = true;
23
+ return out;
24
+ }
25
+ if (arg === "--input_file" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
26
+ out.inputFile = argv[++i];
27
+ }
28
+ else if (arg.startsWith("--input_file=")) {
29
+ out.inputFile = arg.slice("--input_file=".length);
30
+ }
31
+ else if (arg === "--output_file" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
32
+ out.outputFile = argv[++i];
33
+ }
34
+ else if (arg.startsWith("--output_file=")) {
35
+ out.outputFile = arg.slice("--output_file=".length);
36
+ }
37
+ else if (arg === "--output_json" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
38
+ out.outputJson = argv[++i];
39
+ }
40
+ else if (arg.startsWith("--output_json=")) {
41
+ out.outputJson = arg.slice("--output_json=".length);
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+ const args = parseArgs(process.argv.slice(2));
47
+ if (args.help) {
48
+ console.log(HELP);
49
+ process.exit(0);
50
+ }
51
+ if (!args.inputFile) {
52
+ console.error("错误: 请使用 --input_file 指定输入文件。");
53
+ console.error(HELP);
54
+ process.exit(1);
55
+ }
56
+ const inputPath = resolve(process.cwd(), args.inputFile);
57
+ let text;
58
+ try {
59
+ text = readFileSync(inputPath, "utf-8");
60
+ }
61
+ catch (e) {
62
+ console.error("无法读取文件:", inputPath, e.message);
63
+ process.exit(1);
64
+ }
65
+ const data = parsePerfData(text);
66
+ const jsonContent = JSON.stringify(formatPerfDataToJson(data), null, 2);
67
+ const textContent = formatPerfDataToText(data);
68
+ function writeTo(path, content) {
69
+ const outPath = resolve(process.cwd(), path);
70
+ const outDir = dirname(outPath);
71
+ mkdirSync(outDir, { recursive: true });
72
+ writeFileSync(outPath, content, "utf-8");
73
+ console.error("已写入:", outPath);
74
+ }
75
+ if (args.outputJson) {
76
+ try {
77
+ writeTo(args.outputJson, jsonContent);
78
+ }
79
+ catch (e) {
80
+ console.error("无法写入 JSON 文件:", e.message);
81
+ process.exit(1);
82
+ }
83
+ }
84
+ if (args.outputFile) {
85
+ try {
86
+ writeTo(args.outputFile, textContent);
87
+ }
88
+ catch (e) {
89
+ console.error("无法写入 txt 文件:", e.message);
90
+ process.exit(1);
91
+ }
92
+ }
93
+ if (!args.outputJson && !args.outputFile) {
94
+ console.log(jsonContent);
95
+ }
@@ -0,0 +1,4 @@
1
+ export { parsePerfData } from "./parser.js";
2
+ export { formatPerfDataToText, formatPerfDataToJson } from "./serializer.js";
3
+ export type { PerfData, RecordSample } from "./types.js";
4
+ export type { RecordSampleJsonExportItem } from "./serializer.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { parsePerfData } from "./parser.js";
2
+ export { formatPerfDataToText, formatPerfDataToJson } from "./serializer.js";
@@ -0,0 +1,5 @@
1
+ import type { PerfData } from "./types.js";
2
+ /**
3
+ * 解析 perf data 文本,仅返回 record sample 结构数组
4
+ */
5
+ export declare function parsePerfData(text: string): PerfData;
package/dist/parser.js ADDED
@@ -0,0 +1,204 @@
1
+ const RECORD_SAMPLE_PREFIX = "record sample:";
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
+ /**
11
+ * 解析 "record sample: type 9, misc 2, size 520" 行
12
+ */
13
+ function parseHeaderLine(line) {
14
+ const typeMatch = line.match(/type\s+(\d+)/);
15
+ const miscMatch = line.match(/misc\s+(\d+)/);
16
+ const sizeMatch = line.match(/size\s+(\d+)/);
17
+ return {
18
+ type: typeMatch ? parseInt(typeMatch[1], 10) : 0,
19
+ misc: miscMatch ? parseInt(miscMatch[1], 10) : 0,
20
+ size: sizeMatch ? parseInt(sizeMatch[1], 10) : 0,
21
+ };
22
+ }
23
+ /**
24
+ * 解析单个 record sample 块(已按行切分好的行数组)
25
+ */
26
+ function parseOneBlock(lines) {
27
+ if (lines.length === 0 || !lines[0].trimStart().startsWith(RECORD_SAMPLE_PREFIX)) {
28
+ throw new Error("Invalid record sample block");
29
+ }
30
+ const header = parseHeaderLine(lines[0]);
31
+ const sample = {
32
+ header,
33
+ sample_type: "",
34
+ id: 0,
35
+ ip: "",
36
+ pid: 0,
37
+ tid: 0,
38
+ time: 0,
39
+ stream_id: 0,
40
+ cpu: 0,
41
+ res: 0,
42
+ period: 0,
43
+ };
44
+ let i = 1;
45
+ while (i < lines.length) {
46
+ const line = lines[i];
47
+ const indent = getIndent(line);
48
+ const trimmed = line.trimStart();
49
+ if (indent === 0 && trimmed.length > 0) {
50
+ break;
51
+ }
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
+ }
81
+ }
82
+ else if (trimmed.startsWith("period ")) {
83
+ sample.period = parseInt(trimmed.replace(/^period\s+/, ""), 10) || 0;
84
+ }
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 = [];
89
+ 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
+ continue;
99
+ }
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
+ }
114
+ i++;
115
+ }
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
+ }
129
+ i++;
130
+ }
131
+ sample.server = { nr, pids };
132
+ continue;
133
+ }
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());
141
+ i++;
142
+ }
143
+ sample.callchainFrames = { count, frames };
144
+ continue;
145
+ }
146
+ }
147
+ i++;
148
+ }
149
+ return sample;
150
+ }
151
+ /**
152
+ * 从完整文本中提取所有 record sample 块(忽略 record comm 等)
153
+ */
154
+ function extractRecordSampleBlocks(text) {
155
+ const lines = text.split(/\r?\n/);
156
+ const blocks = [];
157
+ let current = [];
158
+ for (let i = 0; i < lines.length; i++) {
159
+ const line = lines[i];
160
+ const trimmed = line.trimStart();
161
+ const indent = getIndent(line);
162
+ if (trimmed.startsWith(RECORD_SAMPLE_PREFIX) && indent === 0) {
163
+ if (current.length > 0) {
164
+ blocks.push(current);
165
+ }
166
+ current = [line];
167
+ continue;
168
+ }
169
+ if (trimmed.startsWith(RECORD_COMM_PREFIX) && indent === 0) {
170
+ if (current.length > 0) {
171
+ blocks.push(current);
172
+ current = [];
173
+ }
174
+ continue;
175
+ }
176
+ if (current.length > 0) {
177
+ current.push(line);
178
+ }
179
+ }
180
+ if (current.length > 0) {
181
+ blocks.push(current);
182
+ }
183
+ return blocks;
184
+ }
185
+ /**
186
+ * 解析 perf data 文本,仅返回 record sample 结构数组
187
+ */
188
+ export function parsePerfData(text) {
189
+ const blocks = extractRecordSampleBlocks(text);
190
+ const recordSamples = [];
191
+ for (const block of blocks) {
192
+ if (block.length === 0)
193
+ continue;
194
+ if (!block[0].trimStart().startsWith(RECORD_SAMPLE_PREFIX))
195
+ continue;
196
+ try {
197
+ recordSamples.push(parseOneBlock(block));
198
+ }
199
+ catch {
200
+ // 跳过无法解析的块
201
+ }
202
+ }
203
+ return { recordSamples };
204
+ }
@@ -0,0 +1,15 @@
1
+ import type { PerfData } from "./types.js";
2
+ /**
3
+ * 将 PerfData 导出为与 sample/perf_data.txt 相同格式的文本(含缩进与每行前缀)
4
+ */
5
+ export declare function formatPerfDataToText(data: PerfData): string;
6
+ /** 导出用 JSON 的单项:issuce 固定为 "unknow",call_chain 为 frames 逐行拼接 */
7
+ export interface RecordSampleJsonExportItem {
8
+ issuce: "unknow";
9
+ call_chain: string;
10
+ }
11
+ /**
12
+ * 将 PerfData 转为导出用 JSON 结构:顶层为数组,每项为 { issuce: "unknow", call_chain: "" },
13
+ * call_chain 由对应 record sample 的 callchainFrames.frames 逐行拼接得到,无则为空字符串。
14
+ */
15
+ export declare function formatPerfDataToJson(data: PerfData): RecordSampleJsonExportItem[];
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 将单个 RecordSample 序列化为与 sample/perf_data.txt 一致的文本格式(含缩进与行前缀)
3
+ */
4
+ function serializeOneSample(sample) {
5
+ const lines = [];
6
+ const { header } = sample;
7
+ lines.push(`record sample: type ${header.type}, misc ${header.misc}, size ${header.size}`);
8
+ lines.push(` sample_type: ${sample.sample_type}`);
9
+ lines.push(` ID ${sample.id}`);
10
+ lines.push(` ip ${sample.ip}`);
11
+ lines.push(` pid ${sample.pid}, tid ${sample.tid}`);
12
+ lines.push(` time ${sample.time}`);
13
+ lines.push(` stream_id ${sample.stream_id}`);
14
+ lines.push(` cpu ${sample.cpu}, res ${sample.res}`);
15
+ lines.push(` period ${sample.period}`);
16
+ if (sample.callchain) {
17
+ lines.push(` callchain nr=${sample.callchain.nr}`);
18
+ for (const addr of sample.callchain.addresses) {
19
+ lines.push(` ${addr}`);
20
+ }
21
+ }
22
+ if (sample.raw) {
23
+ lines.push(` raw size=${sample.raw.size}`);
24
+ for (const { hex, short } of sample.raw.lines) {
25
+ lines.push(short ? ` ${hex} (${short})` : ` ${hex}`);
26
+ }
27
+ }
28
+ if (sample.server) {
29
+ lines.push(` server nr=${sample.server.nr}`);
30
+ for (const pid of sample.server.pids) {
31
+ lines.push(` pid: ${pid}`);
32
+ }
33
+ }
34
+ if (sample.callchainFrames) {
35
+ lines.push(` `);
36
+ lines.push(` callchain: ${sample.callchainFrames.count}`);
37
+ for (const frame of sample.callchainFrames.frames) {
38
+ lines.push(` ${frame}`);
39
+ }
40
+ }
41
+ return lines.join("\n");
42
+ }
43
+ /**
44
+ * 将 PerfData 导出为与 sample/perf_data.txt 相同格式的文本(含缩进与每行前缀)
45
+ */
46
+ export function formatPerfDataToText(data) {
47
+ return data.recordSamples.map(serializeOneSample).join("\n\n");
48
+ }
49
+ /**
50
+ * 将 PerfData 转为导出用 JSON 结构:顶层为数组,每项为 { issuce: "unknow", call_chain: "" },
51
+ * call_chain 由对应 record sample 的 callchainFrames.frames 逐行拼接得到,无则为空字符串。
52
+ */
53
+ export function formatPerfDataToJson(data) {
54
+ return data.recordSamples.map((s) => ({
55
+ issuce: "unknow",
56
+ call_chain: s.callchainFrames ? s.callchainFrames.frames.join("\n") : "",
57
+ }));
58
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 解析后的 perf record sample 结构
3
+ */
4
+ export interface RecordSampleHeader {
5
+ type: number;
6
+ misc: number;
7
+ size: number;
8
+ }
9
+ export interface CallchainEntry {
10
+ nr: number;
11
+ addresses: string[];
12
+ }
13
+ export interface RawEntry {
14
+ size: number;
15
+ lines: Array<{
16
+ hex: string;
17
+ short?: string;
18
+ }>;
19
+ }
20
+ export interface ServerEntry {
21
+ nr: number;
22
+ pids: number[];
23
+ }
24
+ export interface CallchainFramesEntry {
25
+ count: number;
26
+ frames: string[];
27
+ }
28
+ export interface RecordSample {
29
+ header: RecordSampleHeader;
30
+ sample_type: string;
31
+ id: number;
32
+ ip: string;
33
+ pid: number;
34
+ tid: number;
35
+ time: number;
36
+ stream_id: number;
37
+ cpu: number;
38
+ res: number;
39
+ period: number;
40
+ callchain?: CallchainEntry;
41
+ raw?: RawEntry;
42
+ server?: ServerEntry;
43
+ callchainFrames?: CallchainFramesEntry;
44
+ }
45
+ export type PerfData = {
46
+ recordSamples: RecordSample[];
47
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "hiperf_txt_parser",
3
+ "version": "1.0.0",
4
+ "description": "Parse perf data.txt and output structured TypeScript data",
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
+ "test": "npm run build && node --test dist/__tests__/parser.test.js"
20
+ },
21
+ "keywords": ["perf", "parser", "library"],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "devDependencies": {
25
+ "@types/node": "^22.10.1",
26
+ "typescript": "^5.7.2"
27
+ }
28
+ }