| // Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| /// This library file contains data structures and helper methods for parsing |
| /// `perf.data` files produced by `perf` tool on Linux. |
| /// |
| /// Format of this file is documented in: |
| /// |
| /// * https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/tools/perf/Documentation/perf.data-file-format.txt |
| /// * https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/tools/perf/util/header.h |
| /// * https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h |
| /// |
| library; |
| |
| import 'dart:ffi'; |
| import 'dart:io'; |
| import 'dart:math' as math; |
| import 'dart:typed_data'; |
| |
| /// `struct perf_header`: header of the `perf.data` file. |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/tools/perf/util/header.h#L64 |
| final class Header extends Struct { |
| @Array(8) |
| external Array<Uint8> magic; |
| |
| @Uint64() |
| external int size; |
| |
| @Uint64() |
| external int attrSize; |
| |
| external FileSection attrs; |
| external FileSection data; |
| external FileSection eventTypes; |
| |
| @Uint64() |
| external int flags; |
| |
| @Array(3) |
| external Array<Uint64> flags1; |
| } |
| |
| /// `struct perf_file_section`: section inside `perf.data` file. |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/tools/perf/util/header.h#L59 |
| final class FileSection extends Struct { |
| @Uint64() |
| external int offset; |
| |
| @Uint64() |
| external int size; |
| |
| @override |
| String toString() { |
| return 'PerfFileSection{offset=$offset,size=$size}'; |
| } |
| } |
| |
| /// Optional sections inside `perf.data` file. |
| /// |
| /// The section is present iff corresponding bit in [PerfHeader.flags] is set. |
| /// |
| /// `PerfFileSection` descriptors for present sections will follow in sequence |
| /// immediately after the data section (i.e. the first `PerfFileSection` will |
| /// be located at `header.data.offset + header.data.size` offset). |
| /// |
| /// See [PerfData.readOptionalSectionHeaders]. |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/tools/perf/util/header.h#L15 |
| enum OptionalSection { |
| reserved, |
| tracingData, |
| buildId, |
| hostname, |
| osRelease, |
| version, |
| arch, |
| nrCpus, |
| cpuDesc, |
| cpuId, |
| totalMem, |
| cmdLine, |
| eventDesc, |
| cpuTopology, |
| numaTopology, |
| branchStack, |
| groupDesc, |
| auxTrace, |
| stat, |
| cache, |
| sampleTime, |
| sampleTopology, |
| clockId, |
| dirFormat, |
| bpfProgInfo, |
| bpfBtf, |
| compressed, |
| cpuPmuCaps, |
| clockData, |
| hybridTopology, |
| pmuCaps |
| } |
| |
| /// `perf_event_header`: common header of all event entries. |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L815 |
| final class EventHeader extends Struct { |
| @Uint32() |
| external int type; |
| |
| @Uint16() |
| external int misc; |
| |
| @Uint16() |
| external int size; |
| |
| @override |
| String toString() => 'PerfEventHeader{type=$type,misc=$misc,size=$size}'; |
| } |
| |
| /// `perf_event_attr`: configuration of the event monitored by `perf`. |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L389 |
| final class EventAttr extends Struct { |
| /// Major type: hardware/software/tracepoint/etc. |
| /// |
| /// See [EventType]. |
| @Uint32() |
| external int type; |
| |
| @Uint32() |
| external int size; |
| |
| /// Type specific configuration information. |
| @Uint64() |
| external int config; |
| |
| @Uint64() |
| external int samplePeriodOrFreq; |
| |
| @Uint64() |
| external int sampleType; |
| |
| @Uint64() |
| external int readFormat; |
| |
| /// Various bit fields which we currently don't care about. |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L414 |
| @Uint64() |
| external int flags; |
| |
| @Uint32() |
| external int wakeupEventOrWatermark; |
| @Uint32() |
| external int bpType; |
| |
| /// Union of `bp_addr`/`kprobe_func`/`uprobe_path`/`config1` |
| @Uint64() |
| external int config1; |
| |
| /// Union of `bp_len`/`kprobe_addr`/`probe_offset`/`config2` |
| @Uint64() |
| external int config2; |
| |
| /// One of `enum perf_branch_sample_type` |
| @Uint64() |
| external int branchSampleType; |
| |
| /// Defines set of user regs to dump on samples. |
| /// See asm/perf_regs.h for details. |
| @Uint64() |
| external int sampleRegsUser; |
| |
| /// Defines size of the user stack to dump on samples. |
| @Uint32() |
| external int sampleStackUser; |
| |
| @Int32() |
| external int clockid; |
| |
| /// Defines set of regs to dump for each sample |
| /// state captured on: |
| /// - precise = 0: PMU interrupt |
| /// - precise > 0: sampled instruction |
| /// |
| /// See asm/perf_regs.h for details. |
| @Uint64() |
| external int sampleRegsIntr; |
| |
| /// Wakeup watermark for AUX area |
| @Uint32() |
| external int auxWatermark; |
| @Uint16() |
| external int sampleMaxStack; |
| @Uint16() |
| external int reserved2; |
| @Uint32() |
| external int auxSampleSize; |
| @Uint32() |
| external int reserved3; |
| |
| /// User provided data if sigtrap=1, passed back to user via |
| /// siginfo_t::si_perf_data, e.g. to permit user to identify the event. |
| /// Note, siginfo_t::si_perf_data is long-sized, and sig_data will be |
| /// truncated accordingly on 32 bit architectures. |
| @Uint64() |
| external int sigData; |
| |
| /// Extension of config2 |
| @Uint64() |
| external int config3; |
| } |
| |
| /// `enum perf_event_type`: type of the recorded event. |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L838 |
| extension type const EventType(int _) implements int { |
| /// `PERF_RECORD_MMAP` |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L879 |
| static const mmap = EventType(1); |
| |
| /// `PERF_RECORD_SAMPLE` |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L947 |
| static const sample = EventType(9); |
| |
| /// `PERF_RECORD_MMAP2` |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L1035 |
| static const mmap2 = EventType(10); |
| } |
| |
| /// `enum perf_type_id` |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L29 |
| extension type const TypeId(int index) implements int { |
| static const hardware = TypeId(0); |
| static const software = TypeId(1); |
| static const tracepoint = TypeId(2); |
| static const hwCache = TypeId(3); |
| static const raw = TypeId(4); |
| static const breakpoint = TypeId(5); |
| } |
| |
| /// `enum perf_event_sample_format`: additional information recorded for sample. |
| /// |
| /// Bits that can be set in [PerfEventAttr.sampleType] to request information |
| /// in the overflow packets. |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L139 |
| extension type const SampleFormat(int bit) implements int { |
| /// `PERF_SAMPLE_IP` |
| static const ip = SampleFormat(1 << 0); |
| |
| /// `PERF_SAMPLE_TID` |
| static const tid = SampleFormat(1 << 1); |
| |
| /// `PERF_SAMPLE_TIME` |
| static const time = SampleFormat(1 << 2); |
| |
| /// `PERF_SAMPLE_ADDR` |
| static const addr = SampleFormat(1 << 3); |
| |
| /// `PERF_SAMPLE_READ` |
| static const read = SampleFormat(1 << 4); |
| |
| /// `PERF_SAMPLE_CALLCHAIN` |
| static const callchain = SampleFormat(1 << 5); |
| |
| /// `PERF_SAMPLE_ID` |
| static const id = SampleFormat(1 << 6); |
| |
| /// `PERF_SAMPLE_CPU` |
| static const cpu = SampleFormat(1 << 7); |
| |
| /// `PERF_SAMPLE_PERIOD` |
| static const period = SampleFormat(1 << 8); |
| |
| /// `PERF_SAMPLE_STREAM_ID` |
| static const streamId = SampleFormat(1 << 9); |
| |
| /// `PERF_SAMPLE_RAW` |
| static const raw = SampleFormat(1 << 10); |
| |
| /// `PERF_SAMPLE_BRANCH_STACK` |
| static const branchStack = SampleFormat(1 << 11); |
| |
| /// `PERF_SAMPLE_REGS_USER` |
| static const regsUser = SampleFormat(1 << 12); |
| |
| /// `PERF_SAMPLE_STACK_USER` |
| static const stackUser = SampleFormat(1 << 13); |
| |
| /// `PERF_SAMPLE_WEIGHT` |
| static const weight = SampleFormat(1 << 14); |
| |
| /// `PERF_SAMPLE_DATA_SRC` |
| static const dataSrc = SampleFormat(1 << 15); |
| |
| /// `PERF_SAMPLE_IDENTIFIER` |
| static const identifier = SampleFormat(1 << 16); |
| |
| /// `PERF_SAMPLE_TRANSACTION` |
| static const transaction = SampleFormat(1 << 17); |
| |
| /// `PERF_SAMPLE_REGS_INTR` |
| static const regsIntr = SampleFormat(1 << 18); |
| |
| /// `PERF_SAMPLE_PHYS_ADDR` |
| static const physAddr = SampleFormat(1 << 19); |
| |
| /// `PERF_SAMPLE_AUX` |
| static const aux = SampleFormat(1 << 20); |
| |
| /// `PERF_SAMPLE_CGROUP` |
| static const cgroup = SampleFormat(1 << 21); |
| |
| /// `PERF_SAMPLE_DATA_PAGE_SIZE` |
| static const dataPageSize = SampleFormat(1 << 22); |
| |
| /// `PERF_SAMPLE_CODE_PAGE_SIZE` |
| static const codePageSize = SampleFormat(1 << 23); |
| |
| /// `PERF_SAMPLE_WEIGHT_STRUCT` |
| static const weightStruct = SampleFormat(1 << 24); |
| |
| static const bitNames = { |
| ip: "ip", |
| tid: "tid", |
| time: "time", |
| addr: "addr", |
| read: "read", |
| callchain: "callchain", |
| id: "id", |
| cpu: "cpu", |
| period: "period", |
| streamId: "streamId", |
| raw: "raw", |
| branchStack: "branchStack", |
| regsUser: "regsUser", |
| stackUser: "stackUser", |
| weight: "weight", |
| dataSrc: "dataSrc", |
| identifier: "identifier", |
| transaction: "transaction", |
| regsIntr: "regsIntr", |
| physAddr: "physAddr", |
| aux: "aux", |
| cgroup: "cgroup", |
| dataPageSize: "dataPageSize", |
| codePageSize: "codePageSize", |
| weightStruct: "weightStruct", |
| }; |
| |
| static String format(int mask) { |
| return SampleFormat.bitNames.entries |
| .where((e) => (mask & e.key) != 0) |
| .map((e) => e.value) |
| .join('|'); |
| } |
| } |
| |
| /// `PERF_RECORD_MMAP` |
| /// |
| /// The `MMAP` events record the `PROT_EXEC` mappings so that we can |
| /// correlate userspace `IP`s to code. |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L879 |
| final class MmapEvent extends Struct { |
| external EventHeader header; |
| |
| @Uint32() |
| external int pid; |
| |
| @Uint32() |
| external int tid; |
| |
| @Uint64() |
| external int addr; |
| |
| @Uint64() |
| external int len; |
| |
| @Uint64() |
| external int pgoffs; |
| |
| @Array.variable() |
| external Array<Uint8> filename; |
| |
| @override |
| String toString() => 'MmapEvent{addr=${addr.formatAsAddress()},' |
| 'len=$len,pgoffs=$pgoffs,' |
| 'filename=${filename.toStringFromZeroTerminated()}}'; |
| } |
| |
| final class BuildId extends Struct { |
| @Uint8() |
| external int size; |
| |
| @Uint8() |
| external int reserved1; |
| |
| @Uint16() |
| external int reserved2; |
| |
| @Array(20) |
| external Array<Uint8> buildId; |
| } |
| |
| final class Ino extends Struct { |
| @Uint32() |
| external int maj; |
| |
| @Uint32() |
| external int min; |
| |
| @Uint64() |
| external int ino; |
| |
| @Uint64() |
| external int inoGeneration; |
| } |
| |
| final class BuildIdOrIno extends Union { |
| external BuildId buildId; |
| external Ino ino; |
| } |
| |
| /// `PERF_RECORD_MMAP2` |
| /// |
| /// The `MMAP2` records are an augmented version of `MMAP` (see [MapEvent]), |
| /// they add `maj`, `min`, `ino` numbers to be used to uniquely identify each |
| /// mapping. |
| /// |
| /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L1035 |
| final class Mmap2Event extends Struct { |
| external EventHeader header; |
| |
| @Uint32() |
| external int pid; |
| |
| @Uint32() |
| external int tid; |
| |
| @Uint64() |
| external int addr; |
| |
| @Uint64() |
| external int len; |
| |
| @Uint64() |
| external int pgoffs; |
| |
| external BuildIdOrIno buildIdOrIno; |
| |
| @Uint32() |
| external int prot; |
| |
| @Uint32() |
| external int flags; |
| |
| @Array.variable() |
| external Array<Uint8> filename; |
| |
| @override |
| String toString() => 'Mmap2Event{addr=${addr.formatAsAddress()},' |
| 'len=$len,pgoffs=$pgoffs,' |
| 'filename=${filename.toStringFromZeroTerminated()}}'; |
| } |
| |
| extension ArrayToString on Array<Uint8> { |
| String toStringFromFixedLength(int length) => |
| String.fromCharCodes([for (var i = 0; i < length; i++) this[i]]); |
| |
| String toStringFromZeroTerminated() { |
| final sb = StringBuffer(); |
| for (var i = 0; this[i] != 0; i++) { |
| sb.writeCharCode(this[i]); |
| } |
| return sb.toString(); |
| } |
| } |
| |
| extension FormatAsAddress on int { |
| String formatAsAddress() => toRadixString(16); |
| } |
| |
| const int kb = 1024; |
| const int mb = 1024 * kb; |
| |
| final class StreamingSectionReader { |
| final RandomAccessFile f; |
| final FileSection section; |
| |
| final chunk = Uint8List(256 * mb); |
| |
| /// Number of bytes available in the chunk. |
| int chunkBytes = 0; |
| |
| /// Offset from the start of the section to the start of the chunk. |
| int chunkOffset = 0; |
| |
| /// Position within the chunk. |
| int pos = 0; |
| |
| StreamingSectionReader(this.f, this.section) { |
| f.setPositionSync(section.offset); |
| refill(); |
| } |
| |
| bool ensure(int bytes) { |
| if (chunkBytes < (pos + bytes)) { |
| refill(); |
| } |
| return chunkBytes >= (pos + bytes); |
| } |
| |
| void refill() { |
| final int leftOverBytes = chunkBytes - pos; |
| for (int i = 0; i < leftOverBytes; i++) { |
| chunk[i] = chunk[i + pos]; |
| } |
| chunkOffset += pos; |
| pos = 0; |
| |
| // Are there any more bytes left to read? |
| if (chunkOffset >= section.size) { |
| chunkBytes = 0; |
| return; |
| } |
| |
| print( |
| "processed $chunkOffset bytes of ${section.size} total (${(chunkOffset / section.size * 100).floor()} %)"); |
| |
| final bytesAlreadyRead = chunkOffset + leftOverBytes; |
| final bytesToRead = |
| math.min(section.size - bytesAlreadyRead, chunk.length - leftOverBytes); |
| final bytesRead = |
| f.readIntoSync(chunk, leftOverBytes, leftOverBytes + bytesToRead); |
| chunkBytes = bytesRead + leftOverBytes; |
| } |
| } |
| |
| final class PerfData { |
| final RandomAccessFile f; |
| |
| final Header header; |
| |
| PerfData(this.f) |
| : header = Struct.create<Header>(f.readSync(sizeOf<Header>())) { |
| final magic = header.magic.toStringFromFixedLength(8); |
| if (magic != 'PERFILE2') { |
| reportError('Incorrect magic in ${f.path} - $magic'); |
| } |
| } |
| |
| List<EventAttr> readAttrs() { |
| f.setPositionSync(header.attrs.offset); |
| final attrs = f.readSync(header.attrs.size); |
| |
| final result = <EventAttr>[]; |
| int pos = 0; |
| while (pos + sizeOf<EventAttr>() < attrs.length) { |
| final attr = Struct.create<EventAttr>(attrs, pos); |
| result.add(attr); |
| pos += attr.size; |
| } |
| return result; |
| } |
| |
| Map<OptionalSection, FileSection> readOptionalSectionHeaders() { |
| f.setPositionSync(header.data.offset + header.data.size); |
| final optionalHeaders = |
| f.readSync(sizeOf<FileSection>() * OptionalSection.values.length); |
| |
| int headerIndex = 0; |
| return { |
| for (final flag in OptionalSection.values) |
| if (header.flags & (1 << flag.index) != 0) |
| flag: Struct.create<FileSection>( |
| optionalHeaders, sizeOf<FileSection>() * headerIndex++), |
| }; |
| } |
| |
| late final _dataReader = StreamingSectionReader(f, header.data); |
| |
| @pragma('vm:prefer-inline') |
| void readEvents(bool Function(int type, Uint8List chunk, int pos) callback) { |
| final reader = _dataReader; |
| |
| while (true) { |
| if (!reader.ensure(sizeOf<EventHeader>())) { |
| // No more events. |
| return; |
| } |
| |
| // Note: `reader.ensure` might refill the chunk and invalidate |
| // created struct so extract values eagerly. |
| final EventHeader(:type, :size) = |
| Struct.create<EventHeader>(reader.chunk, reader.pos); |
| if (!reader.ensure(size)) { |
| return; |
| } |
| |
| // At this point we are guaranteed to have the whole event in the chunk |
| // starting at `reader.pos`. |
| if (!callback(type, reader.chunk, reader.pos)) { |
| reader.pos += size; |
| return; |
| } |
| reader.pos += size; |
| } |
| } |
| |
| Never reportError(String message) => throw ParseError(f.path, message); |
| } |
| |
| final class ParseError extends Error { |
| final String file; |
| final String message; |
| |
| ParseError(this.file, this.message); |
| |
| @override |
| String toString() => 'Failed to parse $file: $message'; |
| } |