blob: 4ef3c4b06c505444f6e958f89656d0775e0d8d49 [file] [edit]
/* ----------------------------------------------------------------------------
Copyright (c) 2018-2026, Microsoft Research, Daan Leijen
This is free software; you can redistribute it and/or modify it under the
terms of the MIT license. A copy of the license can be found in the file
"LICENSE" at the root of this distribution.
-----------------------------------------------------------------------------*/
/* ----------------------------------------------------------------------------
Disclaimer: Unlike the rest of the considerate hand-crafted mimalloc sources,
this file was written with AI assistance (for the protobuf format generation).
-----------------------------------------------------------------------------*/
// Write snapshots of the pprof profiler in the (textual) pprof heap profile format or the protobuf format.
#include "mimalloc.h"
#include "mimalloc/util/libc.h"
#include "mimalloc/util/options.h"
#include "mimalloc/util/stats.h"
#include "pprof.h"
#if !MI_PROFILE_USE_BUILTIN
void _mi_pprof_profiler_snapshot(mi_pprof_profiler_t* profiler) {
MI_UNUSED(profiler);
}
#else
// forward declarations; implemented further below.
static bool mi_pprof_snapshot_text(mi_pprof_profiler_t* prof, const char* fname);
static bool mi_pprof_snapshot_proto(mi_pprof_profiler_t* prof, const char* fname);
// Write out the current profiler data to a new snapshot file named
// `<base_file_name>.<seq>.<ext>` with an incrementing sequence number
// (`.heap` for the text format, mimicking the naming used by the original
// (gperftools) pprof heap profiler; `.pb` for the (uncompressed) protobuf
// format). The format is chosen when the profiler is created (see
// `mi_pprof_profiler_new`). The base file name is also set at creation time;
// if it is NULL, no snapshot is written and this function does nothing.
void _mi_pprof_profiler_snapshot(mi_pprof_profiler_t* prof) {
if (prof == NULL) return;
bool was_running = mi_profiler_stop(&prof->profiler);
mi_locations_t* locations = &prof->locations;
if (locations->buckets != NULL && prof->base_file_name != NULL) {
const size_t seq = ++prof->snapshot_count;
char fname[1024];
bool ok;
if (!prof->format_text) {
_mi_snprintf(fname, sizeof(fname), "%s.%04zu.pb", prof->base_file_name, seq);
ok = mi_pprof_snapshot_proto(prof, fname);
}
else {
_mi_snprintf(fname, sizeof(fname), "%s.%04zu.heap", prof->base_file_name, seq);
ok = mi_pprof_snapshot_text(prof, fname);
}
// A snapshot is best effort: it can fail when the profiler's own heap is out
// of memory (or the file cannot be created), in which case no (or only a
// partial) snapshot is written. Never let that affect the profiled program.
if (ok) {
_mi_verbose_message("pprof profiler snapshot: %s\n", fname);
}
else {
_mi_verbose_message("pprof profiler: unable to write snapshot (out of memory?)\n");
}
}
if (was_running) { mi_profiler_start(&prof->profiler); }
}
// ---------------------------------------------------------------------------
// Estimate the actual values from the sampled ones (using Q32 fixed-point arithmetic)
// ---------------------------------------------------------------------------
#define MI_Q32_ONE ((uint64_t)1 << 32)
// `n/d` in Q32 (saturating) without needing 128-bit arithmetic
static uint64_t mi_div_q32(uint64_t n, uint64_t d) {
uint64_t q = n / d;
if (q >= MI_Q32_ONE) return UINT64_MAX;
uint64_t r = n % d;
for (int i = 0; i < 32; i++) { // long division for the 32 fractional bits (where `r < d`)
q <<= 1;
if (r >= d - r) { r -= d - r; q |= 1; } else { r <<= 1; }
}
return q;
}
// `1 - e^(-x)` in Q32 for `x` in Q32: halve `x` until small, use a Taylor series, and double back using `1 - e^(-2x) = y*(2-y)` where `y = 1 - e^(-x)`
static uint64_t mi_one_minus_exp_neg_q32(uint64_t x) {
int k = 0;
while (x > MI_Q32_ONE/2) { x >>= 1; k++; }
uint64_t y = x;
uint64_t term = x;
for (unsigned i = 2; i <= 10; i++) { // partial sums stay positive as the terms decrease
term = ((term * x) >> 32) / i;
y = ((i & 1) != 0 ? y + term : y - term);
}
while (k-- > 0) { y = (y * (2*MI_Q32_ONE - y)) >> 32; } // no overflow as `y < 1`
return y;
}
// `n/p` for a probability `p` in Q32
static int64_t mi_unsample_value(uint64_t n, uint64_t p) {
const uint64_t m = mi_div_q32(n, p);
return (m > INT64_MAX ? INT64_MAX : (int64_t)m);
}
// Estimate the actual count and bytes from the sampled ones (as pprof does for sampled heap profiles):
// an object of size `s` is sampled with probability `1 - e^(-s/rate)`.
static void mi_pprof_unsample(int64_t* count, int64_t* bytes, size_t rate) {
if (*count <= 0 || *bytes <= 0 || rate <= 1) return;
const uint64_t n = (uint64_t)*count;
const uint64_t b = (uint64_t)*bytes;
const uint64_t avg = b / n;
if (avg/32 >= rate) return; // sampled with probability 1 (within 1e-13)
const uint64_t x = mi_div_q32(avg, rate) + mi_div_q32(b % n, n) / rate; // `(b/n)/rate` in Q32
uint64_t p = mi_one_minus_exp_neg_q32(x);
if (p == 0) { p = 1; }
*count = mi_unsample_value(n, p);
*bytes = mi_unsample_value(b, p);
}
// ---------------------------------------------------------------------------
// Writing the profile in the (original, textual) pprof heap profile format
// ---------------------------------------------------------------------------
// Write out the current profiler data in the pprof heap profile text format:
// a header line with the totals, followed by one line per unique call
// location, and a trailer with the mapped libraries (used by `pprof` to
// symbolize the addresses).
static bool mi_pprof_snapshot_text(mi_pprof_profiler_t* prof, const char* fname) {
mi_locations_t* locations = &prof->locations;
FILE* f = _mi_pprof_fopen(fname, false);
if (f == NULL) return false;
// first pass: compute the totals over all locations
// (snapshot is assumed to run single-threaded while sampling is stopped, so
// `alloc_count`/`alloc_bytes` can be read directly; `inuse_count`/`inuse_bytes`
// are still `_Atomic` fields so we use a relaxed load for those)
uint64_t total_inuse_objects = 0;
uint64_t total_inuse_bytes = 0;
uint64_t total_alloc_objects = 0;
uint64_t total_alloc_bytes = 0;
for (size_t i = 0; i < locations->bucket_count; i++) {
for (mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, &locations->buckets[i]); loc != NULL; loc = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next)) {
total_inuse_objects += (uint64_t)mi_atomic_load_relaxed(&loc->inuse_count);
total_inuse_bytes += (uint64_t)mi_atomic_load_relaxed(&loc->inuse_bytes);
total_alloc_objects += (uint64_t)loc->alloc_count;
total_alloc_bytes += (uint64_t)loc->alloc_bytes;
}
}
// the sample rate in the header lets pprof estimate the actual counts and bytes
_mi_pprof_fprintf(f, "heap profile: %6l8d: %8l8d [%6l8d: %8l8d] @ heap_v2/%zu\n",
total_inuse_objects, total_inuse_bytes, total_alloc_objects, total_alloc_bytes, prof->sample_threshold);
// second pass: write one line per unique call location
for (size_t i = 0; i < locations->bucket_count; i++) {
for (mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, &locations->buckets[i]); loc != NULL; loc = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next)) {
const size_t inuse_count = mi_atomic_load_relaxed(&loc->inuse_count);
const size_t inuse_bytes = mi_atomic_load_relaxed(&loc->inuse_bytes);
_mi_pprof_fprintf(f, "%6l8d: %8l8d [%6l8d: %8l8d] @",
(uint64_t)inuse_count, (uint64_t)inuse_bytes,
(uint64_t)loc->alloc_count, (uint64_t)loc->alloc_bytes);
for (size_t j = 0; j < loc->callstack.count; j++) {
_mi_pprof_fprintf(f, " 0x%tx", (size_t)(uintptr_t)loc->callstack.frames[j]);
}
fprintf(f, "\n");
}
}
_mi_pprof_write_mapped_libraries(f);
const bool ok = (ferror(f) == 0);
if (fclose(f) != 0) return false;
return ok;
}
// ---------------------------------------------------------------------------
// Writing the profile in the modern `pprof` protobuf format
// (`perftools.profiles.Profile`, see
// https://github.com/google/pprof/blob/main/proto/profile.proto)
//
// On-disk, `pprof` profiles are normally gzip-compressed, but `pprof` also
// accepts a *raw* (uncompressed) serialized `Profile` message: it only tries
// to gunzip the input if it starts with the gzip magic bytes, and otherwise
// parses it directly as a protobuf message. We take advantage of that so we
// don't need to implement a deflate/gzip writer.
//
// We still need the locations hash table for this format: samples are
// aggregated per call site (thread + callstack), exactly as for the text
// format, rather than emitting one `Sample` per individual allocation. What
// changes is the *representation* of a callstack: instead of a flat list of
// hex addresses embedded directly in a text line, each frame address becomes
// its own (deduplicated) `Location` entry, referenced by id from `Sample.
// location_id`. The originating thread id -- which the text format doesn't
// record explicitly (it is only implicit in which addresses got sampled) --
// is attached to each `Sample` via a numeric `Label` (pprof has no dedicated
// thread-id field, but labels are the generic mechanism for this).
// ---------------------------------------------------------------------------
// A simple growable byte buffer, allocated from the profiler's own heap.
// If an allocation fails, `failed` is set and all further writes become no-ops;
// the caller must check it and discard the whole snapshot.
typedef struct {
mi_heap_t* heap;
uint8_t* data;
size_t size;
size_t capacity;
bool failed;
} mi_pb_buf_t;
static void mi_pb_init(mi_pb_buf_t* b, mi_heap_t* heap) {
b->heap = heap;
b->data = NULL;
b->size = 0;
b->capacity = 0;
b->failed = false;
}
static void mi_pb_done(mi_pb_buf_t* b) {
mi_free(b->data);
b->data = NULL;
b->size = 0;
b->capacity = 0;
b->failed = false;
}
static bool mi_pb_reserve(mi_pb_buf_t* b, size_t extra) {
if (b->failed) return false;
if (b->size + extra <= b->capacity) return true;
size_t new_capacity = (b->capacity == 0 ? 4096 : b->capacity * 2);
while (new_capacity < b->size + extra) { new_capacity *= 2; }
uint8_t* const new_data = (uint8_t*)mi_heap_realloc(b->heap, b->data, new_capacity);
if (new_data == NULL) { b->failed = true; return false; } // keep the old buffer (and capacity) intact
b->data = new_data;
b->capacity = new_capacity;
return true;
}
static void mi_pb_bytes(mi_pb_buf_t* b, const void* p, size_t n) {
if (n == 0) return; // avoid calling memcpy with a NULL source
if (!mi_pb_reserve(b, n)) return;
memcpy(b->data + b->size, p, n);
b->size += n;
}
static void mi_pb_byte(mi_pb_buf_t* b, uint8_t byte) {
mi_pb_bytes(b, &byte, 1);
}
// Unsigned LEB128 (protobuf "varint") encoding.
static void mi_pb_varint(mi_pb_buf_t* b, uint64_t v) {
do {
uint8_t byte = (uint8_t)(v & 0x7F);
v >>= 7;
if (v != 0) { byte |= 0x80; }
mi_pb_byte(b, byte);
} while (v != 0);
}
static void mi_pb_tag(mi_pb_buf_t* b, uint32_t field, uint32_t wiretype) {
mi_pb_varint(b, ((uint64_t)field << 3) | wiretype);
}
// Scalar varint fields; proto3 encoders may omit fields set to their default
// (0/false) value, which we do here to keep the output compact.
static void mi_pb_uint64_field(mi_pb_buf_t* b, uint32_t field, uint64_t v) {
if (v == 0) return;
mi_pb_tag(b, field, 0);
mi_pb_varint(b, v);
}
static void mi_pb_int64_field(mi_pb_buf_t* b, uint32_t field, int64_t v) {
if (v == 0) return;
mi_pb_tag(b, field, 0);
mi_pb_varint(b, (uint64_t)v); // proto3 plain int64 is encoded as a varint of its 2's complement bit pattern
}
// Length-delimited fields (string/bytes/embedded message) are always emitted,
// even when empty: this matters for `string_table[0]` (which must be the
// empty string) and for repeated message fields (an empty message is still a
// valid, distinct list entry).
static void mi_pb_len_field(mi_pb_buf_t* b, uint32_t field, const void* data, size_t len) {
mi_pb_tag(b, field, 2);
mi_pb_varint(b, len);
mi_pb_bytes(b, data, len);
}
static void mi_pb_message_field(mi_pb_buf_t* b, uint32_t field, const mi_pb_buf_t* msg) {
if (msg->failed) { b->failed = true; return; } // propagate a failure from the nested message
mi_pb_len_field(b, field, msg->data, msg->size);
}
// A packed repeated scalar field (used for `Sample.location_id`/`Sample.value`):
// all elements are varint-encoded and concatenated into one length-delimited blob.
static void mi_pb_packed_uint64_field(mi_pb_buf_t* b, uint32_t field, const uint64_t* values, size_t count) {
if (count == 0) return;
mi_pb_buf_t tmp;
mi_pb_init(&tmp, b->heap);
for (size_t i = 0; i < count; i++) { mi_pb_varint(&tmp, values[i]); }
mi_pb_message_field(b, field, &tmp);
mi_pb_done(&tmp);
}
static void mi_pb_packed_int64_field(mi_pb_buf_t* b, uint32_t field, const int64_t* values, size_t count) {
mi_pb_packed_uint64_field(b, field, (const uint64_t*)values, count); // same varint bit pattern
}
// ---------------------------------------------------------------------------
// String table: dedups strings into `Profile.string_table` (field 6) and
// hands back the (1-based; index 0 is reserved for "") index of each string.
// A linear search is fine here: a profile typically only has a handful to a
// few hundred distinct strings (sample type names, mapped file paths).
// ---------------------------------------------------------------------------
typedef struct {
char* str;
int64_t index;
} mi_pb_string_entry_t;
typedef struct {
mi_heap_t* heap;
mi_pb_string_entry_t* entries;
size_t count;
size_t capacity;
bool failed; // set if an allocation failed; the table is then incomplete
mi_pb_buf_t table; // the serialized `string_table` entries (Profile field 6)
} mi_pb_strings_t;
static void mi_pb_strings_init(mi_pb_strings_t* strings, mi_heap_t* heap) {
strings->heap = heap;
strings->entries = NULL;
strings->count = 0;
strings->capacity = 0;
strings->failed = false;
mi_pb_init(&strings->table, heap);
mi_pb_len_field(&strings->table, 6, "", 0); // index 0 must always be ""
}
static void mi_pb_strings_done(mi_pb_strings_t* strings) {
for (size_t i = 0; i < strings->count; i++) { mi_free(strings->entries[i].str); }
mi_free(strings->entries);
mi_pb_done(&strings->table);
}
static int64_t mi_pb_strings_intern(mi_pb_strings_t* strings, const char* s) {
if (strings->failed) return 0;
if (s == NULL) { s = ""; }
for (size_t i = 0; i < strings->count; i++) {
if (_mi_streq(strings->entries[i].str, s)) { return strings->entries[i].index; }
}
if (strings->count == strings->capacity) {
const size_t new_capacity = (strings->capacity == 0 ? 32 : strings->capacity * 2);
mi_pb_string_entry_t* const new_entries = (mi_pb_string_entry_t*)mi_heap_realloc(strings->heap, strings->entries, new_capacity * sizeof(*strings->entries));
if (new_entries == NULL) { strings->failed = true; return 0; }
strings->entries = new_entries;
strings->capacity = new_capacity;
}
char* const copy = mi_heap_strndup(strings->heap, s, 1024);
if (copy == NULL) { strings->failed = true; return 0; }
const int64_t index = (int64_t)(strings->count + 1); // index 0 is reserved for ""
strings->entries[strings->count].str = copy;
strings->entries[strings->count].index = index;
strings->count++;
mi_pb_len_field(&strings->table, 6, s, strlen(s));
if (strings->table.failed) { strings->failed = true; return 0; }
return index;
}
// ---------------------------------------------------------------------------
// A small single-threaded hash map from a raw frame address to the id of its
// (deduplicated) `Location` entry. Only ever built and consulted from within
// `mi_pprof_snapshot_proto`, so (unlike the locations hash table) it needs no
// atomics at all.
// ---------------------------------------------------------------------------
typedef struct mi_pb_addr_entry_s {
struct mi_pb_addr_entry_s* next;
uintptr_t addr;
uint64_t id;
} mi_pb_addr_entry_t;
typedef struct {
mi_heap_t* heap;
size_t bucket_count;
mi_pb_addr_entry_t** buckets;
bool failed; // set if an allocation failed (the map is then unusable)
} mi_pb_addr_map_t;
static void mi_pb_addr_map_init(mi_pb_addr_map_t* map, mi_heap_t* heap, size_t bucket_count) {
map->heap = heap;
map->bucket_count = (bucket_count == 0 ? 1 : bucket_count);
map->buckets = (mi_pb_addr_entry_t**)mi_heap_zalloc(heap, map->bucket_count * sizeof(*map->buckets));
map->failed = (map->buckets == NULL);
}
static void mi_pb_addr_map_done(mi_pb_addr_map_t* map) {
if (map->buckets == NULL) return;
for (size_t i = 0; i < map->bucket_count; i++) {
mi_pb_addr_entry_t* e = map->buckets[i];
while (e != NULL) {
mi_pb_addr_entry_t* const next = e->next;
mi_free(e);
e = next;
}
}
mi_free(map->buckets);
map->buckets = NULL;
}
// Returns the existing location id for `addr`, or assigns it `next_id` and
// returns that (setting `*is_new` to true) if this is the first time `addr`
// is seen.
static uint64_t mi_pb_addr_map_find_or_insert(mi_pb_addr_map_t* map, mi_addr_t addr, uint64_t next_id, bool* is_new) {
if (map->failed || map->buckets == NULL) { map->failed = true; *is_new = false; return 0; }
const size_t idx = (size_t)(addr ^ (addr >> 16)) % map->bucket_count;
for (mi_pb_addr_entry_t* e = map->buckets[idx]; e != NULL; e = e->next) {
if (e->addr == addr) { *is_new = false; return e->id; }
}
mi_pb_addr_entry_t* e = mi_heap_malloc_tp(mi_pb_addr_entry_t, map->heap);
if (e == NULL) { map->failed = true; *is_new = false; return 0; }
e->addr = addr;
e->id = next_id;
e->next = map->buckets[idx];
map->buckets[idx] = e;
*is_new = true;
return next_id;
}
// Find the (1-based) `Mapping` id of the module containing `addr`, or 0 if
// `addr` doesn't fall within any collected module range.
static uint64_t mi_pprof_find_mapping_id(const mi_pprof_modules_t* mods, uintptr_t addr) {
for (size_t i = 0; i < mods->count; i++) {
if (addr >= mods->modules[i].start && addr < mods->modules[i].end) { return (uint64_t)(i + 1); }
}
return 0;
}
// Build the current profiler data as a raw (uncompressed) `perftools.profiles.Profile`
// protobuf message in `profile` (see the file-level comment above).
// Returns `false` if any allocation failed; the caller releases all buffers in either case.
static bool mi_pprof_build_proto(mi_pprof_profiler_t* prof, mi_pb_buf_t* profile, mi_pb_strings_t* strings,
mi_pprof_modules_t* mods, mi_pb_addr_map_t* addr_map)
{
mi_heap_t* heap = prof->profile_heap;
mi_locations_t* locations = &prof->locations;
const int64_t str_count = mi_pb_strings_intern(strings, "count");
const int64_t str_bytes = mi_pb_strings_intern(strings, "bytes");
const int64_t str_alloc_objects = mi_pb_strings_intern(strings, "alloc_objects");
const int64_t str_alloc_bytes = mi_pb_strings_intern(strings, "alloc_bytes");
const int64_t str_inuse_objects = mi_pb_strings_intern(strings, "inuse_objects");
const int64_t str_inuse_bytes = mi_pb_strings_intern(strings, "inuse_bytes");
const int64_t str_space = mi_pb_strings_intern(strings, "space");
const int64_t str_thread = mi_pb_strings_intern(strings, "thread");
if (strings->failed) return false;
// Profile.sample_type = 1: [alloc_objects:count, alloc_bytes:bytes, inuse_objects:count, inuse_bytes:bytes]
{
const int64_t types[4] = { str_alloc_objects, str_alloc_bytes, str_inuse_objects, str_inuse_bytes };
const int64_t units[4] = { str_count, str_bytes, str_count, str_bytes };
for (size_t i = 0; i < 4; i++) {
mi_pb_buf_t vt;
mi_pb_init(&vt, heap);
mi_pb_int64_field(&vt, 1, types[i]);
mi_pb_int64_field(&vt, 2, units[i]);
mi_pb_message_field(profile, 1, &vt);
mi_pb_done(&vt);
}
}
// Profile.mapping = 3: the modules loaded into this process, so `pprof` can
// symbolize the (raw) addresses recorded in each `Location` below.
_mi_pprof_collect_modules(heap, mods);
if (mods->failed) return false;
for (size_t i = 0; i < mods->count; i++) {
const mi_pprof_module_t* m = &mods->modules[i];
mi_pb_buf_t mapping;
mi_pb_init(&mapping, heap);
mi_pb_uint64_field(&mapping, 1, (uint64_t)(i + 1)); // id
mi_pb_uint64_field(&mapping, 2, (uint64_t)m->start); // memory_start
mi_pb_uint64_field(&mapping, 3, (uint64_t)m->end); // memory_limit
if (m->path != NULL) {
mi_pb_int64_field(&mapping, 5, mi_pb_strings_intern(strings, m->path)); // filename
}
mi_pb_message_field(profile, 3, &mapping);
mi_pb_done(&mapping);
if (profile->failed || strings->failed) return false;
}
// Profile.location = 4 and Profile.sample = 2: one (deduplicated) `Location`
// per unique frame address, and one `Sample` per unique call site (i.e. one
// per entry in our own locations hash table), exactly mirroring the text
// format's aggregation.
mi_pb_addr_map_init(addr_map, heap, locations->bucket_count);
if (addr_map->failed) return false;
uint64_t next_location_id = 1;
for (size_t i = 0; i < locations->bucket_count; i++) {
for (mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, &locations->buckets[i]); loc != NULL; loc = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next)) {
const size_t inuse_count = mi_atomic_load_relaxed(&loc->inuse_count);
const size_t inuse_bytes = mi_atomic_load_relaxed(&loc->inuse_bytes);
const size_t frame_count = (loc->callstack.count > MI_MAX_BACKTRACE_DEPTH ? MI_MAX_BACKTRACE_DEPTH : loc->callstack.count);
uint64_t location_ids[MI_MAX_BACKTRACE_DEPTH];
for (size_t j = 0; j < frame_count; j++) {
const mi_addr_t addr = mi_addr(loc->callstack.frames[j]);
bool is_new = false;
const uint64_t id = mi_pb_addr_map_find_or_insert(addr_map, addr, next_location_id, &is_new);
if (addr_map->failed) return false;
location_ids[j] = id;
if (is_new) {
next_location_id++;
mi_pb_buf_t location;
mi_pb_init(&location, heap);
mi_pb_uint64_field(&location, 1, id); // id
mi_pb_uint64_field(&location, 2, mi_pprof_find_mapping_id(mods, addr)); // mapping_id
mi_pb_uint64_field(&location, 3, (uint64_t)addr); // address
mi_pb_message_field(profile, 4, &location);
mi_pb_done(&location);
}
}
mi_pb_buf_t sample;
mi_pb_init(&sample, heap);
mi_pb_packed_uint64_field(&sample, 1, location_ids, frame_count); // location_id (leaf-first, matching our capture order)
int64_t values[4] = {
loc->alloc_count, loc->alloc_bytes,
(int64_t)inuse_count, (int64_t)inuse_bytes
};
mi_pprof_unsample(&values[0], &values[1], prof->sample_threshold);
mi_pprof_unsample(&values[2], &values[3], prof->sample_threshold);
mi_pb_packed_int64_field(&sample, 2, values, 4); // value
{
// attach the originating thread id as a numeric label: pprof has no
// dedicated thread-id field, but `Sample.label` is the generic
// mechanism for this kind of per-sample metadata.
mi_pb_buf_t label;
mi_pb_init(&label, heap);
mi_pb_int64_field(&label, 1, str_thread); // key
mi_pb_int64_field(&label, 3, (int64_t)loc->thread_id); // num
mi_pb_message_field(&sample, 3, &label);
mi_pb_done(&label);
}
mi_pb_message_field(profile, 2, &sample);
mi_pb_done(&sample);
if (profile->failed) return false;
}
}
// Profile.period_type = 11 / Profile.period = 12: matches the convention
// used by Go's heap profiles ("space"/"bytes", sampling period in bytes).
{
mi_pb_buf_t period_type;
mi_pb_init(&period_type, heap);
mi_pb_int64_field(&period_type, 1, str_space);
mi_pb_int64_field(&period_type, 2, str_bytes);
mi_pb_message_field(profile, 11, &period_type);
mi_pb_done(&period_type);
}
mi_pb_int64_field(profile, 12, (int64_t)prof->sample_threshold); // period
const int64_t elapsed_nanos = (int64_t)(_mi_clock_now() - prof->start_clock) * 1000000LL; // clock is in milli-secs
mi_pb_int64_field(profile, 9, prof->start_time_secs * 1000000000LL + elapsed_nanos); // time_nanos
mi_pb_int64_field(profile, 10, elapsed_nanos); // duration_nanos
// the string table (Profile.string_table = 6) can only be finalized now,
// since strings were interned into it while building the message above.
if (strings->failed) return false;
mi_pb_bytes(profile, strings->table.data, strings->table.size);
if (profile->failed) return false;
return true;
}
static bool mi_pprof_write_file(const char* fname, const mi_pb_buf_t* profile) {
FILE* f = _mi_pprof_fopen(fname, true);
if (f == NULL) return false;
bool ok = (fwrite(profile->data, 1, profile->size, f) == profile->size);
if (fclose(f) != 0) { ok = false; }
return ok;
}
// Write out the current profiler data as a protobuf `Profile` message.
// Returns `false` if any allocation failed (in which case no file is written):
// the snapshot is best effort and must never crash the profiled program.
static bool mi_pprof_snapshot_proto(mi_pprof_profiler_t* prof, const char* fname) {
mi_heap_t* heap = prof->profile_heap;
mi_pb_buf_t profile;
mi_pb_init(&profile, heap);
mi_pb_strings_t strings;
mi_pb_strings_init(&strings, heap);
mi_pprof_modules_t mods;
mods.modules = NULL;
mods.count = 0;
mods.capacity = 0;
mods.failed = false;
mi_pb_addr_map_t addr_map;
addr_map.heap = heap;
addr_map.bucket_count = 0;
addr_map.buckets = NULL;
addr_map.failed = false;
bool ok = mi_pprof_build_proto(prof, &profile, &strings, &mods, &addr_map);
mi_pb_addr_map_done(&addr_map);
_mi_pprof_modules_done(&mods);
mi_pb_strings_done(&strings);
if (ok) { ok = mi_pprof_write_file(fname, &profile); }
mi_pb_done(&profile);
return ok;
}
#endif // MI_PROFILE_USE_BUILTIN