this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 451 lines 15 kB view raw
1const std = @import("std"); 2const mem = std.mem; 3 4const log = std.log.scoped(.austin); 5 6pub const Process = struct { 7 /// Process ID 8 pid: usize, 9 /// Sub-interpreter ID, 10 iid: usize, 11 /// Thread ID 12 tid: usize, 13}; 14 15pub const ProcessID = usize; 16 17pub const Frame = struct { 18 module: []const u8 = undefined, 19 function: []const u8 = undefined, 20 line_number: isize = 0, 21}; 22 23pub const FrameID = usize; 24 25pub const FrameWrapper = union(enum) { 26 frame: FrameID, 27 invalid: void, 28 gc: void, 29}; 30 31pub const Metric = struct { 32 time_delta: usize, 33 /// 1 / true for idle, 0 / false otherwise 34 idle_state: bool, 35 /// positive for memory allocations, negative for deallocations 36 rss_memory_delta: isize, 37}; 38 39pub const Sample = struct { 40 process: ProcessID, 41 frames: []FrameWrapper, 42 metric: Metric, 43}; 44 45pub const PartialSample = struct { 46 process: ProcessID, 47 frames: []FrameWrapper, 48}; 49 50pub const SampleWrapper = union(enum) { 51 full: Sample, 52 partial: PartialSample, 53 metric: Metric, 54}; 55 56pub const Metadata = struct { 57 austin: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 }, 58 interval: usize = 0, 59 mode: enum { full, unknown } = .unknown, // at the moment only full is supported, not memory 60 memory: usize = 0, 61 multiprocess: bool = false, 62 duration: usize = 0, 63 gc: ?usize = null, 64 65 // Add metadata about which fields signal end of parsing 66 const EndFields = std.StaticStringMap(void).initComptime(.{ 67 .{"duration"}, .{"gc"}, 68 }); 69 70 pub fn parseField(self: *Metadata, field_name: []const u8, value: []const u8) !bool { 71 // Returns true if this field signals end of parsing 72 73 // Use comptime reflection to generate parsing for each field 74 inline for (std.meta.fields(Metadata)) |field| { 75 if (std.mem.eql(u8, field_name, field.name)) { 76 const field_ptr = &@field(self, field.name); 77 78 switch (field.type) { 79 std.SemanticVersion => { 80 field_ptr.* = try std.SemanticVersion.parse(value); 81 }, 82 usize, ?usize => { 83 field_ptr.* = try std.fmt.parseUnsigned(usize, value, 0); 84 }, 85 bool => { 86 field_ptr.* = std.mem.eql(u8, "on", value); 87 }, 88 @TypeOf(self.mode) => { 89 field_ptr.* = std.meta.stringToEnum(@TypeOf(self.mode), value) orelse return error.UnknownMetadataFieldValue; 90 }, 91 else => @compileError("Unsupported metadata field type: " ++ @typeName(field.type)), 92 } 93 94 return EndFields.has(field.name); 95 } 96 } 97 98 return error.UnknownMetadataField; 99 } 100}; 101 102pub const Profile = struct { 103 meta: Metadata = .{}, 104 frames: []Frame = undefined, 105 samples: []SampleWrapper = undefined, 106 processes: []Process = undefined, 107 108 arena: mem.Allocator, 109 110 pub fn deinit(self: *Profile) void { 111 for (self.frames) |frame| { 112 self.arena.free(frame.module); 113 self.arena.free(frame.function); 114 } 115 116 for (self.samples) |sample| { 117 switch (sample) { 118 .metric => {}, 119 inline else => |s| { 120 self.arena.free(s.frames); 121 }, 122 } 123 } 124 125 self.arena.free(self.frames); 126 self.arena.free(self.samples); 127 self.arena.free(self.processes); 128 } 129 130 pub fn getFrame(self: *const Profile, id: FrameID) ?Frame { 131 if (id < self.frames.len) { 132 return self.frames[id]; 133 } 134 return null; 135 } 136}; 137 138pub const Parser = struct { 139 const ParseError = error{ 140 InvalidSample, 141 NoPID, 142 InvalidPID, 143 NoThreadInfo, 144 NoIID, 145 InvalidIID, 146 NoTID, 147 InvalidTID, 148 NoModule, 149 NoFunction, 150 NoLineNumber, 151 InvalidLineNumber, 152 NoTimeDelta, 153 NoIdleState, 154 NoRSSMemoryDelta, 155 }; 156 157 arena: mem.Allocator, 158 profile: Profile, 159 frames: std.ArrayList(Frame), 160 samples: std.ArrayList(SampleWrapper), 161 processes: std.ArrayList(Process), 162 163 pub fn init(arena: mem.Allocator) !Parser { 164 return .{ 165 .arena = arena, 166 .profile = .{ .arena = arena }, 167 .frames = try .initCapacity(arena, 100), 168 .samples = try .initCapacity(arena, 1_000), 169 .processes = try .initCapacity(arena, 1), 170 }; 171 } 172 173 pub fn parse(self: *Parser, raw_reader: anytype, progress: *std.Progress.Node) !Profile { 174 defer self.samples.deinit(); 175 defer self.frames.deinit(); 176 defer self.processes.deinit(); 177 178 var buffered_reader = std.io.bufferedReader(raw_reader); 179 var reader = buffered_reader.reader(); 180 181 var reached_end: bool = false; 182 183 var line: std.ArrayList(u8) = .init(self.arena); 184 defer line.deinit(); 185 186 var line_num: usize = 1; 187 while (true) : (line_num += 1) { 188 defer line.clearAndFree(); 189 defer progress.completeOne(); 190 191 try reader.streamUntilDelimiter(line.writer(), '\n', null); 192 193 if (line.items.len == 0) { 194 if (reached_end) break; 195 continue; 196 } 197 198 if (line.items[0] == '#') { 199 // Metadata 200 const input = mem.trimStart(u8, line.items, "# "); 201 var it = mem.tokenizeScalar(u8, input, ':'); 202 203 const key = it.next() orelse return error.MissingKey; 204 const value = mem.trimStart(u8, it.next() orelse return error.MissingValue, " "); 205 206 reached_end = self.profile.meta.parseField(key, value) catch |err| switch (err) { 207 error.UnknownMetadataField => { 208 log.warn("Unknown metadata field: {s}", .{key}); 209 continue; 210 }, 211 else => return err, 212 }; 213 } else { 214 // Sample 215 const sample = self.parseSample(line, line_num) catch |err| { 216 log.warn("Caught {} while parsing line {}:\n\x1b[90m{} |\x1b[0m {s}{s}", .{ 217 err, 218 line_num, 219 line_num, 220 if (line.items.len <= 80) line.items else line.items[0..80], 221 if (line.items.len <= 80) "" else " ...", 222 }); 223 if (err == ParseError.InvalidSample) continue; 224 225 std.process.exit(1); 226 }; 227 228 try self.samples.append(sample); 229 } 230 } 231 232 self.profile.frames = try self.frames.toOwnedSlice(); 233 self.profile.samples = try self.samples.toOwnedSlice(); 234 self.profile.processes = try self.processes.toOwnedSlice(); 235 return self.profile; 236 } 237 238 const ParseSampleError = ParseError || mem.Allocator.Error || std.fmt.ParseIntError; 239 240 fn parseSample(self: *Parser, line: std.ArrayList(u8), line_num: usize) ParseSampleError!SampleWrapper { 241 var sample = Sample{ 242 .frames = undefined, 243 .metric = undefined, 244 .process = undefined, 245 }; 246 247 var line_it_back = mem.splitBackwardsScalar(u8, line.items, ' '); 248 const metric_raw = line_it_back.next().?; 249 250 const data_raw = line.items[0 .. line.items.len - metric_raw.len - 1]; 251 252 if (!mem.containsAtLeast(u8, metric_raw, 2, ",")) { 253 // defer self.arena.destroy(sample); 254 255 self.parseFullSample(&sample, data_raw, line_num) catch |err| { 256 switch (err) { 257 ParseError.NoPID, 258 ParseError.InvalidPID, 259 ParseError.NoThreadInfo, 260 ParseError.NoIID, 261 ParseError.InvalidIID, 262 ParseError.NoTID, 263 ParseError.InvalidTID, 264 => return ParseError.InvalidSample, 265 else => {}, 266 } 267 }; 268 269 log.warn( 270 "No metrics found while parsing line {}, returning partial sample:\n\x1b[90m{} |\x1b[0m {s}{s}", 271 .{ 272 line_num, 273 line_num, 274 if (line.items.len <= 80) line.items else line.items[0..80], 275 if (line.items.len <= 80) "" else " ...", 276 }, 277 ); 278 return SampleWrapper{ .partial = .{ 279 .process = sample.process, 280 .frames = sample.frames, 281 } }; 282 } 283 284 // Metric 285 var metric = mem.tokenizeScalar(u8, metric_raw, ','); 286 sample.metric = Metric{ 287 .time_delta = try std.fmt.parseUnsigned( 288 usize, 289 metric.next() orelse return ParseError.NoTimeDelta, 290 0, 291 ), 292 .idle_state = try std.fmt.parseUnsigned( 293 u8, 294 metric.next() orelse return ParseError.NoIdleState, 295 0, 296 ) == 1, 297 .rss_memory_delta = try std.fmt.parseInt( 298 isize, 299 metric.next() orelse return ParseError.NoRSSMemoryDelta, 300 0, 301 ), 302 }; 303 304 self.parseFullSample(&sample, data_raw, line_num) catch |err| { 305 // defer self.arena.destroy(sample); 306 307 log.debug( 308 "Caught {} while parsing line {}, returning metrics " ++ 309 "(time delta: {}, idle state: {}, rss memory delta: {}):\n" ++ 310 "\x1b[90m{} |\x1b[0m {s}{s}", 311 .{ 312 err, 313 line_num, 314 sample.metric.time_delta, 315 sample.metric.idle_state, 316 sample.metric.rss_memory_delta, 317 line_num, 318 if (line.items.len <= 80) line.items else line.items[0..80], 319 if (line.items.len <= 80) "" else " ...", 320 }, 321 ); 322 return SampleWrapper{ .metric = sample.metric }; 323 }; 324 325 return SampleWrapper{ .full = sample }; 326 } 327 328 fn parseFullSample(self: *Parser, sample: *Sample, data_raw: []const u8, line_num: usize) !void { 329 var data = std.mem.tokenizeScalar(u8, data_raw, ';'); 330 331 // PID 332 const pid = data.next() orelse return ParseError.NoPID; 333 334 const thread_raw = data.next() orelse return ParseError.NoThreadInfo; 335 var thread = std.mem.tokenizeScalar(u8, thread_raw, ':'); 336 337 // IID 338 const iid = thread.next() orelse return ParseError.NoIID; 339 340 // TID 341 const tid = thread.next() orelse return ParseError.NoTID; 342 343 sample.process = try self.storeProcess(.{ 344 .pid = std.fmt.parseUnsigned(usize, pid[1..], 0) catch return ParseError.InvalidPID, 345 .iid = std.fmt.parseUnsigned(usize, iid[1..], 0) catch return ParseError.InvalidIID, 346 .tid = std.fmt.parseUnsigned(usize, tid, 0) catch return ParseError.InvalidTID, 347 }); 348 349 // Frames 350 var frames: std.ArrayList(FrameWrapper) = try .initCapacity(self.arena, 1); 351 352 while (data.next()) |frame_raw| { 353 const frame = self.parseFrame(frame_raw) catch |err| { 354 switch (err) { 355 ParseError.NoModule, 356 ParseError.InvalidLineNumber, 357 ParseError.NoFunction, 358 ParseError.NoLineNumber, 359 => { 360 log.debug( 361 "Caught invalid frame while parsing line {}:\n\x1b[90m{} |\x1b[0m {s}{s}", 362 .{ 363 line_num, 364 line_num, 365 if (frame_raw.len <= 80) frame_raw else frame_raw[0..80], 366 if (frame_raw.len <= 80) "" else " ...", 367 }, 368 ); 369 try frames.append(FrameWrapper{ .invalid = {} }); 370 continue; 371 }, 372 else => return err, 373 } 374 }; 375 try frames.append(frame); 376 } 377 378 sample.frames = try frames.toOwnedSlice(); 379 } 380 381 fn parseFrame(self: *Parser, frame_raw: []const u8) !FrameWrapper { 382 var frame = std.mem.tokenizeScalar(u8, frame_raw, ':'); 383 const module = frame.next() orelse return ParseError.NoModule; 384 385 if (mem.startsWith(u8, frame_raw, "::")) { 386 return try self.storeFrame( 387 "", 388 "", 389 std.fmt.parseInt(isize, module, 0) catch 390 return ParseError.InvalidLineNumber, 391 ); 392 } 393 394 if (mem.eql(u8, module, "GC")) 395 return FrameWrapper{ .gc = {} }; 396 397 if (mem.eql(u8, module, "INVALID")) 398 return FrameWrapper{ .invalid = {} }; 399 400 const function = frame.next() orelse return ParseError.NoFunction; 401 402 const line_number = std.fmt.parseInt( 403 isize, 404 frame.next() orelse return ParseError.NoLineNumber, 405 0, 406 ) catch return ParseError.InvalidLineNumber; 407 408 return try self.storeFrame(module, function, line_number); 409 } 410 411 fn storeProcess( 412 self: *Parser, 413 process: Process, 414 ) !ProcessID { 415 var count: usize = 0; 416 for (self.processes.items, 0..) |*p, id| { 417 if (p.pid == process.pid and p.iid == process.iid and p.tid == process.tid) { 418 return id; 419 } 420 count += 1; 421 } 422 423 try self.processes.append(process); 424 return count + 1; 425 } 426 427 fn storeFrame( 428 self: *Parser, 429 module: []const u8, 430 function: []const u8, 431 line_number: isize, 432 ) !FrameWrapper { 433 var count: usize = 0; 434 for (self.frames.items, 0..) |*frame, id| { 435 if (mem.eql(u8, frame.module, module) and 436 mem.eql(u8, frame.function, function) and 437 frame.line_number == line_number) 438 { 439 return FrameWrapper{ .frame = id }; 440 } 441 count += 1; 442 } 443 444 var f: Frame = .{ .line_number = line_number }; 445 f.module = try self.arena.dupe(u8, module); 446 f.function = try self.arena.dupe(u8, function); 447 448 try self.frames.append(f); 449 return FrameWrapper{ .frame = count + 1 }; 450 } 451};