@obsunified/pprof-decoder 2.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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +379 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
- package/src/index.test.ts +182 -0
- package/src/index.ts +512 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sawan Ruparel and obs-unified contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @obsunified/pprof-decoder
|
|
2
|
+
|
|
3
|
+
Minimal pprof decoder + encoder. Runs in browsers and Cloudflare Workers (uses
|
|
4
|
+
the platform `DecompressionStream`/`CompressionStream` APIs — no Node-native
|
|
5
|
+
dependencies, no Emscripten).
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { decodeProfile, encodeProfile } from "@obsunified/pprof-decoder";
|
|
9
|
+
|
|
10
|
+
const profile = await decodeProfile(gzippedBuffer);
|
|
11
|
+
profile.sample.forEach((s) => console.log(s.value, s.locationId));
|
|
12
|
+
|
|
13
|
+
const reEncoded = await encodeProfile(profile);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Used by the collector's `/v1/profiles` ingest and by the dashboard's flame-graph
|
|
17
|
+
renderer. Designed to do **decode + minor re-encode**, not full profile
|
|
18
|
+
manipulation — for that, use Google's `pprof` toolchain.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal pprof decoder + encoder. Shared between the collector (server-
|
|
3
|
+
* side ingest parsing) and the dashboard (browser-side flame graph
|
|
4
|
+
* rendering). Both runtimes expose DecompressionStream/CompressionStream
|
|
5
|
+
* natively, so gzip handling lives here too.
|
|
6
|
+
*
|
|
7
|
+
* Why hand-rolled rather than `pprof-format` / `protobufjs`:
|
|
8
|
+
* - we only need 5 of pprof's message types (sample, location, function,
|
|
9
|
+
* value_type, string_table) — protobufjs pulls in a full reflection
|
|
10
|
+
* runtime that triples the dashboard bundle for no upside
|
|
11
|
+
* - the wire-format reader is < 100 lines and pure JS, deployable to
|
|
12
|
+
* Workers without polyfills
|
|
13
|
+
*
|
|
14
|
+
* Wire format reference: https://protobuf.dev/programming-guides/encoding/
|
|
15
|
+
* pprof message definitions: https://github.com/google/pprof/blob/main/proto/profile.proto
|
|
16
|
+
*
|
|
17
|
+
* Fields read: Profile.{sample_type, sample, location, function, string_table};
|
|
18
|
+
* Sample.{location_id, value, label}; Location.line.function_id; Function.{name, filename}.
|
|
19
|
+
*
|
|
20
|
+
* Fields explicitly skipped: Mapping, drop_frames, time_nanos, period,
|
|
21
|
+
* comments — none affect a flame graph render. If pprof gains a field
|
|
22
|
+
* we care about, extend decodePprof here once.
|
|
23
|
+
*/
|
|
24
|
+
declare class Reader {
|
|
25
|
+
private readonly buf;
|
|
26
|
+
private pos;
|
|
27
|
+
private readonly decoder;
|
|
28
|
+
constructor(buf: Uint8Array);
|
|
29
|
+
get eof(): boolean;
|
|
30
|
+
get position(): number;
|
|
31
|
+
readVarint(): number;
|
|
32
|
+
readBytes(len: number): Uint8Array;
|
|
33
|
+
readString(len: number): string;
|
|
34
|
+
skipField(wireType: number): void;
|
|
35
|
+
}
|
|
36
|
+
interface PprofLabel {
|
|
37
|
+
keyIdx: number;
|
|
38
|
+
strIdx: number;
|
|
39
|
+
num: number;
|
|
40
|
+
}
|
|
41
|
+
interface PprofSample {
|
|
42
|
+
locationIds: number[];
|
|
43
|
+
values: number[];
|
|
44
|
+
labels: PprofLabel[];
|
|
45
|
+
}
|
|
46
|
+
interface PprofLine {
|
|
47
|
+
functionId: number;
|
|
48
|
+
line: number;
|
|
49
|
+
}
|
|
50
|
+
interface PprofLocation {
|
|
51
|
+
id: number;
|
|
52
|
+
lines: PprofLine[];
|
|
53
|
+
functionIds: number[];
|
|
54
|
+
}
|
|
55
|
+
interface PprofFunction {
|
|
56
|
+
id: number;
|
|
57
|
+
nameIdx: number;
|
|
58
|
+
filenameIdx: number;
|
|
59
|
+
}
|
|
60
|
+
interface PprofValueType {
|
|
61
|
+
typeIdx: number;
|
|
62
|
+
unitIdx: number;
|
|
63
|
+
}
|
|
64
|
+
interface PprofProfile {
|
|
65
|
+
sampleTypes: PprofValueType[];
|
|
66
|
+
samples: PprofSample[];
|
|
67
|
+
locations: Map<number, PprofLocation>;
|
|
68
|
+
functions: Map<number, PprofFunction>;
|
|
69
|
+
stringTable: string[];
|
|
70
|
+
}
|
|
71
|
+
declare const decodePprof: (raw: Uint8Array) => PprofProfile;
|
|
72
|
+
/**
|
|
73
|
+
* Decompress (if gzipped) + decode a raw pprof byte buffer. Workers and
|
|
74
|
+
* modern browsers both expose `DecompressionStream("gzip")` natively, so
|
|
75
|
+
* a single implementation serves both runtimes.
|
|
76
|
+
*/
|
|
77
|
+
declare const decodePprofBlob: (body: ArrayBuffer | Uint8Array) => Promise<PprofProfile>;
|
|
78
|
+
declare const encodePprof: (profile: PprofProfile) => Uint8Array;
|
|
79
|
+
/** Convenience: gzip a Uint8Array using CompressionStream. */
|
|
80
|
+
declare const gzipBytes: (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
81
|
+
|
|
82
|
+
export { type PprofFunction, type PprofLabel, type PprofLine, type PprofLocation, type PprofProfile, type PprofSample, type PprofValueType, Reader, decodePprof, decodePprofBlob, encodePprof, gzipBytes };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var WIRE_VARINT = 0;
|
|
3
|
+
var WIRE_FIXED64 = 1;
|
|
4
|
+
var WIRE_LENGTH_DELIMITED = 2;
|
|
5
|
+
var WIRE_FIXED32 = 5;
|
|
6
|
+
var Reader = class {
|
|
7
|
+
constructor(buf) {
|
|
8
|
+
this.buf = buf;
|
|
9
|
+
}
|
|
10
|
+
pos = 0;
|
|
11
|
+
decoder = new TextDecoder("utf-8");
|
|
12
|
+
get eof() {
|
|
13
|
+
return this.pos >= this.buf.length;
|
|
14
|
+
}
|
|
15
|
+
get position() {
|
|
16
|
+
return this.pos;
|
|
17
|
+
}
|
|
18
|
+
readVarint() {
|
|
19
|
+
let result = 0;
|
|
20
|
+
let shift = 0;
|
|
21
|
+
while (true) {
|
|
22
|
+
if (this.pos >= this.buf.length) {
|
|
23
|
+
throw new Error("varint: unexpected end of buffer");
|
|
24
|
+
}
|
|
25
|
+
const byte = this.buf[this.pos++];
|
|
26
|
+
result += (byte & 127) * 2 ** shift;
|
|
27
|
+
if ((byte & 128) === 0) return result;
|
|
28
|
+
shift += 7;
|
|
29
|
+
if (shift > 63) throw new Error("varint: too long");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
readBytes(len) {
|
|
33
|
+
if (this.pos + len > this.buf.length) {
|
|
34
|
+
throw new Error("readBytes: out of range");
|
|
35
|
+
}
|
|
36
|
+
const slice = this.buf.subarray(this.pos, this.pos + len);
|
|
37
|
+
this.pos += len;
|
|
38
|
+
return slice;
|
|
39
|
+
}
|
|
40
|
+
readString(len) {
|
|
41
|
+
const bytes = this.readBytes(len);
|
|
42
|
+
return this.decoder.decode(bytes);
|
|
43
|
+
}
|
|
44
|
+
skipField(wireType) {
|
|
45
|
+
switch (wireType) {
|
|
46
|
+
case WIRE_VARINT:
|
|
47
|
+
this.readVarint();
|
|
48
|
+
return;
|
|
49
|
+
case WIRE_FIXED64:
|
|
50
|
+
this.pos += 8;
|
|
51
|
+
return;
|
|
52
|
+
case WIRE_LENGTH_DELIMITED: {
|
|
53
|
+
const len = this.readVarint();
|
|
54
|
+
this.pos += len;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
case WIRE_FIXED32:
|
|
58
|
+
this.pos += 4;
|
|
59
|
+
return;
|
|
60
|
+
default:
|
|
61
|
+
throw new Error(`unsupported wire type: ${wireType}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var decodePackedVarints = (r, len) => {
|
|
66
|
+
const end = r.position + len;
|
|
67
|
+
const out = [];
|
|
68
|
+
while (r.position < end) out.push(r.readVarint());
|
|
69
|
+
return out;
|
|
70
|
+
};
|
|
71
|
+
var decodeLabel = (r, len) => {
|
|
72
|
+
const end = r.position + len;
|
|
73
|
+
const label = { keyIdx: 0, strIdx: 0, num: 0 };
|
|
74
|
+
while (r.position < end) {
|
|
75
|
+
const tag = r.readVarint();
|
|
76
|
+
const fieldNum = tag >>> 3;
|
|
77
|
+
const wire = tag & 7;
|
|
78
|
+
if (fieldNum === 1 && wire === WIRE_VARINT) label.keyIdx = r.readVarint();
|
|
79
|
+
else if (fieldNum === 2 && wire === WIRE_VARINT)
|
|
80
|
+
label.strIdx = r.readVarint();
|
|
81
|
+
else if (fieldNum === 3 && wire === WIRE_VARINT) label.num = r.readVarint();
|
|
82
|
+
else r.skipField(wire);
|
|
83
|
+
}
|
|
84
|
+
return label;
|
|
85
|
+
};
|
|
86
|
+
var decodeSample = (r, len) => {
|
|
87
|
+
const end = r.position + len;
|
|
88
|
+
const sample = {
|
|
89
|
+
locationIds: [],
|
|
90
|
+
values: [],
|
|
91
|
+
labels: []
|
|
92
|
+
};
|
|
93
|
+
while (r.position < end) {
|
|
94
|
+
const tag = r.readVarint();
|
|
95
|
+
const fieldNum = tag >>> 3;
|
|
96
|
+
const wire = tag & 7;
|
|
97
|
+
if (fieldNum === 1) {
|
|
98
|
+
if (wire === WIRE_LENGTH_DELIMITED) {
|
|
99
|
+
const subLen = r.readVarint();
|
|
100
|
+
sample.locationIds.push(...decodePackedVarints(r, subLen));
|
|
101
|
+
} else if (wire === WIRE_VARINT) {
|
|
102
|
+
sample.locationIds.push(r.readVarint());
|
|
103
|
+
} else r.skipField(wire);
|
|
104
|
+
} else if (fieldNum === 2) {
|
|
105
|
+
if (wire === WIRE_LENGTH_DELIMITED) {
|
|
106
|
+
const subLen = r.readVarint();
|
|
107
|
+
sample.values.push(...decodePackedVarints(r, subLen));
|
|
108
|
+
} else if (wire === WIRE_VARINT) {
|
|
109
|
+
sample.values.push(r.readVarint());
|
|
110
|
+
} else r.skipField(wire);
|
|
111
|
+
} else if (fieldNum === 3 && wire === WIRE_LENGTH_DELIMITED) {
|
|
112
|
+
const subLen = r.readVarint();
|
|
113
|
+
sample.labels.push(decodeLabel(r, subLen));
|
|
114
|
+
} else r.skipField(wire);
|
|
115
|
+
}
|
|
116
|
+
return sample;
|
|
117
|
+
};
|
|
118
|
+
var decodeLine = (r, len) => {
|
|
119
|
+
const end = r.position + len;
|
|
120
|
+
let functionId = 0;
|
|
121
|
+
let line = 0;
|
|
122
|
+
while (r.position < end) {
|
|
123
|
+
const tag = r.readVarint();
|
|
124
|
+
const fieldNum = tag >>> 3;
|
|
125
|
+
const wire = tag & 7;
|
|
126
|
+
if (fieldNum === 1 && wire === WIRE_VARINT) functionId = r.readVarint();
|
|
127
|
+
else if (fieldNum === 2 && wire === WIRE_VARINT) line = r.readVarint();
|
|
128
|
+
else r.skipField(wire);
|
|
129
|
+
}
|
|
130
|
+
return { functionId, line };
|
|
131
|
+
};
|
|
132
|
+
var decodeLocation = (r, len) => {
|
|
133
|
+
const end = r.position + len;
|
|
134
|
+
const loc = { id: 0, lines: [], functionIds: [] };
|
|
135
|
+
while (r.position < end) {
|
|
136
|
+
const tag = r.readVarint();
|
|
137
|
+
const fieldNum = tag >>> 3;
|
|
138
|
+
const wire = tag & 7;
|
|
139
|
+
if (fieldNum === 1 && wire === WIRE_VARINT) loc.id = r.readVarint();
|
|
140
|
+
else if (fieldNum === 4 && wire === WIRE_LENGTH_DELIMITED) {
|
|
141
|
+
const subLen = r.readVarint();
|
|
142
|
+
const line = decodeLine(r, subLen);
|
|
143
|
+
loc.lines.push(line);
|
|
144
|
+
loc.functionIds.push(line.functionId);
|
|
145
|
+
} else r.skipField(wire);
|
|
146
|
+
}
|
|
147
|
+
return loc;
|
|
148
|
+
};
|
|
149
|
+
var decodeFunction = (r, len) => {
|
|
150
|
+
const end = r.position + len;
|
|
151
|
+
const fn = { id: 0, nameIdx: 0, filenameIdx: 0 };
|
|
152
|
+
while (r.position < end) {
|
|
153
|
+
const tag = r.readVarint();
|
|
154
|
+
const fieldNum = tag >>> 3;
|
|
155
|
+
const wire = tag & 7;
|
|
156
|
+
if (fieldNum === 1 && wire === WIRE_VARINT) fn.id = r.readVarint();
|
|
157
|
+
else if (fieldNum === 2 && wire === WIRE_VARINT)
|
|
158
|
+
fn.nameIdx = r.readVarint();
|
|
159
|
+
else if (fieldNum === 4 && wire === WIRE_VARINT)
|
|
160
|
+
fn.filenameIdx = r.readVarint();
|
|
161
|
+
else r.skipField(wire);
|
|
162
|
+
}
|
|
163
|
+
return fn;
|
|
164
|
+
};
|
|
165
|
+
var decodeValueType = (r, len) => {
|
|
166
|
+
const end = r.position + len;
|
|
167
|
+
const vt = { typeIdx: 0, unitIdx: 0 };
|
|
168
|
+
while (r.position < end) {
|
|
169
|
+
const tag = r.readVarint();
|
|
170
|
+
const fieldNum = tag >>> 3;
|
|
171
|
+
const wire = tag & 7;
|
|
172
|
+
if (fieldNum === 1 && wire === WIRE_VARINT) vt.typeIdx = r.readVarint();
|
|
173
|
+
else if (fieldNum === 2 && wire === WIRE_VARINT)
|
|
174
|
+
vt.unitIdx = r.readVarint();
|
|
175
|
+
else r.skipField(wire);
|
|
176
|
+
}
|
|
177
|
+
return vt;
|
|
178
|
+
};
|
|
179
|
+
var decodePprof = (raw) => {
|
|
180
|
+
const r = new Reader(raw);
|
|
181
|
+
const profile = {
|
|
182
|
+
sampleTypes: [],
|
|
183
|
+
samples: [],
|
|
184
|
+
locations: /* @__PURE__ */ new Map(),
|
|
185
|
+
functions: /* @__PURE__ */ new Map(),
|
|
186
|
+
stringTable: []
|
|
187
|
+
};
|
|
188
|
+
while (!r.eof) {
|
|
189
|
+
const tag = r.readVarint();
|
|
190
|
+
const fieldNum = tag >>> 3;
|
|
191
|
+
const wire = tag & 7;
|
|
192
|
+
if (fieldNum === 1 && wire === WIRE_LENGTH_DELIMITED) {
|
|
193
|
+
const len = r.readVarint();
|
|
194
|
+
profile.sampleTypes.push(decodeValueType(r, len));
|
|
195
|
+
} else if (fieldNum === 2 && wire === WIRE_LENGTH_DELIMITED) {
|
|
196
|
+
const len = r.readVarint();
|
|
197
|
+
profile.samples.push(decodeSample(r, len));
|
|
198
|
+
} else if (fieldNum === 4 && wire === WIRE_LENGTH_DELIMITED) {
|
|
199
|
+
const len = r.readVarint();
|
|
200
|
+
const loc = decodeLocation(r, len);
|
|
201
|
+
profile.locations.set(loc.id, loc);
|
|
202
|
+
} else if (fieldNum === 5 && wire === WIRE_LENGTH_DELIMITED) {
|
|
203
|
+
const len = r.readVarint();
|
|
204
|
+
const fn = decodeFunction(r, len);
|
|
205
|
+
profile.functions.set(fn.id, fn);
|
|
206
|
+
} else if (fieldNum === 6 && wire === WIRE_LENGTH_DELIMITED) {
|
|
207
|
+
const len = r.readVarint();
|
|
208
|
+
profile.stringTable.push(r.readString(len));
|
|
209
|
+
} else {
|
|
210
|
+
r.skipField(wire);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return profile;
|
|
214
|
+
};
|
|
215
|
+
var decodePprofBlob = async (body) => {
|
|
216
|
+
const buf = body instanceof Uint8Array ? body : new Uint8Array(body);
|
|
217
|
+
let raw;
|
|
218
|
+
if (buf.length >= 2 && buf[0] === 31 && buf[1] === 139) {
|
|
219
|
+
const stream = new Response(buf).body?.pipeThrough(
|
|
220
|
+
new DecompressionStream("gzip")
|
|
221
|
+
);
|
|
222
|
+
raw = new Uint8Array(await new Response(stream).arrayBuffer());
|
|
223
|
+
} else {
|
|
224
|
+
raw = buf;
|
|
225
|
+
}
|
|
226
|
+
return decodePprof(raw);
|
|
227
|
+
};
|
|
228
|
+
var ByteBuilder = class {
|
|
229
|
+
buf = new Uint8Array(1024 * 16);
|
|
230
|
+
// 16KB initial capacity
|
|
231
|
+
len = 0;
|
|
232
|
+
get length() {
|
|
233
|
+
return this.len;
|
|
234
|
+
}
|
|
235
|
+
push(byte) {
|
|
236
|
+
if (this.len >= this.buf.length) {
|
|
237
|
+
const next = new Uint8Array(this.buf.length * 2);
|
|
238
|
+
next.set(this.buf);
|
|
239
|
+
this.buf = next;
|
|
240
|
+
}
|
|
241
|
+
this.buf[this.len++] = byte;
|
|
242
|
+
}
|
|
243
|
+
[Symbol.iterator]() {
|
|
244
|
+
return this.buf.subarray(0, this.len)[Symbol.iterator]();
|
|
245
|
+
}
|
|
246
|
+
toArray() {
|
|
247
|
+
return this.buf.subarray(0, this.len);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
var writeVarint = (out, value) => {
|
|
251
|
+
while (value >= 128) {
|
|
252
|
+
out.push(value & 127 | 128);
|
|
253
|
+
value = Math.floor(value / 128);
|
|
254
|
+
}
|
|
255
|
+
out.push(value & 127);
|
|
256
|
+
};
|
|
257
|
+
var writeTag = (out, fieldNum, wireType) => writeVarint(out, fieldNum << 3 | wireType);
|
|
258
|
+
var writeLengthDelimited = (out, fieldNum, bytes) => {
|
|
259
|
+
writeTag(out, fieldNum, WIRE_LENGTH_DELIMITED);
|
|
260
|
+
writeVarint(out, bytes.length);
|
|
261
|
+
for (let i = 0; i < bytes.length; i++) out.push(bytes[i]);
|
|
262
|
+
};
|
|
263
|
+
var writeString = (out, fieldNum, str) => {
|
|
264
|
+
const bytes = new TextEncoder().encode(str);
|
|
265
|
+
writeLengthDelimited(out, fieldNum, bytes);
|
|
266
|
+
};
|
|
267
|
+
var writePackedVarints = (out, fieldNum, values) => {
|
|
268
|
+
if (values.length === 0) return;
|
|
269
|
+
const inner = [];
|
|
270
|
+
for (const v of values) writeVarint(inner, v);
|
|
271
|
+
writeLengthDelimited(out, fieldNum, inner);
|
|
272
|
+
};
|
|
273
|
+
var encodeLabel = (label) => {
|
|
274
|
+
const out = [];
|
|
275
|
+
if (label.keyIdx) {
|
|
276
|
+
writeTag(out, 1, WIRE_VARINT);
|
|
277
|
+
writeVarint(out, label.keyIdx);
|
|
278
|
+
}
|
|
279
|
+
if (label.strIdx) {
|
|
280
|
+
writeTag(out, 2, WIRE_VARINT);
|
|
281
|
+
writeVarint(out, label.strIdx);
|
|
282
|
+
}
|
|
283
|
+
if (label.num) {
|
|
284
|
+
writeTag(out, 3, WIRE_VARINT);
|
|
285
|
+
writeVarint(out, label.num);
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
};
|
|
289
|
+
var encodeSample = (sample) => {
|
|
290
|
+
const out = [];
|
|
291
|
+
writePackedVarints(out, 1, sample.locationIds);
|
|
292
|
+
writePackedVarints(out, 2, sample.values);
|
|
293
|
+
for (const label of sample.labels) {
|
|
294
|
+
writeLengthDelimited(out, 3, encodeLabel(label));
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
};
|
|
298
|
+
var encodeValueType = (vt) => {
|
|
299
|
+
const out = [];
|
|
300
|
+
if (vt.typeIdx) {
|
|
301
|
+
writeTag(out, 1, WIRE_VARINT);
|
|
302
|
+
writeVarint(out, vt.typeIdx);
|
|
303
|
+
}
|
|
304
|
+
if (vt.unitIdx) {
|
|
305
|
+
writeTag(out, 2, WIRE_VARINT);
|
|
306
|
+
writeVarint(out, vt.unitIdx);
|
|
307
|
+
}
|
|
308
|
+
return out;
|
|
309
|
+
};
|
|
310
|
+
var encodeLocation = (loc) => {
|
|
311
|
+
const out = [];
|
|
312
|
+
if (loc.id) {
|
|
313
|
+
writeTag(out, 1, WIRE_VARINT);
|
|
314
|
+
writeVarint(out, loc.id);
|
|
315
|
+
}
|
|
316
|
+
const lines = loc.lines || (loc.functionIds || []).map((fid) => ({ functionId: fid, line: 0 }));
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
const lineBytes = [];
|
|
319
|
+
if (line.functionId) {
|
|
320
|
+
writeTag(lineBytes, 1, WIRE_VARINT);
|
|
321
|
+
writeVarint(lineBytes, line.functionId);
|
|
322
|
+
}
|
|
323
|
+
if (line.line) {
|
|
324
|
+
writeTag(lineBytes, 2, WIRE_VARINT);
|
|
325
|
+
writeVarint(lineBytes, line.line);
|
|
326
|
+
}
|
|
327
|
+
writeLengthDelimited(out, 4, lineBytes);
|
|
328
|
+
}
|
|
329
|
+
return out;
|
|
330
|
+
};
|
|
331
|
+
var encodeFunction = (fn) => {
|
|
332
|
+
const out = [];
|
|
333
|
+
if (fn.id) {
|
|
334
|
+
writeTag(out, 1, WIRE_VARINT);
|
|
335
|
+
writeVarint(out, fn.id);
|
|
336
|
+
}
|
|
337
|
+
if (fn.nameIdx) {
|
|
338
|
+
writeTag(out, 2, WIRE_VARINT);
|
|
339
|
+
writeVarint(out, fn.nameIdx);
|
|
340
|
+
}
|
|
341
|
+
if (fn.filenameIdx) {
|
|
342
|
+
writeTag(out, 4, WIRE_VARINT);
|
|
343
|
+
writeVarint(out, fn.filenameIdx);
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
};
|
|
347
|
+
var encodePprof = (profile) => {
|
|
348
|
+
const out = new ByteBuilder();
|
|
349
|
+
for (const vt of profile.sampleTypes) {
|
|
350
|
+
writeLengthDelimited(out, 1, encodeValueType(vt));
|
|
351
|
+
}
|
|
352
|
+
for (const sample of profile.samples) {
|
|
353
|
+
writeLengthDelimited(out, 2, encodeSample(sample));
|
|
354
|
+
}
|
|
355
|
+
for (const loc of profile.locations.values()) {
|
|
356
|
+
writeLengthDelimited(out, 4, encodeLocation(loc));
|
|
357
|
+
}
|
|
358
|
+
for (const fn of profile.functions.values()) {
|
|
359
|
+
writeLengthDelimited(out, 5, encodeFunction(fn));
|
|
360
|
+
}
|
|
361
|
+
for (const str of profile.stringTable) {
|
|
362
|
+
writeString(out, 6, str);
|
|
363
|
+
}
|
|
364
|
+
return out.toArray();
|
|
365
|
+
};
|
|
366
|
+
var gzipBytes = async (bytes) => {
|
|
367
|
+
const stream = new Response(bytes).body?.pipeThrough(
|
|
368
|
+
new CompressionStream("gzip")
|
|
369
|
+
);
|
|
370
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
371
|
+
};
|
|
372
|
+
export {
|
|
373
|
+
Reader,
|
|
374
|
+
decodePprof,
|
|
375
|
+
decodePprofBlob,
|
|
376
|
+
encodePprof,
|
|
377
|
+
gzipBytes
|
|
378
|
+
};
|
|
379
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Minimal pprof decoder + encoder. Shared between the collector (server-\n * side ingest parsing) and the dashboard (browser-side flame graph\n * rendering). Both runtimes expose DecompressionStream/CompressionStream\n * natively, so gzip handling lives here too.\n *\n * Why hand-rolled rather than `pprof-format` / `protobufjs`:\n * - we only need 5 of pprof's message types (sample, location, function,\n * value_type, string_table) — protobufjs pulls in a full reflection\n * runtime that triples the dashboard bundle for no upside\n * - the wire-format reader is < 100 lines and pure JS, deployable to\n * Workers without polyfills\n *\n * Wire format reference: https://protobuf.dev/programming-guides/encoding/\n * pprof message definitions: https://github.com/google/pprof/blob/main/proto/profile.proto\n *\n * Fields read: Profile.{sample_type, sample, location, function, string_table};\n * Sample.{location_id, value, label}; Location.line.function_id; Function.{name, filename}.\n *\n * Fields explicitly skipped: Mapping, drop_frames, time_nanos, period,\n * comments — none affect a flame graph render. If pprof gains a field\n * we care about, extend decodePprof here once.\n */\n\n// ── Wire types ────────────────────────────────────────────────────────\n\nconst WIRE_VARINT = 0;\nconst WIRE_FIXED64 = 1;\nconst WIRE_LENGTH_DELIMITED = 2;\nconst WIRE_FIXED32 = 5;\n\nexport class Reader {\n\tprivate pos = 0;\n\tprivate readonly decoder = new TextDecoder(\"utf-8\");\n\tconstructor(private readonly buf: Uint8Array) {}\n\n\tget eof(): boolean {\n\t\treturn this.pos >= this.buf.length;\n\t}\n\n\tget position(): number {\n\t\treturn this.pos;\n\t}\n\n\treadVarint(): number {\n\t\tlet result = 0;\n\t\tlet shift = 0;\n\t\twhile (true) {\n\t\t\tif (this.pos >= this.buf.length) {\n\t\t\t\tthrow new Error(\"varint: unexpected end of buffer\");\n\t\t\t}\n\t\t\tconst byte = this.buf[this.pos++];\n\t\t\t// JS numbers are precise up to 2^53. pprof fields we read\n\t\t\t// (string-table indices, function ids, sample values) all fit.\n\t\t\t// We cap shift at 63 to catch malformed 11+ byte varints.\n\t\t\tresult += (byte & 0x7f) * 2 ** shift;\n\t\t\tif ((byte & 0x80) === 0) return result;\n\t\t\tshift += 7;\n\t\t\tif (shift > 63) throw new Error(\"varint: too long\");\n\t\t}\n\t}\n\n\treadBytes(len: number): Uint8Array {\n\t\tif (this.pos + len > this.buf.length) {\n\t\t\tthrow new Error(\"readBytes: out of range\");\n\t\t}\n\t\tconst slice = this.buf.subarray(this.pos, this.pos + len);\n\t\tthis.pos += len;\n\t\treturn slice;\n\t}\n\n\treadString(len: number): string {\n\t\tconst bytes = this.readBytes(len);\n\t\treturn this.decoder.decode(bytes);\n\t}\n\n\tskipField(wireType: number): void {\n\t\tswitch (wireType) {\n\t\t\tcase WIRE_VARINT:\n\t\t\t\tthis.readVarint();\n\t\t\t\treturn;\n\t\t\tcase WIRE_FIXED64:\n\t\t\t\tthis.pos += 8;\n\t\t\t\treturn;\n\t\t\tcase WIRE_LENGTH_DELIMITED: {\n\t\t\t\tconst len = this.readVarint();\n\t\t\t\tthis.pos += len;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase WIRE_FIXED32:\n\t\t\t\tthis.pos += 4;\n\t\t\t\treturn;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`unsupported wire type: ${wireType}`);\n\t\t}\n\t}\n}\n\n// ── pprof types we care about ────────────────────────────────────────\n\nexport interface PprofLabel {\n\tkeyIdx: number;\n\tstrIdx: number;\n\tnum: number;\n}\n\nexport interface PprofSample {\n\tlocationIds: number[];\n\tvalues: number[];\n\tlabels: PprofLabel[];\n}\n\nexport interface PprofLine {\n\tfunctionId: number;\n\tline: number;\n}\n\nexport interface PprofLocation {\n\tid: number;\n\tlines: PprofLine[];\n\tfunctionIds: number[];\n}\n\nexport interface PprofFunction {\n\tid: number;\n\tnameIdx: number;\n\tfilenameIdx: number;\n}\n\nexport interface PprofValueType {\n\ttypeIdx: number;\n\tunitIdx: number;\n}\n\nexport interface PprofProfile {\n\tsampleTypes: PprofValueType[];\n\tsamples: PprofSample[];\n\tlocations: Map<number, PprofLocation>;\n\tfunctions: Map<number, PprofFunction>;\n\tstringTable: string[];\n}\n\n// ── Field-level decoders ─────────────────────────────────────────────\n\nconst decodePackedVarints = (r: Reader, len: number): number[] => {\n\tconst end = r.position + len;\n\tconst out: number[] = [];\n\twhile (r.position < end) out.push(r.readVarint());\n\treturn out;\n};\n\nconst decodeLabel = (r: Reader, len: number): PprofLabel => {\n\tconst end = r.position + len;\n\tconst label: PprofLabel = { keyIdx: 0, strIdx: 0, num: 0 };\n\twhile (r.position < end) {\n\t\tconst tag = r.readVarint();\n\t\tconst fieldNum = tag >>> 3;\n\t\tconst wire = tag & 7;\n\t\tif (fieldNum === 1 && wire === WIRE_VARINT) label.keyIdx = r.readVarint();\n\t\telse if (fieldNum === 2 && wire === WIRE_VARINT)\n\t\t\tlabel.strIdx = r.readVarint();\n\t\telse if (fieldNum === 3 && wire === WIRE_VARINT) label.num = r.readVarint();\n\t\telse r.skipField(wire);\n\t}\n\treturn label;\n};\n\nconst decodeSample = (r: Reader, len: number): PprofSample => {\n\tconst end = r.position + len;\n\tconst sample: PprofSample = {\n\t\tlocationIds: [],\n\t\tvalues: [],\n\t\tlabels: [],\n\t};\n\twhile (r.position < end) {\n\t\tconst tag = r.readVarint();\n\t\tconst fieldNum = tag >>> 3;\n\t\tconst wire = tag & 7;\n\t\tif (fieldNum === 1) {\n\t\t\t// repeated uint64 location_id — usually packed\n\t\t\tif (wire === WIRE_LENGTH_DELIMITED) {\n\t\t\t\tconst subLen = r.readVarint();\n\t\t\t\tsample.locationIds.push(...decodePackedVarints(r, subLen));\n\t\t\t} else if (wire === WIRE_VARINT) {\n\t\t\t\tsample.locationIds.push(r.readVarint());\n\t\t\t} else r.skipField(wire);\n\t\t} else if (fieldNum === 2) {\n\t\t\t// repeated int64 value — usually packed\n\t\t\tif (wire === WIRE_LENGTH_DELIMITED) {\n\t\t\t\tconst subLen = r.readVarint();\n\t\t\t\tsample.values.push(...decodePackedVarints(r, subLen));\n\t\t\t} else if (wire === WIRE_VARINT) {\n\t\t\t\tsample.values.push(r.readVarint());\n\t\t\t} else r.skipField(wire);\n\t\t} else if (fieldNum === 3 && wire === WIRE_LENGTH_DELIMITED) {\n\t\t\tconst subLen = r.readVarint();\n\t\t\tsample.labels.push(decodeLabel(r, subLen));\n\t\t} else r.skipField(wire);\n\t}\n\treturn sample;\n};\n\nconst decodeLine = (r: Reader, len: number): PprofLine => {\n\tconst end = r.position + len;\n\tlet functionId = 0;\n\tlet line = 0;\n\twhile (r.position < end) {\n\t\tconst tag = r.readVarint();\n\t\tconst fieldNum = tag >>> 3;\n\t\tconst wire = tag & 7;\n\t\tif (fieldNum === 1 && wire === WIRE_VARINT) functionId = r.readVarint();\n\t\telse if (fieldNum === 2 && wire === WIRE_VARINT) line = r.readVarint();\n\t\telse r.skipField(wire);\n\t}\n\treturn { functionId, line };\n};\n\nconst decodeLocation = (r: Reader, len: number): PprofLocation => {\n\tconst end = r.position + len;\n\tconst loc: PprofLocation = { id: 0, lines: [], functionIds: [] };\n\twhile (r.position < end) {\n\t\tconst tag = r.readVarint();\n\t\tconst fieldNum = tag >>> 3;\n\t\tconst wire = tag & 7;\n\t\tif (fieldNum === 1 && wire === WIRE_VARINT) loc.id = r.readVarint();\n\t\telse if (fieldNum === 4 && wire === WIRE_LENGTH_DELIMITED) {\n\t\t\tconst subLen = r.readVarint();\n\t\t\tconst line = decodeLine(r, subLen);\n\t\t\tloc.lines.push(line);\n\t\t\tloc.functionIds.push(line.functionId);\n\t\t} else r.skipField(wire);\n\t}\n\treturn loc;\n};\n\nconst decodeFunction = (r: Reader, len: number): PprofFunction => {\n\tconst end = r.position + len;\n\tconst fn: PprofFunction = { id: 0, nameIdx: 0, filenameIdx: 0 };\n\twhile (r.position < end) {\n\t\tconst tag = r.readVarint();\n\t\tconst fieldNum = tag >>> 3;\n\t\tconst wire = tag & 7;\n\t\tif (fieldNum === 1 && wire === WIRE_VARINT) fn.id = r.readVarint();\n\t\telse if (fieldNum === 2 && wire === WIRE_VARINT)\n\t\t\tfn.nameIdx = r.readVarint();\n\t\telse if (fieldNum === 4 && wire === WIRE_VARINT)\n\t\t\tfn.filenameIdx = r.readVarint();\n\t\telse r.skipField(wire);\n\t}\n\treturn fn;\n};\n\nconst decodeValueType = (r: Reader, len: number): PprofValueType => {\n\tconst end = r.position + len;\n\tconst vt: PprofValueType = { typeIdx: 0, unitIdx: 0 };\n\twhile (r.position < end) {\n\t\tconst tag = r.readVarint();\n\t\tconst fieldNum = tag >>> 3;\n\t\tconst wire = tag & 7;\n\t\tif (fieldNum === 1 && wire === WIRE_VARINT) vt.typeIdx = r.readVarint();\n\t\telse if (fieldNum === 2 && wire === WIRE_VARINT)\n\t\t\tvt.unitIdx = r.readVarint();\n\t\telse r.skipField(wire);\n\t}\n\treturn vt;\n};\n\n// ── Top-level decoder ────────────────────────────────────────────────\n\nexport const decodePprof = (raw: Uint8Array): PprofProfile => {\n\tconst r = new Reader(raw);\n\tconst profile: PprofProfile = {\n\t\tsampleTypes: [],\n\t\tsamples: [],\n\t\tlocations: new Map(),\n\t\tfunctions: new Map(),\n\t\tstringTable: [],\n\t};\n\n\twhile (!r.eof) {\n\t\tconst tag = r.readVarint();\n\t\tconst fieldNum = tag >>> 3;\n\t\tconst wire = tag & 7;\n\t\tif (fieldNum === 1 && wire === WIRE_LENGTH_DELIMITED) {\n\t\t\tconst len = r.readVarint();\n\t\t\tprofile.sampleTypes.push(decodeValueType(r, len));\n\t\t} else if (fieldNum === 2 && wire === WIRE_LENGTH_DELIMITED) {\n\t\t\tconst len = r.readVarint();\n\t\t\tprofile.samples.push(decodeSample(r, len));\n\t\t} else if (fieldNum === 4 && wire === WIRE_LENGTH_DELIMITED) {\n\t\t\tconst len = r.readVarint();\n\t\t\tconst loc = decodeLocation(r, len);\n\t\t\tprofile.locations.set(loc.id, loc);\n\t\t} else if (fieldNum === 5 && wire === WIRE_LENGTH_DELIMITED) {\n\t\t\tconst len = r.readVarint();\n\t\t\tconst fn = decodeFunction(r, len);\n\t\t\tprofile.functions.set(fn.id, fn);\n\t\t} else if (fieldNum === 6 && wire === WIRE_LENGTH_DELIMITED) {\n\t\t\tconst len = r.readVarint();\n\t\t\tprofile.stringTable.push(r.readString(len));\n\t\t} else {\n\t\t\tr.skipField(wire);\n\t\t}\n\t}\n\n\treturn profile;\n};\n\n/**\n * Decompress (if gzipped) + decode a raw pprof byte buffer. Workers and\n * modern browsers both expose `DecompressionStream(\"gzip\")` natively, so\n * a single implementation serves both runtimes.\n */\nexport const decodePprofBlob = async (\n\tbody: ArrayBuffer | Uint8Array,\n): Promise<PprofProfile> => {\n\tconst buf = body instanceof Uint8Array ? body : new Uint8Array(body);\n\tlet raw: Uint8Array;\n\t// gzip magic byte sniff. Uploaders may pre-decompress, but the\n\t// canonical pprof wire form is gzipped.\n\tif (buf.length >= 2 && buf[0] === 0x1f && buf[1] === 0x8b) {\n\t\t// Cast: Uint8Array is a valid BodyInit at runtime in both Workers\n\t\t// and browsers; lib.dom types disagree.\n\t\tconst stream = new Response(buf as unknown as BodyInit).body?.pipeThrough(\n\t\t\tnew DecompressionStream(\"gzip\"),\n\t\t);\n\t\traw = new Uint8Array(await new Response(stream).arrayBuffer());\n\t} else {\n\t\traw = buf;\n\t}\n\treturn decodePprof(raw);\n};\n\n// ── Encoder (used by the server-side trace_id filter that re-emits a\n// smaller blob; safe to expose from the shared package).\n// Symmetric to the decoder above; only emits the fields we read.\n\ninterface ByteSink {\n\tpush(byte: number): void;\n}\n\nclass ByteBuilder implements ByteSink {\n\tprivate buf = new Uint8Array(1024 * 16); // 16KB initial capacity\n\tprivate len = 0;\n\n\tget length(): number {\n\t\treturn this.len;\n\t}\n\n\tpush(byte: number): void {\n\t\tif (this.len >= this.buf.length) {\n\t\t\tconst next = new Uint8Array(this.buf.length * 2);\n\t\t\tnext.set(this.buf);\n\t\t\tthis.buf = next;\n\t\t}\n\t\tthis.buf[this.len++] = byte;\n\t}\n\n\t[Symbol.iterator]() {\n\t\treturn this.buf.subarray(0, this.len)[Symbol.iterator]();\n\t}\n\n\ttoArray(): Uint8Array {\n\t\treturn this.buf.subarray(0, this.len);\n\t}\n}\n\nconst writeVarint = (out: ByteSink, value: number): void => {\n\twhile (value >= 0x80) {\n\t\tout.push((value & 0x7f) | 0x80);\n\t\tvalue = Math.floor(value / 128);\n\t}\n\tout.push(value & 0x7f);\n};\n\nconst writeTag = (out: ByteSink, fieldNum: number, wireType: number): void =>\n\twriteVarint(out, (fieldNum << 3) | wireType);\n\nconst writeLengthDelimited = (\n\tout: ByteSink,\n\tfieldNum: number,\n\tbytes: number[] | Uint8Array,\n): void => {\n\twriteTag(out, fieldNum, WIRE_LENGTH_DELIMITED);\n\twriteVarint(out, bytes.length);\n\tfor (let i = 0; i < bytes.length; i++) out.push(bytes[i]);\n};\n\nconst writeString = (out: ByteSink, fieldNum: number, str: string): void => {\n\tconst bytes = new TextEncoder().encode(str);\n\twriteLengthDelimited(out, fieldNum, bytes);\n};\n\nconst writePackedVarints = (\n\tout: ByteSink,\n\tfieldNum: number,\n\tvalues: number[],\n): void => {\n\tif (values.length === 0) return;\n\tconst inner: number[] = [];\n\tfor (const v of values) writeVarint(inner, v);\n\twriteLengthDelimited(out, fieldNum, inner);\n};\n\nconst encodeLabel = (label: PprofLabel): number[] => {\n\tconst out: number[] = [];\n\tif (label.keyIdx) {\n\t\twriteTag(out, 1, WIRE_VARINT);\n\t\twriteVarint(out, label.keyIdx);\n\t}\n\tif (label.strIdx) {\n\t\twriteTag(out, 2, WIRE_VARINT);\n\t\twriteVarint(out, label.strIdx);\n\t}\n\tif (label.num) {\n\t\twriteTag(out, 3, WIRE_VARINT);\n\t\twriteVarint(out, label.num);\n\t}\n\treturn out;\n};\n\nconst encodeSample = (sample: PprofSample): number[] => {\n\tconst out: number[] = [];\n\twritePackedVarints(out, 1, sample.locationIds);\n\twritePackedVarints(out, 2, sample.values);\n\tfor (const label of sample.labels) {\n\t\twriteLengthDelimited(out, 3, encodeLabel(label));\n\t}\n\treturn out;\n};\n\nconst encodeValueType = (vt: PprofValueType): number[] => {\n\tconst out: number[] = [];\n\tif (vt.typeIdx) {\n\t\twriteTag(out, 1, WIRE_VARINT);\n\t\twriteVarint(out, vt.typeIdx);\n\t}\n\tif (vt.unitIdx) {\n\t\twriteTag(out, 2, WIRE_VARINT);\n\t\twriteVarint(out, vt.unitIdx);\n\t}\n\treturn out;\n};\n\nconst encodeLocation = (loc: PprofLocation): number[] => {\n\tconst out: number[] = [];\n\tif (loc.id) {\n\t\twriteTag(out, 1, WIRE_VARINT);\n\t\twriteVarint(out, loc.id);\n\t}\n\tconst lines =\n\t\tloc.lines ||\n\t\t(loc.functionIds || []).map((fid) => ({ functionId: fid, line: 0 }));\n\tfor (const line of lines) {\n\t\tconst lineBytes: number[] = [];\n\t\tif (line.functionId) {\n\t\t\twriteTag(lineBytes, 1, WIRE_VARINT);\n\t\t\twriteVarint(lineBytes, line.functionId);\n\t\t}\n\t\tif (line.line) {\n\t\t\twriteTag(lineBytes, 2, WIRE_VARINT);\n\t\t\twriteVarint(lineBytes, line.line);\n\t\t}\n\t\twriteLengthDelimited(out, 4, lineBytes);\n\t}\n\treturn out;\n};\n\nconst encodeFunction = (fn: PprofFunction): number[] => {\n\tconst out: number[] = [];\n\tif (fn.id) {\n\t\twriteTag(out, 1, WIRE_VARINT);\n\t\twriteVarint(out, fn.id);\n\t}\n\tif (fn.nameIdx) {\n\t\twriteTag(out, 2, WIRE_VARINT);\n\t\twriteVarint(out, fn.nameIdx);\n\t}\n\tif (fn.filenameIdx) {\n\t\twriteTag(out, 4, WIRE_VARINT);\n\t\twriteVarint(out, fn.filenameIdx);\n\t}\n\treturn out;\n};\n\nexport const encodePprof = (profile: PprofProfile): Uint8Array => {\n\tconst out = new ByteBuilder();\n\tfor (const vt of profile.sampleTypes) {\n\t\twriteLengthDelimited(out, 1, encodeValueType(vt));\n\t}\n\tfor (const sample of profile.samples) {\n\t\twriteLengthDelimited(out, 2, encodeSample(sample));\n\t}\n\tfor (const loc of profile.locations.values()) {\n\t\twriteLengthDelimited(out, 4, encodeLocation(loc));\n\t}\n\tfor (const fn of profile.functions.values()) {\n\t\twriteLengthDelimited(out, 5, encodeFunction(fn));\n\t}\n\tfor (const str of profile.stringTable) {\n\t\twriteString(out, 6, str);\n\t}\n\treturn out.toArray();\n};\n\n/** Convenience: gzip a Uint8Array using CompressionStream. */\nexport const gzipBytes = async (bytes: Uint8Array): Promise<Uint8Array> => {\n\tconst stream = new Response(bytes as unknown as BodyInit).body?.pipeThrough(\n\t\tnew CompressionStream(\"gzip\"),\n\t);\n\treturn new Uint8Array(await new Response(stream).arrayBuffer());\n};\n"],"mappings":";AA0BA,IAAM,cAAc;AACpB,IAAM,eAAe;AACrB,IAAM,wBAAwB;AAC9B,IAAM,eAAe;AAEd,IAAM,SAAN,MAAa;AAAA,EAGnB,YAA6B,KAAiB;AAAjB;AAAA,EAAkB;AAAA,EAFvC,MAAM;AAAA,EACG,UAAU,IAAI,YAAY,OAAO;AAAA,EAGlD,IAAI,MAAe;AAClB,WAAO,KAAK,OAAO,KAAK,IAAI;AAAA,EAC7B;AAAA,EAEA,IAAI,WAAmB;AACtB,WAAO,KAAK;AAAA,EACb;AAAA,EAEA,aAAqB;AACpB,QAAI,SAAS;AACb,QAAI,QAAQ;AACZ,WAAO,MAAM;AACZ,UAAI,KAAK,OAAO,KAAK,IAAI,QAAQ;AAChC,cAAM,IAAI,MAAM,kCAAkC;AAAA,MACnD;AACA,YAAM,OAAO,KAAK,IAAI,KAAK,KAAK;AAIhC,iBAAW,OAAO,OAAQ,KAAK;AAC/B,WAAK,OAAO,SAAU,EAAG,QAAO;AAChC,eAAS;AACT,UAAI,QAAQ,GAAI,OAAM,IAAI,MAAM,kBAAkB;AAAA,IACnD;AAAA,EACD;AAAA,EAEA,UAAU,KAAyB;AAClC,QAAI,KAAK,MAAM,MAAM,KAAK,IAAI,QAAQ;AACrC,YAAM,IAAI,MAAM,yBAAyB;AAAA,IAC1C;AACA,UAAM,QAAQ,KAAK,IAAI,SAAS,KAAK,KAAK,KAAK,MAAM,GAAG;AACxD,SAAK,OAAO;AACZ,WAAO;AAAA,EACR;AAAA,EAEA,WAAW,KAAqB;AAC/B,UAAM,QAAQ,KAAK,UAAU,GAAG;AAChC,WAAO,KAAK,QAAQ,OAAO,KAAK;AAAA,EACjC;AAAA,EAEA,UAAU,UAAwB;AACjC,YAAQ,UAAU;AAAA,MACjB,KAAK;AACJ,aAAK,WAAW;AAChB;AAAA,MACD,KAAK;AACJ,aAAK,OAAO;AACZ;AAAA,MACD,KAAK,uBAAuB;AAC3B,cAAM,MAAM,KAAK,WAAW;AAC5B,aAAK,OAAO;AACZ;AAAA,MACD;AAAA,MACA,KAAK;AACJ,aAAK,OAAO;AACZ;AAAA,MACD;AACC,cAAM,IAAI,MAAM,0BAA0B,QAAQ,EAAE;AAAA,IACtD;AAAA,EACD;AACD;AAgDA,IAAM,sBAAsB,CAAC,GAAW,QAA0B;AACjE,QAAM,MAAM,EAAE,WAAW;AACzB,QAAM,MAAgB,CAAC;AACvB,SAAO,EAAE,WAAW,IAAK,KAAI,KAAK,EAAE,WAAW,CAAC;AAChD,SAAO;AACR;AAEA,IAAM,cAAc,CAAC,GAAW,QAA4B;AAC3D,QAAM,MAAM,EAAE,WAAW;AACzB,QAAM,QAAoB,EAAE,QAAQ,GAAG,QAAQ,GAAG,KAAK,EAAE;AACzD,SAAO,EAAE,WAAW,KAAK;AACxB,UAAM,MAAM,EAAE,WAAW;AACzB,UAAM,WAAW,QAAQ;AACzB,UAAM,OAAO,MAAM;AACnB,QAAI,aAAa,KAAK,SAAS,YAAa,OAAM,SAAS,EAAE,WAAW;AAAA,aAC/D,aAAa,KAAK,SAAS;AACnC,YAAM,SAAS,EAAE,WAAW;AAAA,aACpB,aAAa,KAAK,SAAS,YAAa,OAAM,MAAM,EAAE,WAAW;AAAA,QACrE,GAAE,UAAU,IAAI;AAAA,EACtB;AACA,SAAO;AACR;AAEA,IAAM,eAAe,CAAC,GAAW,QAA6B;AAC7D,QAAM,MAAM,EAAE,WAAW;AACzB,QAAM,SAAsB;AAAA,IAC3B,aAAa,CAAC;AAAA,IACd,QAAQ,CAAC;AAAA,IACT,QAAQ,CAAC;AAAA,EACV;AACA,SAAO,EAAE,WAAW,KAAK;AACxB,UAAM,MAAM,EAAE,WAAW;AACzB,UAAM,WAAW,QAAQ;AACzB,UAAM,OAAO,MAAM;AACnB,QAAI,aAAa,GAAG;AAEnB,UAAI,SAAS,uBAAuB;AACnC,cAAM,SAAS,EAAE,WAAW;AAC5B,eAAO,YAAY,KAAK,GAAG,oBAAoB,GAAG,MAAM,CAAC;AAAA,MAC1D,WAAW,SAAS,aAAa;AAChC,eAAO,YAAY,KAAK,EAAE,WAAW,CAAC;AAAA,MACvC,MAAO,GAAE,UAAU,IAAI;AAAA,IACxB,WAAW,aAAa,GAAG;AAE1B,UAAI,SAAS,uBAAuB;AACnC,cAAM,SAAS,EAAE,WAAW;AAC5B,eAAO,OAAO,KAAK,GAAG,oBAAoB,GAAG,MAAM,CAAC;AAAA,MACrD,WAAW,SAAS,aAAa;AAChC,eAAO,OAAO,KAAK,EAAE,WAAW,CAAC;AAAA,MAClC,MAAO,GAAE,UAAU,IAAI;AAAA,IACxB,WAAW,aAAa,KAAK,SAAS,uBAAuB;AAC5D,YAAM,SAAS,EAAE,WAAW;AAC5B,aAAO,OAAO,KAAK,YAAY,GAAG,MAAM,CAAC;AAAA,IAC1C,MAAO,GAAE,UAAU,IAAI;AAAA,EACxB;AACA,SAAO;AACR;AAEA,IAAM,aAAa,CAAC,GAAW,QAA2B;AACzD,QAAM,MAAM,EAAE,WAAW;AACzB,MAAI,aAAa;AACjB,MAAI,OAAO;AACX,SAAO,EAAE,WAAW,KAAK;AACxB,UAAM,MAAM,EAAE,WAAW;AACzB,UAAM,WAAW,QAAQ;AACzB,UAAM,OAAO,MAAM;AACnB,QAAI,aAAa,KAAK,SAAS,YAAa,cAAa,EAAE,WAAW;AAAA,aAC7D,aAAa,KAAK,SAAS,YAAa,QAAO,EAAE,WAAW;AAAA,QAChE,GAAE,UAAU,IAAI;AAAA,EACtB;AACA,SAAO,EAAE,YAAY,KAAK;AAC3B;AAEA,IAAM,iBAAiB,CAAC,GAAW,QAA+B;AACjE,QAAM,MAAM,EAAE,WAAW;AACzB,QAAM,MAAqB,EAAE,IAAI,GAAG,OAAO,CAAC,GAAG,aAAa,CAAC,EAAE;AAC/D,SAAO,EAAE,WAAW,KAAK;AACxB,UAAM,MAAM,EAAE,WAAW;AACzB,UAAM,WAAW,QAAQ;AACzB,UAAM,OAAO,MAAM;AACnB,QAAI,aAAa,KAAK,SAAS,YAAa,KAAI,KAAK,EAAE,WAAW;AAAA,aACzD,aAAa,KAAK,SAAS,uBAAuB;AAC1D,YAAM,SAAS,EAAE,WAAW;AAC5B,YAAM,OAAO,WAAW,GAAG,MAAM;AACjC,UAAI,MAAM,KAAK,IAAI;AACnB,UAAI,YAAY,KAAK,KAAK,UAAU;AAAA,IACrC,MAAO,GAAE,UAAU,IAAI;AAAA,EACxB;AACA,SAAO;AACR;AAEA,IAAM,iBAAiB,CAAC,GAAW,QAA+B;AACjE,QAAM,MAAM,EAAE,WAAW;AACzB,QAAM,KAAoB,EAAE,IAAI,GAAG,SAAS,GAAG,aAAa,EAAE;AAC9D,SAAO,EAAE,WAAW,KAAK;AACxB,UAAM,MAAM,EAAE,WAAW;AACzB,UAAM,WAAW,QAAQ;AACzB,UAAM,OAAO,MAAM;AACnB,QAAI,aAAa,KAAK,SAAS,YAAa,IAAG,KAAK,EAAE,WAAW;AAAA,aACxD,aAAa,KAAK,SAAS;AACnC,SAAG,UAAU,EAAE,WAAW;AAAA,aAClB,aAAa,KAAK,SAAS;AACnC,SAAG,cAAc,EAAE,WAAW;AAAA,QAC1B,GAAE,UAAU,IAAI;AAAA,EACtB;AACA,SAAO;AACR;AAEA,IAAM,kBAAkB,CAAC,GAAW,QAAgC;AACnE,QAAM,MAAM,EAAE,WAAW;AACzB,QAAM,KAAqB,EAAE,SAAS,GAAG,SAAS,EAAE;AACpD,SAAO,EAAE,WAAW,KAAK;AACxB,UAAM,MAAM,EAAE,WAAW;AACzB,UAAM,WAAW,QAAQ;AACzB,UAAM,OAAO,MAAM;AACnB,QAAI,aAAa,KAAK,SAAS,YAAa,IAAG,UAAU,EAAE,WAAW;AAAA,aAC7D,aAAa,KAAK,SAAS;AACnC,SAAG,UAAU,EAAE,WAAW;AAAA,QACtB,GAAE,UAAU,IAAI;AAAA,EACtB;AACA,SAAO;AACR;AAIO,IAAM,cAAc,CAAC,QAAkC;AAC7D,QAAM,IAAI,IAAI,OAAO,GAAG;AACxB,QAAM,UAAwB;AAAA,IAC7B,aAAa,CAAC;AAAA,IACd,SAAS,CAAC;AAAA,IACV,WAAW,oBAAI,IAAI;AAAA,IACnB,WAAW,oBAAI,IAAI;AAAA,IACnB,aAAa,CAAC;AAAA,EACf;AAEA,SAAO,CAAC,EAAE,KAAK;AACd,UAAM,MAAM,EAAE,WAAW;AACzB,UAAM,WAAW,QAAQ;AACzB,UAAM,OAAO,MAAM;AACnB,QAAI,aAAa,KAAK,SAAS,uBAAuB;AACrD,YAAM,MAAM,EAAE,WAAW;AACzB,cAAQ,YAAY,KAAK,gBAAgB,GAAG,GAAG,CAAC;AAAA,IACjD,WAAW,aAAa,KAAK,SAAS,uBAAuB;AAC5D,YAAM,MAAM,EAAE,WAAW;AACzB,cAAQ,QAAQ,KAAK,aAAa,GAAG,GAAG,CAAC;AAAA,IAC1C,WAAW,aAAa,KAAK,SAAS,uBAAuB;AAC5D,YAAM,MAAM,EAAE,WAAW;AACzB,YAAM,MAAM,eAAe,GAAG,GAAG;AACjC,cAAQ,UAAU,IAAI,IAAI,IAAI,GAAG;AAAA,IAClC,WAAW,aAAa,KAAK,SAAS,uBAAuB;AAC5D,YAAM,MAAM,EAAE,WAAW;AACzB,YAAM,KAAK,eAAe,GAAG,GAAG;AAChC,cAAQ,UAAU,IAAI,GAAG,IAAI,EAAE;AAAA,IAChC,WAAW,aAAa,KAAK,SAAS,uBAAuB;AAC5D,YAAM,MAAM,EAAE,WAAW;AACzB,cAAQ,YAAY,KAAK,EAAE,WAAW,GAAG,CAAC;AAAA,IAC3C,OAAO;AACN,QAAE,UAAU,IAAI;AAAA,IACjB;AAAA,EACD;AAEA,SAAO;AACR;AAOO,IAAM,kBAAkB,OAC9B,SAC2B;AAC3B,QAAM,MAAM,gBAAgB,aAAa,OAAO,IAAI,WAAW,IAAI;AACnE,MAAI;AAGJ,MAAI,IAAI,UAAU,KAAK,IAAI,CAAC,MAAM,MAAQ,IAAI,CAAC,MAAM,KAAM;AAG1D,UAAM,SAAS,IAAI,SAAS,GAA0B,EAAE,MAAM;AAAA,MAC7D,IAAI,oBAAoB,MAAM;AAAA,IAC/B;AACA,UAAM,IAAI,WAAW,MAAM,IAAI,SAAS,MAAM,EAAE,YAAY,CAAC;AAAA,EAC9D,OAAO;AACN,UAAM;AAAA,EACP;AACA,SAAO,YAAY,GAAG;AACvB;AAUA,IAAM,cAAN,MAAsC;AAAA,EAC7B,MAAM,IAAI,WAAW,OAAO,EAAE;AAAA;AAAA,EAC9B,MAAM;AAAA,EAEd,IAAI,SAAiB;AACpB,WAAO,KAAK;AAAA,EACb;AAAA,EAEA,KAAK,MAAoB;AACxB,QAAI,KAAK,OAAO,KAAK,IAAI,QAAQ;AAChC,YAAM,OAAO,IAAI,WAAW,KAAK,IAAI,SAAS,CAAC;AAC/C,WAAK,IAAI,KAAK,GAAG;AACjB,WAAK,MAAM;AAAA,IACZ;AACA,SAAK,IAAI,KAAK,KAAK,IAAI;AAAA,EACxB;AAAA,EAEA,CAAC,OAAO,QAAQ,IAAI;AACnB,WAAO,KAAK,IAAI,SAAS,GAAG,KAAK,GAAG,EAAE,OAAO,QAAQ,EAAE;AAAA,EACxD;AAAA,EAEA,UAAsB;AACrB,WAAO,KAAK,IAAI,SAAS,GAAG,KAAK,GAAG;AAAA,EACrC;AACD;AAEA,IAAM,cAAc,CAAC,KAAe,UAAwB;AAC3D,SAAO,SAAS,KAAM;AACrB,QAAI,KAAM,QAAQ,MAAQ,GAAI;AAC9B,YAAQ,KAAK,MAAM,QAAQ,GAAG;AAAA,EAC/B;AACA,MAAI,KAAK,QAAQ,GAAI;AACtB;AAEA,IAAM,WAAW,CAAC,KAAe,UAAkB,aAClD,YAAY,KAAM,YAAY,IAAK,QAAQ;AAE5C,IAAM,uBAAuB,CAC5B,KACA,UACA,UACU;AACV,WAAS,KAAK,UAAU,qBAAqB;AAC7C,cAAY,KAAK,MAAM,MAAM;AAC7B,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,KAAI,KAAK,MAAM,CAAC,CAAC;AACzD;AAEA,IAAM,cAAc,CAAC,KAAe,UAAkB,QAAsB;AAC3E,QAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,GAAG;AAC1C,uBAAqB,KAAK,UAAU,KAAK;AAC1C;AAEA,IAAM,qBAAqB,CAC1B,KACA,UACA,WACU;AACV,MAAI,OAAO,WAAW,EAAG;AACzB,QAAM,QAAkB,CAAC;AACzB,aAAW,KAAK,OAAQ,aAAY,OAAO,CAAC;AAC5C,uBAAqB,KAAK,UAAU,KAAK;AAC1C;AAEA,IAAM,cAAc,CAAC,UAAgC;AACpD,QAAM,MAAgB,CAAC;AACvB,MAAI,MAAM,QAAQ;AACjB,aAAS,KAAK,GAAG,WAAW;AAC5B,gBAAY,KAAK,MAAM,MAAM;AAAA,EAC9B;AACA,MAAI,MAAM,QAAQ;AACjB,aAAS,KAAK,GAAG,WAAW;AAC5B,gBAAY,KAAK,MAAM,MAAM;AAAA,EAC9B;AACA,MAAI,MAAM,KAAK;AACd,aAAS,KAAK,GAAG,WAAW;AAC5B,gBAAY,KAAK,MAAM,GAAG;AAAA,EAC3B;AACA,SAAO;AACR;AAEA,IAAM,eAAe,CAAC,WAAkC;AACvD,QAAM,MAAgB,CAAC;AACvB,qBAAmB,KAAK,GAAG,OAAO,WAAW;AAC7C,qBAAmB,KAAK,GAAG,OAAO,MAAM;AACxC,aAAW,SAAS,OAAO,QAAQ;AAClC,yBAAqB,KAAK,GAAG,YAAY,KAAK,CAAC;AAAA,EAChD;AACA,SAAO;AACR;AAEA,IAAM,kBAAkB,CAAC,OAAiC;AACzD,QAAM,MAAgB,CAAC;AACvB,MAAI,GAAG,SAAS;AACf,aAAS,KAAK,GAAG,WAAW;AAC5B,gBAAY,KAAK,GAAG,OAAO;AAAA,EAC5B;AACA,MAAI,GAAG,SAAS;AACf,aAAS,KAAK,GAAG,WAAW;AAC5B,gBAAY,KAAK,GAAG,OAAO;AAAA,EAC5B;AACA,SAAO;AACR;AAEA,IAAM,iBAAiB,CAAC,QAAiC;AACxD,QAAM,MAAgB,CAAC;AACvB,MAAI,IAAI,IAAI;AACX,aAAS,KAAK,GAAG,WAAW;AAC5B,gBAAY,KAAK,IAAI,EAAE;AAAA,EACxB;AACA,QAAM,QACL,IAAI,UACH,IAAI,eAAe,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,YAAY,KAAK,MAAM,EAAE,EAAE;AACpE,aAAW,QAAQ,OAAO;AACzB,UAAM,YAAsB,CAAC;AAC7B,QAAI,KAAK,YAAY;AACpB,eAAS,WAAW,GAAG,WAAW;AAClC,kBAAY,WAAW,KAAK,UAAU;AAAA,IACvC;AACA,QAAI,KAAK,MAAM;AACd,eAAS,WAAW,GAAG,WAAW;AAClC,kBAAY,WAAW,KAAK,IAAI;AAAA,IACjC;AACA,yBAAqB,KAAK,GAAG,SAAS;AAAA,EACvC;AACA,SAAO;AACR;AAEA,IAAM,iBAAiB,CAAC,OAAgC;AACvD,QAAM,MAAgB,CAAC;AACvB,MAAI,GAAG,IAAI;AACV,aAAS,KAAK,GAAG,WAAW;AAC5B,gBAAY,KAAK,GAAG,EAAE;AAAA,EACvB;AACA,MAAI,GAAG,SAAS;AACf,aAAS,KAAK,GAAG,WAAW;AAC5B,gBAAY,KAAK,GAAG,OAAO;AAAA,EAC5B;AACA,MAAI,GAAG,aAAa;AACnB,aAAS,KAAK,GAAG,WAAW;AAC5B,gBAAY,KAAK,GAAG,WAAW;AAAA,EAChC;AACA,SAAO;AACR;AAEO,IAAM,cAAc,CAAC,YAAsC;AACjE,QAAM,MAAM,IAAI,YAAY;AAC5B,aAAW,MAAM,QAAQ,aAAa;AACrC,yBAAqB,KAAK,GAAG,gBAAgB,EAAE,CAAC;AAAA,EACjD;AACA,aAAW,UAAU,QAAQ,SAAS;AACrC,yBAAqB,KAAK,GAAG,aAAa,MAAM,CAAC;AAAA,EAClD;AACA,aAAW,OAAO,QAAQ,UAAU,OAAO,GAAG;AAC7C,yBAAqB,KAAK,GAAG,eAAe,GAAG,CAAC;AAAA,EACjD;AACA,aAAW,MAAM,QAAQ,UAAU,OAAO,GAAG;AAC5C,yBAAqB,KAAK,GAAG,eAAe,EAAE,CAAC;AAAA,EAChD;AACA,aAAW,OAAO,QAAQ,aAAa;AACtC,gBAAY,KAAK,GAAG,GAAG;AAAA,EACxB;AACA,SAAO,IAAI,QAAQ;AACpB;AAGO,IAAM,YAAY,OAAO,UAA2C;AAC1E,QAAM,SAAS,IAAI,SAAS,KAA4B,EAAE,MAAM;AAAA,IAC/D,IAAI,kBAAkB,MAAM;AAAA,EAC7B;AACA,SAAO,IAAI,WAAW,MAAM,IAAI,SAAS,MAAM,EAAE,YAAY,CAAC;AAC/D;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@obsunified/pprof-decoder",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Minimal pprof decoder + encoder. Runs in browsers and Cloudflare Workers (uses DecompressionStream/CompressionStream).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/obs-unified/obs-unified.git",
|
|
9
|
+
"directory": "packages/pprof-decoder"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"tsup": "^8.3.5",
|
|
28
|
+
"typescript": "^6.0.2"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"registry": "https://registry.npmjs.org/"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"type-check": "tsc --noEmit",
|
|
37
|
+
"test": "vitest run --passWithNoTests"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust-boundary tests for @obsunified/pprof-decoder.
|
|
3
|
+
*
|
|
4
|
+
* The collector decodes pprof bytes straight from untrusted producers
|
|
5
|
+
* (telemetry-sdk emitters and Beyla agents). Bugs in this decoder either
|
|
6
|
+
* crash the receiver or silently produce wrong analytics. The tests
|
|
7
|
+
* focus on (a) the encode→decode round trip we rely on for the trace_id
|
|
8
|
+
* filter and (b) failure modes on malformed input.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
decodePprof,
|
|
14
|
+
decodePprofBlob,
|
|
15
|
+
encodePprof,
|
|
16
|
+
gzipBytes,
|
|
17
|
+
type PprofProfile,
|
|
18
|
+
Reader,
|
|
19
|
+
} from "./index";
|
|
20
|
+
|
|
21
|
+
const emptyProfile = (): PprofProfile => ({
|
|
22
|
+
sampleTypes: [],
|
|
23
|
+
samples: [],
|
|
24
|
+
locations: new Map(),
|
|
25
|
+
functions: new Map(),
|
|
26
|
+
stringTable: [""],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const tinyProfile = (): PprofProfile => {
|
|
30
|
+
const stringTable = [
|
|
31
|
+
"",
|
|
32
|
+
"cpu",
|
|
33
|
+
"nanoseconds",
|
|
34
|
+
"main.handle",
|
|
35
|
+
"main.go",
|
|
36
|
+
"trace_id",
|
|
37
|
+
"abc123def4567890",
|
|
38
|
+
];
|
|
39
|
+
const functions = new Map();
|
|
40
|
+
functions.set(1, { id: 1, nameIdx: 3, filenameIdx: 4 });
|
|
41
|
+
const locations = new Map();
|
|
42
|
+
locations.set(1, {
|
|
43
|
+
id: 1,
|
|
44
|
+
lines: [{ functionId: 1, line: 42 }],
|
|
45
|
+
functionIds: [1],
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
sampleTypes: [{ typeIdx: 1, unitIdx: 2 }],
|
|
49
|
+
samples: [
|
|
50
|
+
{
|
|
51
|
+
locationIds: [1],
|
|
52
|
+
values: [500],
|
|
53
|
+
labels: [{ keyIdx: 5, strIdx: 6, num: 0 }],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
locations,
|
|
57
|
+
functions,
|
|
58
|
+
stringTable,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe("pprof-decoder round-trip", () => {
|
|
63
|
+
it("decodes a tiny encoded profile preserving structure", () => {
|
|
64
|
+
const encoded = encodePprof(tinyProfile());
|
|
65
|
+
const decoded = decodePprof(encoded);
|
|
66
|
+
|
|
67
|
+
expect(decoded.stringTable).toEqual([
|
|
68
|
+
"",
|
|
69
|
+
"cpu",
|
|
70
|
+
"nanoseconds",
|
|
71
|
+
"main.handle",
|
|
72
|
+
"main.go",
|
|
73
|
+
"trace_id",
|
|
74
|
+
"abc123def4567890",
|
|
75
|
+
]);
|
|
76
|
+
expect(decoded.sampleTypes).toEqual([{ typeIdx: 1, unitIdx: 2 }]);
|
|
77
|
+
expect(decoded.samples).toHaveLength(1);
|
|
78
|
+
expect(decoded.samples[0].values).toEqual([500]);
|
|
79
|
+
expect(decoded.samples[0].locationIds).toEqual([1]);
|
|
80
|
+
expect(decoded.samples[0].labels[0]).toEqual({
|
|
81
|
+
keyIdx: 5,
|
|
82
|
+
strIdx: 6,
|
|
83
|
+
num: 0,
|
|
84
|
+
});
|
|
85
|
+
expect(decoded.functions.get(1)).toEqual({
|
|
86
|
+
id: 1,
|
|
87
|
+
nameIdx: 3,
|
|
88
|
+
filenameIdx: 4,
|
|
89
|
+
});
|
|
90
|
+
expect(decoded.locations.get(1)).toEqual({
|
|
91
|
+
id: 1,
|
|
92
|
+
lines: [{ functionId: 1, line: 42 }],
|
|
93
|
+
functionIds: [1],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("round-trips an empty profile to a non-throwing decode", () => {
|
|
98
|
+
const decoded = decodePprof(encodePprof(emptyProfile()));
|
|
99
|
+
expect(decoded.samples).toEqual([]);
|
|
100
|
+
expect(decoded.locations.size).toBe(0);
|
|
101
|
+
expect(decoded.functions.size).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("pprof-decoder blob handling", () => {
|
|
106
|
+
it("decodes a gzipped blob", async () => {
|
|
107
|
+
const raw = encodePprof(tinyProfile());
|
|
108
|
+
const gz = await gzipBytes(raw);
|
|
109
|
+
expect(gz[0]).toBe(0x1f);
|
|
110
|
+
expect(gz[1]).toBe(0x8b);
|
|
111
|
+
const decoded = await decodePprofBlob(gz);
|
|
112
|
+
expect(decoded.samples).toHaveLength(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("decodes an uncompressed blob (no gzip magic) via passthrough", async () => {
|
|
116
|
+
const raw = encodePprof(tinyProfile());
|
|
117
|
+
const decoded = await decodePprofBlob(raw);
|
|
118
|
+
expect(decoded.samples).toHaveLength(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("accepts ArrayBuffer input", async () => {
|
|
122
|
+
const raw = encodePprof(tinyProfile());
|
|
123
|
+
const ab = raw.buffer.slice(
|
|
124
|
+
raw.byteOffset,
|
|
125
|
+
raw.byteOffset + raw.byteLength,
|
|
126
|
+
) as ArrayBuffer;
|
|
127
|
+
const decoded = await decodePprofBlob(ab);
|
|
128
|
+
expect(decoded.samples).toHaveLength(1);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("pprof-decoder error paths", () => {
|
|
133
|
+
it("throws on truncated varint (high bit set, no continuation byte)", () => {
|
|
134
|
+
// 0x80 keeps the continuation bit on but the buffer ends.
|
|
135
|
+
const r = new Reader(new Uint8Array([0x80]));
|
|
136
|
+
expect(() => r.readVarint()).toThrow(/unexpected end/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("throws on over-long varint (>10 bytes / shift > 63)", () => {
|
|
140
|
+
// 11 bytes of 0xff would push shift past 63 before terminating.
|
|
141
|
+
const bytes = new Uint8Array(11);
|
|
142
|
+
for (let i = 0; i < 10; i++) bytes[i] = 0xff;
|
|
143
|
+
bytes[10] = 0x7f;
|
|
144
|
+
const r = new Reader(bytes);
|
|
145
|
+
expect(() => r.readVarint()).toThrow(/too long/);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("throws on readBytes out-of-range", () => {
|
|
149
|
+
const r = new Reader(new Uint8Array([0x01, 0x02]));
|
|
150
|
+
expect(() => r.readBytes(10)).toThrow(/out of range/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("throws on unsupported wire type in skipField", () => {
|
|
154
|
+
const r = new Reader(new Uint8Array([0x00]));
|
|
155
|
+
expect(() => r.skipField(7)).toThrow(/unsupported wire type/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("decodePprof throws on a truncated stream", () => {
|
|
159
|
+
const encoded = encodePprof(tinyProfile());
|
|
160
|
+
// Lop off the last 5 bytes — guaranteed to land mid-field.
|
|
161
|
+
const truncated = encoded.slice(0, Math.max(0, encoded.length - 5));
|
|
162
|
+
expect(() => decodePprof(truncated)).toThrow();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("decodePprof skips unknown top-level fields without throwing", () => {
|
|
166
|
+
// Construct a buffer that opens with a known field then an unknown
|
|
167
|
+
// (field 99, wire 2 = length-delimited, len 3). The decoder must
|
|
168
|
+
// skipField rather than choke.
|
|
169
|
+
const known = encodePprof(emptyProfile());
|
|
170
|
+
const unknown = new Uint8Array([
|
|
171
|
+
(99 << 3) | 2, // tag: field 99, wire 2
|
|
172
|
+
3, // len
|
|
173
|
+
0x01,
|
|
174
|
+
0x02,
|
|
175
|
+
0x03,
|
|
176
|
+
]);
|
|
177
|
+
const merged = new Uint8Array(known.length + unknown.length);
|
|
178
|
+
merged.set(known, 0);
|
|
179
|
+
merged.set(unknown, known.length);
|
|
180
|
+
expect(() => decodePprof(merged)).not.toThrow();
|
|
181
|
+
});
|
|
182
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal pprof decoder + encoder. Shared between the collector (server-
|
|
3
|
+
* side ingest parsing) and the dashboard (browser-side flame graph
|
|
4
|
+
* rendering). Both runtimes expose DecompressionStream/CompressionStream
|
|
5
|
+
* natively, so gzip handling lives here too.
|
|
6
|
+
*
|
|
7
|
+
* Why hand-rolled rather than `pprof-format` / `protobufjs`:
|
|
8
|
+
* - we only need 5 of pprof's message types (sample, location, function,
|
|
9
|
+
* value_type, string_table) — protobufjs pulls in a full reflection
|
|
10
|
+
* runtime that triples the dashboard bundle for no upside
|
|
11
|
+
* - the wire-format reader is < 100 lines and pure JS, deployable to
|
|
12
|
+
* Workers without polyfills
|
|
13
|
+
*
|
|
14
|
+
* Wire format reference: https://protobuf.dev/programming-guides/encoding/
|
|
15
|
+
* pprof message definitions: https://github.com/google/pprof/blob/main/proto/profile.proto
|
|
16
|
+
*
|
|
17
|
+
* Fields read: Profile.{sample_type, sample, location, function, string_table};
|
|
18
|
+
* Sample.{location_id, value, label}; Location.line.function_id; Function.{name, filename}.
|
|
19
|
+
*
|
|
20
|
+
* Fields explicitly skipped: Mapping, drop_frames, time_nanos, period,
|
|
21
|
+
* comments — none affect a flame graph render. If pprof gains a field
|
|
22
|
+
* we care about, extend decodePprof here once.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// ── Wire types ────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const WIRE_VARINT = 0;
|
|
28
|
+
const WIRE_FIXED64 = 1;
|
|
29
|
+
const WIRE_LENGTH_DELIMITED = 2;
|
|
30
|
+
const WIRE_FIXED32 = 5;
|
|
31
|
+
|
|
32
|
+
export class Reader {
|
|
33
|
+
private pos = 0;
|
|
34
|
+
private readonly decoder = new TextDecoder("utf-8");
|
|
35
|
+
constructor(private readonly buf: Uint8Array) {}
|
|
36
|
+
|
|
37
|
+
get eof(): boolean {
|
|
38
|
+
return this.pos >= this.buf.length;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get position(): number {
|
|
42
|
+
return this.pos;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
readVarint(): number {
|
|
46
|
+
let result = 0;
|
|
47
|
+
let shift = 0;
|
|
48
|
+
while (true) {
|
|
49
|
+
if (this.pos >= this.buf.length) {
|
|
50
|
+
throw new Error("varint: unexpected end of buffer");
|
|
51
|
+
}
|
|
52
|
+
const byte = this.buf[this.pos++];
|
|
53
|
+
// JS numbers are precise up to 2^53. pprof fields we read
|
|
54
|
+
// (string-table indices, function ids, sample values) all fit.
|
|
55
|
+
// We cap shift at 63 to catch malformed 11+ byte varints.
|
|
56
|
+
result += (byte & 0x7f) * 2 ** shift;
|
|
57
|
+
if ((byte & 0x80) === 0) return result;
|
|
58
|
+
shift += 7;
|
|
59
|
+
if (shift > 63) throw new Error("varint: too long");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
readBytes(len: number): Uint8Array {
|
|
64
|
+
if (this.pos + len > this.buf.length) {
|
|
65
|
+
throw new Error("readBytes: out of range");
|
|
66
|
+
}
|
|
67
|
+
const slice = this.buf.subarray(this.pos, this.pos + len);
|
|
68
|
+
this.pos += len;
|
|
69
|
+
return slice;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
readString(len: number): string {
|
|
73
|
+
const bytes = this.readBytes(len);
|
|
74
|
+
return this.decoder.decode(bytes);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
skipField(wireType: number): void {
|
|
78
|
+
switch (wireType) {
|
|
79
|
+
case WIRE_VARINT:
|
|
80
|
+
this.readVarint();
|
|
81
|
+
return;
|
|
82
|
+
case WIRE_FIXED64:
|
|
83
|
+
this.pos += 8;
|
|
84
|
+
return;
|
|
85
|
+
case WIRE_LENGTH_DELIMITED: {
|
|
86
|
+
const len = this.readVarint();
|
|
87
|
+
this.pos += len;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
case WIRE_FIXED32:
|
|
91
|
+
this.pos += 4;
|
|
92
|
+
return;
|
|
93
|
+
default:
|
|
94
|
+
throw new Error(`unsupported wire type: ${wireType}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── pprof types we care about ────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export interface PprofLabel {
|
|
102
|
+
keyIdx: number;
|
|
103
|
+
strIdx: number;
|
|
104
|
+
num: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface PprofSample {
|
|
108
|
+
locationIds: number[];
|
|
109
|
+
values: number[];
|
|
110
|
+
labels: PprofLabel[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface PprofLine {
|
|
114
|
+
functionId: number;
|
|
115
|
+
line: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface PprofLocation {
|
|
119
|
+
id: number;
|
|
120
|
+
lines: PprofLine[];
|
|
121
|
+
functionIds: number[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface PprofFunction {
|
|
125
|
+
id: number;
|
|
126
|
+
nameIdx: number;
|
|
127
|
+
filenameIdx: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface PprofValueType {
|
|
131
|
+
typeIdx: number;
|
|
132
|
+
unitIdx: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface PprofProfile {
|
|
136
|
+
sampleTypes: PprofValueType[];
|
|
137
|
+
samples: PprofSample[];
|
|
138
|
+
locations: Map<number, PprofLocation>;
|
|
139
|
+
functions: Map<number, PprofFunction>;
|
|
140
|
+
stringTable: string[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Field-level decoders ─────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
const decodePackedVarints = (r: Reader, len: number): number[] => {
|
|
146
|
+
const end = r.position + len;
|
|
147
|
+
const out: number[] = [];
|
|
148
|
+
while (r.position < end) out.push(r.readVarint());
|
|
149
|
+
return out;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const decodeLabel = (r: Reader, len: number): PprofLabel => {
|
|
153
|
+
const end = r.position + len;
|
|
154
|
+
const label: PprofLabel = { keyIdx: 0, strIdx: 0, num: 0 };
|
|
155
|
+
while (r.position < end) {
|
|
156
|
+
const tag = r.readVarint();
|
|
157
|
+
const fieldNum = tag >>> 3;
|
|
158
|
+
const wire = tag & 7;
|
|
159
|
+
if (fieldNum === 1 && wire === WIRE_VARINT) label.keyIdx = r.readVarint();
|
|
160
|
+
else if (fieldNum === 2 && wire === WIRE_VARINT)
|
|
161
|
+
label.strIdx = r.readVarint();
|
|
162
|
+
else if (fieldNum === 3 && wire === WIRE_VARINT) label.num = r.readVarint();
|
|
163
|
+
else r.skipField(wire);
|
|
164
|
+
}
|
|
165
|
+
return label;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const decodeSample = (r: Reader, len: number): PprofSample => {
|
|
169
|
+
const end = r.position + len;
|
|
170
|
+
const sample: PprofSample = {
|
|
171
|
+
locationIds: [],
|
|
172
|
+
values: [],
|
|
173
|
+
labels: [],
|
|
174
|
+
};
|
|
175
|
+
while (r.position < end) {
|
|
176
|
+
const tag = r.readVarint();
|
|
177
|
+
const fieldNum = tag >>> 3;
|
|
178
|
+
const wire = tag & 7;
|
|
179
|
+
if (fieldNum === 1) {
|
|
180
|
+
// repeated uint64 location_id — usually packed
|
|
181
|
+
if (wire === WIRE_LENGTH_DELIMITED) {
|
|
182
|
+
const subLen = r.readVarint();
|
|
183
|
+
sample.locationIds.push(...decodePackedVarints(r, subLen));
|
|
184
|
+
} else if (wire === WIRE_VARINT) {
|
|
185
|
+
sample.locationIds.push(r.readVarint());
|
|
186
|
+
} else r.skipField(wire);
|
|
187
|
+
} else if (fieldNum === 2) {
|
|
188
|
+
// repeated int64 value — usually packed
|
|
189
|
+
if (wire === WIRE_LENGTH_DELIMITED) {
|
|
190
|
+
const subLen = r.readVarint();
|
|
191
|
+
sample.values.push(...decodePackedVarints(r, subLen));
|
|
192
|
+
} else if (wire === WIRE_VARINT) {
|
|
193
|
+
sample.values.push(r.readVarint());
|
|
194
|
+
} else r.skipField(wire);
|
|
195
|
+
} else if (fieldNum === 3 && wire === WIRE_LENGTH_DELIMITED) {
|
|
196
|
+
const subLen = r.readVarint();
|
|
197
|
+
sample.labels.push(decodeLabel(r, subLen));
|
|
198
|
+
} else r.skipField(wire);
|
|
199
|
+
}
|
|
200
|
+
return sample;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const decodeLine = (r: Reader, len: number): PprofLine => {
|
|
204
|
+
const end = r.position + len;
|
|
205
|
+
let functionId = 0;
|
|
206
|
+
let line = 0;
|
|
207
|
+
while (r.position < end) {
|
|
208
|
+
const tag = r.readVarint();
|
|
209
|
+
const fieldNum = tag >>> 3;
|
|
210
|
+
const wire = tag & 7;
|
|
211
|
+
if (fieldNum === 1 && wire === WIRE_VARINT) functionId = r.readVarint();
|
|
212
|
+
else if (fieldNum === 2 && wire === WIRE_VARINT) line = r.readVarint();
|
|
213
|
+
else r.skipField(wire);
|
|
214
|
+
}
|
|
215
|
+
return { functionId, line };
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const decodeLocation = (r: Reader, len: number): PprofLocation => {
|
|
219
|
+
const end = r.position + len;
|
|
220
|
+
const loc: PprofLocation = { id: 0, lines: [], functionIds: [] };
|
|
221
|
+
while (r.position < end) {
|
|
222
|
+
const tag = r.readVarint();
|
|
223
|
+
const fieldNum = tag >>> 3;
|
|
224
|
+
const wire = tag & 7;
|
|
225
|
+
if (fieldNum === 1 && wire === WIRE_VARINT) loc.id = r.readVarint();
|
|
226
|
+
else if (fieldNum === 4 && wire === WIRE_LENGTH_DELIMITED) {
|
|
227
|
+
const subLen = r.readVarint();
|
|
228
|
+
const line = decodeLine(r, subLen);
|
|
229
|
+
loc.lines.push(line);
|
|
230
|
+
loc.functionIds.push(line.functionId);
|
|
231
|
+
} else r.skipField(wire);
|
|
232
|
+
}
|
|
233
|
+
return loc;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const decodeFunction = (r: Reader, len: number): PprofFunction => {
|
|
237
|
+
const end = r.position + len;
|
|
238
|
+
const fn: PprofFunction = { id: 0, nameIdx: 0, filenameIdx: 0 };
|
|
239
|
+
while (r.position < end) {
|
|
240
|
+
const tag = r.readVarint();
|
|
241
|
+
const fieldNum = tag >>> 3;
|
|
242
|
+
const wire = tag & 7;
|
|
243
|
+
if (fieldNum === 1 && wire === WIRE_VARINT) fn.id = r.readVarint();
|
|
244
|
+
else if (fieldNum === 2 && wire === WIRE_VARINT)
|
|
245
|
+
fn.nameIdx = r.readVarint();
|
|
246
|
+
else if (fieldNum === 4 && wire === WIRE_VARINT)
|
|
247
|
+
fn.filenameIdx = r.readVarint();
|
|
248
|
+
else r.skipField(wire);
|
|
249
|
+
}
|
|
250
|
+
return fn;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const decodeValueType = (r: Reader, len: number): PprofValueType => {
|
|
254
|
+
const end = r.position + len;
|
|
255
|
+
const vt: PprofValueType = { typeIdx: 0, unitIdx: 0 };
|
|
256
|
+
while (r.position < end) {
|
|
257
|
+
const tag = r.readVarint();
|
|
258
|
+
const fieldNum = tag >>> 3;
|
|
259
|
+
const wire = tag & 7;
|
|
260
|
+
if (fieldNum === 1 && wire === WIRE_VARINT) vt.typeIdx = r.readVarint();
|
|
261
|
+
else if (fieldNum === 2 && wire === WIRE_VARINT)
|
|
262
|
+
vt.unitIdx = r.readVarint();
|
|
263
|
+
else r.skipField(wire);
|
|
264
|
+
}
|
|
265
|
+
return vt;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// ── Top-level decoder ────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
export const decodePprof = (raw: Uint8Array): PprofProfile => {
|
|
271
|
+
const r = new Reader(raw);
|
|
272
|
+
const profile: PprofProfile = {
|
|
273
|
+
sampleTypes: [],
|
|
274
|
+
samples: [],
|
|
275
|
+
locations: new Map(),
|
|
276
|
+
functions: new Map(),
|
|
277
|
+
stringTable: [],
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
while (!r.eof) {
|
|
281
|
+
const tag = r.readVarint();
|
|
282
|
+
const fieldNum = tag >>> 3;
|
|
283
|
+
const wire = tag & 7;
|
|
284
|
+
if (fieldNum === 1 && wire === WIRE_LENGTH_DELIMITED) {
|
|
285
|
+
const len = r.readVarint();
|
|
286
|
+
profile.sampleTypes.push(decodeValueType(r, len));
|
|
287
|
+
} else if (fieldNum === 2 && wire === WIRE_LENGTH_DELIMITED) {
|
|
288
|
+
const len = r.readVarint();
|
|
289
|
+
profile.samples.push(decodeSample(r, len));
|
|
290
|
+
} else if (fieldNum === 4 && wire === WIRE_LENGTH_DELIMITED) {
|
|
291
|
+
const len = r.readVarint();
|
|
292
|
+
const loc = decodeLocation(r, len);
|
|
293
|
+
profile.locations.set(loc.id, loc);
|
|
294
|
+
} else if (fieldNum === 5 && wire === WIRE_LENGTH_DELIMITED) {
|
|
295
|
+
const len = r.readVarint();
|
|
296
|
+
const fn = decodeFunction(r, len);
|
|
297
|
+
profile.functions.set(fn.id, fn);
|
|
298
|
+
} else if (fieldNum === 6 && wire === WIRE_LENGTH_DELIMITED) {
|
|
299
|
+
const len = r.readVarint();
|
|
300
|
+
profile.stringTable.push(r.readString(len));
|
|
301
|
+
} else {
|
|
302
|
+
r.skipField(wire);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return profile;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Decompress (if gzipped) + decode a raw pprof byte buffer. Workers and
|
|
311
|
+
* modern browsers both expose `DecompressionStream("gzip")` natively, so
|
|
312
|
+
* a single implementation serves both runtimes.
|
|
313
|
+
*/
|
|
314
|
+
export const decodePprofBlob = async (
|
|
315
|
+
body: ArrayBuffer | Uint8Array,
|
|
316
|
+
): Promise<PprofProfile> => {
|
|
317
|
+
const buf = body instanceof Uint8Array ? body : new Uint8Array(body);
|
|
318
|
+
let raw: Uint8Array;
|
|
319
|
+
// gzip magic byte sniff. Uploaders may pre-decompress, but the
|
|
320
|
+
// canonical pprof wire form is gzipped.
|
|
321
|
+
if (buf.length >= 2 && buf[0] === 0x1f && buf[1] === 0x8b) {
|
|
322
|
+
// Cast: Uint8Array is a valid BodyInit at runtime in both Workers
|
|
323
|
+
// and browsers; lib.dom types disagree.
|
|
324
|
+
const stream = new Response(buf as unknown as BodyInit).body?.pipeThrough(
|
|
325
|
+
new DecompressionStream("gzip"),
|
|
326
|
+
);
|
|
327
|
+
raw = new Uint8Array(await new Response(stream).arrayBuffer());
|
|
328
|
+
} else {
|
|
329
|
+
raw = buf;
|
|
330
|
+
}
|
|
331
|
+
return decodePprof(raw);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// ── Encoder (used by the server-side trace_id filter that re-emits a
|
|
335
|
+
// smaller blob; safe to expose from the shared package).
|
|
336
|
+
// Symmetric to the decoder above; only emits the fields we read.
|
|
337
|
+
|
|
338
|
+
interface ByteSink {
|
|
339
|
+
push(byte: number): void;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
class ByteBuilder implements ByteSink {
|
|
343
|
+
private buf = new Uint8Array(1024 * 16); // 16KB initial capacity
|
|
344
|
+
private len = 0;
|
|
345
|
+
|
|
346
|
+
get length(): number {
|
|
347
|
+
return this.len;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
push(byte: number): void {
|
|
351
|
+
if (this.len >= this.buf.length) {
|
|
352
|
+
const next = new Uint8Array(this.buf.length * 2);
|
|
353
|
+
next.set(this.buf);
|
|
354
|
+
this.buf = next;
|
|
355
|
+
}
|
|
356
|
+
this.buf[this.len++] = byte;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
[Symbol.iterator]() {
|
|
360
|
+
return this.buf.subarray(0, this.len)[Symbol.iterator]();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
toArray(): Uint8Array {
|
|
364
|
+
return this.buf.subarray(0, this.len);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const writeVarint = (out: ByteSink, value: number): void => {
|
|
369
|
+
while (value >= 0x80) {
|
|
370
|
+
out.push((value & 0x7f) | 0x80);
|
|
371
|
+
value = Math.floor(value / 128);
|
|
372
|
+
}
|
|
373
|
+
out.push(value & 0x7f);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const writeTag = (out: ByteSink, fieldNum: number, wireType: number): void =>
|
|
377
|
+
writeVarint(out, (fieldNum << 3) | wireType);
|
|
378
|
+
|
|
379
|
+
const writeLengthDelimited = (
|
|
380
|
+
out: ByteSink,
|
|
381
|
+
fieldNum: number,
|
|
382
|
+
bytes: number[] | Uint8Array,
|
|
383
|
+
): void => {
|
|
384
|
+
writeTag(out, fieldNum, WIRE_LENGTH_DELIMITED);
|
|
385
|
+
writeVarint(out, bytes.length);
|
|
386
|
+
for (let i = 0; i < bytes.length; i++) out.push(bytes[i]);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const writeString = (out: ByteSink, fieldNum: number, str: string): void => {
|
|
390
|
+
const bytes = new TextEncoder().encode(str);
|
|
391
|
+
writeLengthDelimited(out, fieldNum, bytes);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const writePackedVarints = (
|
|
395
|
+
out: ByteSink,
|
|
396
|
+
fieldNum: number,
|
|
397
|
+
values: number[],
|
|
398
|
+
): void => {
|
|
399
|
+
if (values.length === 0) return;
|
|
400
|
+
const inner: number[] = [];
|
|
401
|
+
for (const v of values) writeVarint(inner, v);
|
|
402
|
+
writeLengthDelimited(out, fieldNum, inner);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const encodeLabel = (label: PprofLabel): number[] => {
|
|
406
|
+
const out: number[] = [];
|
|
407
|
+
if (label.keyIdx) {
|
|
408
|
+
writeTag(out, 1, WIRE_VARINT);
|
|
409
|
+
writeVarint(out, label.keyIdx);
|
|
410
|
+
}
|
|
411
|
+
if (label.strIdx) {
|
|
412
|
+
writeTag(out, 2, WIRE_VARINT);
|
|
413
|
+
writeVarint(out, label.strIdx);
|
|
414
|
+
}
|
|
415
|
+
if (label.num) {
|
|
416
|
+
writeTag(out, 3, WIRE_VARINT);
|
|
417
|
+
writeVarint(out, label.num);
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const encodeSample = (sample: PprofSample): number[] => {
|
|
423
|
+
const out: number[] = [];
|
|
424
|
+
writePackedVarints(out, 1, sample.locationIds);
|
|
425
|
+
writePackedVarints(out, 2, sample.values);
|
|
426
|
+
for (const label of sample.labels) {
|
|
427
|
+
writeLengthDelimited(out, 3, encodeLabel(label));
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const encodeValueType = (vt: PprofValueType): number[] => {
|
|
433
|
+
const out: number[] = [];
|
|
434
|
+
if (vt.typeIdx) {
|
|
435
|
+
writeTag(out, 1, WIRE_VARINT);
|
|
436
|
+
writeVarint(out, vt.typeIdx);
|
|
437
|
+
}
|
|
438
|
+
if (vt.unitIdx) {
|
|
439
|
+
writeTag(out, 2, WIRE_VARINT);
|
|
440
|
+
writeVarint(out, vt.unitIdx);
|
|
441
|
+
}
|
|
442
|
+
return out;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const encodeLocation = (loc: PprofLocation): number[] => {
|
|
446
|
+
const out: number[] = [];
|
|
447
|
+
if (loc.id) {
|
|
448
|
+
writeTag(out, 1, WIRE_VARINT);
|
|
449
|
+
writeVarint(out, loc.id);
|
|
450
|
+
}
|
|
451
|
+
const lines =
|
|
452
|
+
loc.lines ||
|
|
453
|
+
(loc.functionIds || []).map((fid) => ({ functionId: fid, line: 0 }));
|
|
454
|
+
for (const line of lines) {
|
|
455
|
+
const lineBytes: number[] = [];
|
|
456
|
+
if (line.functionId) {
|
|
457
|
+
writeTag(lineBytes, 1, WIRE_VARINT);
|
|
458
|
+
writeVarint(lineBytes, line.functionId);
|
|
459
|
+
}
|
|
460
|
+
if (line.line) {
|
|
461
|
+
writeTag(lineBytes, 2, WIRE_VARINT);
|
|
462
|
+
writeVarint(lineBytes, line.line);
|
|
463
|
+
}
|
|
464
|
+
writeLengthDelimited(out, 4, lineBytes);
|
|
465
|
+
}
|
|
466
|
+
return out;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const encodeFunction = (fn: PprofFunction): number[] => {
|
|
470
|
+
const out: number[] = [];
|
|
471
|
+
if (fn.id) {
|
|
472
|
+
writeTag(out, 1, WIRE_VARINT);
|
|
473
|
+
writeVarint(out, fn.id);
|
|
474
|
+
}
|
|
475
|
+
if (fn.nameIdx) {
|
|
476
|
+
writeTag(out, 2, WIRE_VARINT);
|
|
477
|
+
writeVarint(out, fn.nameIdx);
|
|
478
|
+
}
|
|
479
|
+
if (fn.filenameIdx) {
|
|
480
|
+
writeTag(out, 4, WIRE_VARINT);
|
|
481
|
+
writeVarint(out, fn.filenameIdx);
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
export const encodePprof = (profile: PprofProfile): Uint8Array => {
|
|
487
|
+
const out = new ByteBuilder();
|
|
488
|
+
for (const vt of profile.sampleTypes) {
|
|
489
|
+
writeLengthDelimited(out, 1, encodeValueType(vt));
|
|
490
|
+
}
|
|
491
|
+
for (const sample of profile.samples) {
|
|
492
|
+
writeLengthDelimited(out, 2, encodeSample(sample));
|
|
493
|
+
}
|
|
494
|
+
for (const loc of profile.locations.values()) {
|
|
495
|
+
writeLengthDelimited(out, 4, encodeLocation(loc));
|
|
496
|
+
}
|
|
497
|
+
for (const fn of profile.functions.values()) {
|
|
498
|
+
writeLengthDelimited(out, 5, encodeFunction(fn));
|
|
499
|
+
}
|
|
500
|
+
for (const str of profile.stringTable) {
|
|
501
|
+
writeString(out, 6, str);
|
|
502
|
+
}
|
|
503
|
+
return out.toArray();
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
/** Convenience: gzip a Uint8Array using CompressionStream. */
|
|
507
|
+
export const gzipBytes = async (bytes: Uint8Array): Promise<Uint8Array> => {
|
|
508
|
+
const stream = new Response(bytes as unknown as BodyInit).body?.pipeThrough(
|
|
509
|
+
new CompressionStream("gzip"),
|
|
510
|
+
);
|
|
511
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
512
|
+
};
|