123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625 | // Implementation of the runtime function profiler (see profiling.h).
#include <gc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#if defined(__x86_64__) || defined(__i386__)
#include <cpuid.h>
#include <x86intrin.h>
#endif
#include <unistd.h>
#include "print.h"
#include "profiling.h"
#include "stdlib.h" // USE_COLOR
#include "util.h"
public
bool tomo_profiling_enabled = false;
public
const char *tomo_profile_noun = "function";
// The functions the report lists, collected when the call tree is summed up at
// exit (so a function that never ran never appears), and the frame of the call
// currently executing. Plain statics: like the rest of the runtime, this is
// single-threaded.
static tomo_profile_site_t *sites = NULL;
static tomo_profile_frame_t *current_frame = NULL;
// Everything the profiler records. The root is synthetic: its children are the
// outermost instrumented calls (usually just main()). Both the table and the
// graph are derived from this at exit.
static tomo_profile_node_t call_tree_root = {};
// Bounds on the tree, so a program with unbounded call depth (mutual recursion)
// or an enormous number of distinct call paths can't grow it without limit.
// Calls past either limit fold into their caller's node: their time still
// counts, it just isn't split out.
#define MAX_TREE_DEPTH 64
#define MAX_TREE_NODES 8192
static int tree_nodes = 0;
static const char *report_path = NULL; // where the table goes; NULL means stderr
static const char *flame_path = NULL; // where the flame graph goes, if it was asked for
// Both clocks sampled at the start of the run, and the tick rate worked out
// from a second sample when the report is printed. Measuring the rate over the
// whole run is what lets the cycle counter be used without knowing its
// frequency (and makes any constant error in it cancel out).
static double profiling_started = 0.0;
static uint64_t ticks_started = 0;
static double seconds_per_tick = 1e-9;
// Ticks are only meaningful once the report knows how fast they ran:
static double tick_seconds(uint64_t t) {
return (double)t * seconds_per_tick;
}
static bool reported = false;
static double now(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
// Whether the CPU's cycle counter is usable as a clock. Set once at startup;
// when it isn't, ticks are plain nanoseconds and everything downstream is
// unchanged, because the tick rate is measured rather than assumed.
static bool use_cycle_counter = false;
// The timestamp taken on entering and leaving every instrumented call, so this
// is the hottest code in an instrumented build. `rdtsc` is a handful of cycles
// against ~20ns for clock_gettime(), which even through the vDSO is a real
// function call; the branch on `use_cycle_counter` costs nothing measurable
// since it always goes the same way.
static inline uint64_t ticks(void) {
#if defined(__x86_64__) || defined(__i386__)
if (use_cycle_counter) return __rdtsc();
#elif defined(__aarch64__)
if (use_cycle_counter) {
uint64_t counter;
__asm__ volatile("mrs %0, cntvct_el0" : "=r"(counter));
return counter;
}
#endif
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000u + (uint64_t)ts.tv_nsec;
}
// A cycle counter is only a clock if it ticks at a constant rate regardless of
// what the CPU's frequency is doing. On x86 that is the "invariant TSC" feature
// bit; on ARM the architected counter is invariant by definition. Anywhere else
// (or on an x86 too old to promise it), fall back to clock_gettime().
static bool cycle_counter_is_usable(void) {
#if defined(__x86_64__) || defined(__i386__)
unsigned int eax, ebx, ecx, edx;
if (__get_cpuid_max(0x80000000u, NULL) < 0x80000007u) return false;
if (!__get_cpuid(0x80000007u, &eax, &ebx, &ecx, &edx)) return false;
return (edx & (1u << 8)) != 0; // invariant TSC
#elif defined(__aarch64__)
return true;
#else
return false;
#endif
}
// Sums the call tree into the per-function totals (defined below, next to the
// report that is its main caller; a forked child also needs it before handing
// its totals over).
static void aggregate_sites(tomo_profile_node_t *parent);
// The node for `site` called from `parent`: the existing child if this path has
// been walked before, otherwise a fresh one. Returns `parent` itself when the
// call is directly recursive (so `fib` calling `fib` stays one node instead of
// growing a node per level) or when a limit has been hit; the caller records
// that as a folded frame.
static tomo_profile_node_t *tree_node_for(tomo_profile_node_t *parent, tomo_profile_site_t *site, int depth) {
if (parent->site == site || depth >= MAX_TREE_DEPTH) return parent;
for (tomo_profile_node_t *child = parent->children; child; child = child->next_sibling)
if (child->site == site) return child;
if (tree_nodes >= MAX_TREE_NODES) return parent;
tomo_profile_node_t *node = calloc(1, sizeof(tomo_profile_node_t));
if (!node) return parent;
tree_nodes += 1;
node->site = site;
node->parent = parent;
// Appended at the end so siblings stay in the order they were first called,
// which is the order the flame graph lays them out in:
tomo_profile_node_t **end = &parent->children;
while (*end)
end = &(*end)->next_sibling;
*end = node;
return node;
}
public
void tomo_profile_begin(tomo_profile_frame_t *frame, tomo_profile_site_t *site) {
tomo_profile_node_t *parent = current_frame ? current_frame->node : &call_tree_root;
int depth = current_frame ? current_frame->depth + 1 : 0;
// The memo turns the usual case, called again from the same place, into
// one comparison; only a call from somewhere new walks the tree:
tomo_profile_node_t *node;
if (site->cached_parent == parent) {
node = site->cached_node;
} else {
node = tree_node_for(parent, site, depth);
site->cached_parent = parent;
site->cached_node = node;
}
*frame = (tomo_profile_frame_t){.node = node, .parent = current_frame, .folded = (node == parent), .depth = depth};
current_frame = frame;
// Taken last, so none of the bookkeeping above lands inside the measurement:
frame->start = ticks();
}
public
void tomo_profile_end(tomo_profile_frame_t *frame) {
uint64_t elapsed = ticks() - frame->start;
frame->node->calls += 1;
// A folded frame's time is already inside the node it shares with its
// caller; adding it again would count the same time twice. Everything else
// the report shows (self time, per-function totals, call counts) is
// summed out of the tree once, at exit, rather than per call.
if (!frame->folded) frame->node->total_ticks += elapsed;
current_frame = frame->parent;
}
// Whether an environment variable is set to something that means "off".
static bool is_off(const char *value) {
return value != NULL
&& (strcmp(value, "0") == 0 || strcmp(value, "no") == 0 || strcmp(value, "false") == 0
|| strcmp(value, "") == 0);
}
public
void tomo_profile_mark_start(void) {
if (profiling_started > 0.0) return; // already stamped
use_cycle_counter = cycle_counter_is_usable();
profiling_started = now();
ticks_started = ticks();
}
// tomo_cleanup() runs this, so the report is printed on every way out that
// the runtime already handles: a normal exit, a `fail()`, or a fatal signal.
static void report_at_cleanup(void *userdata) {
(void)userdata;
tomo_profile_report();
}
public
void tomo_profile_enable(void) {
tomo_profile_mark_start();
// Where the two outputs go. Both are read here rather than at exit so that a
// program which changes its own environment can't move them mid-run:
const char *file = getenv("PROFILE_FILE");
if (file && file[0] != '\0') report_path = file;
const char *flame = getenv("FLAME_GRAPH");
if (flame && flame[0] != '\0') flame_path = flame;
tomo_profiling_enabled = true;
// An instrumented program calls tomo_profile_start() ahead of tomo_init(),
// so the collector may not be up yet and tomo_at_cleanup() allocates.
// GC_INIT() is idempotent, so tomo_init()'s own call stays a no-op.
GC_INIT();
tomo_at_cleanup((Closure_t){.fn = report_at_cleanup, .userdata = NULL});
}
public
void tomo_profile_start(void) {
// Instrumented programs profile every run; PROFILE=0 is the way to run one
// without paying for (or printing) a profile:
if (is_off(getenv("PROFILE"))) return;
tomo_profile_enable();
}
public
void tomo_profile_reset(void) {
call_tree_root = (tomo_profile_node_t){};
current_frame = NULL;
sites = NULL;
tree_nodes = 0;
}
// One line per entry: "<ticks> <calls> <name>". The name goes last so it may
// contain spaces, which the counts never do.
public
void tomo_profile_serialize(int fd) {
if (!tomo_profiling_enabled) return;
// Nothing else runs in this process afterwards, so summing the tree into
// the sites here (rather than at exit, which never comes for a forked
// child) is safe:
aggregate_sites(&call_tree_root);
char buf[512];
for (tomo_profile_site_t *site = sites; site; site = site->next) {
int n = snprintf(buf, sizeof(buf), "%llu %lld %s\n", (unsigned long long)site->total_ticks,
(long long)site->calls, site->name);
if (n > 0) (void)!write(fd, buf, (size_t)n);
}
}
public
void tomo_profile_merge(int fd) {
if (!tomo_profiling_enabled) {
close(fd);
return;
}
FILE *f = fdopen(fd, "r");
if (!f) return;
char line[512];
while (fgets(line, sizeof(line), f)) {
unsigned long long total;
long long calls;
int offset = 0;
if (sscanf(line, "%llu %lld %n", &total, &calls, &offset) < 2) continue;
char *name = line + offset;
size_t length = strlen(name);
if (length > 0 && name[length - 1] == '\n') name[length - 1] = '\0';
// The work happened in another process, so it has no place in this
// one's call tree: it becomes a top-level entry, which is where a
// forked child's work belongs anyway. The name lives in a stack buffer
// here, so both it and its site have to be copied to outlive the loop.
tomo_profile_node_t *node = NULL;
for (tomo_profile_node_t *child = call_tree_root.children; child; child = child->next_sibling)
if (streq(child->site->name, name)) node = child;
if (node == NULL) {
tomo_profile_site_t *site = calloc(1, sizeof(tomo_profile_site_t));
char *copy = calloc(length + 1, 1);
if (!site || !copy) break;
memcpy(copy, name, length);
site->name = copy;
node = tree_node_for(&call_tree_root, site, 0);
if (node == &call_tree_root) break; // out of nodes
}
node->total_ticks += (uint64_t)total;
node->calls += (int64_t)calls;
}
fclose(f); // also closes fd
}
// The site's file, shortened to a path relative to the working directory when
// it is under it, since the absolute paths the compiler bakes in are long and
// the leading directories are the same for every line.
static const char *short_path(const char *path) {
if (path == NULL) return NULL;
static char cwd[4096] = "";
static bool got_cwd = false;
if (!got_cwd) {
got_cwd = true;
if (!getcwd(cwd, sizeof(cwd))) cwd[0] = '\0';
}
size_t len = strlen(cwd);
if (len > 0 && strncmp(path, cwd, len) == 0 && path[len] == '/') return path + len + 1;
return path;
}
// A duration in whichever unit keeps it readable: these span
// nanoseconds (a one-line getter called a million times) to seconds (a function
// called once), and a single fixed unit makes one end or the other unreadable.
static const char *format_time(double seconds, char *buf, size_t size) {
if (seconds < 1e-6) snprintf(buf, size, "%.0fns", 1e9 * seconds);
else if (seconds < 1e-3) snprintf(buf, size, "%.2fus", 1e6 * seconds);
else if (seconds < 1.0) snprintf(buf, size, "%.2fms", 1e3 * seconds);
else snprintf(buf, size, "%.2fs", seconds);
return buf;
}
static int by_self_time(const void *a, const void *b) {
uint64_t self_a = (*(tomo_profile_site_t *const *)a)->self_ticks,
self_b = (*(tomo_profile_site_t *const *)b)->self_ticks;
if (self_a < self_b) return 1;
if (self_a > self_b) return -1;
return 0;
}
// How many characters `text` is, as opposed to how many bytes: Tomo identifiers
// can be non-ASCII, and it is characters that have to be counted against the
// room a label has.
static int utf8_length(const char *text) {
int length = 0;
for (const char *c = text; *c; c++)
if ((*c & 0xC0) != 0x80) length += 1;
return length;
}
// Copy as much of `src` as fits in `max_chars` characters, never splitting a
// UTF-8 sequence in half.
static void copy_truncated(char *dst, size_t dst_size, const char *src, int max_chars) {
size_t used = 0;
int cols = 0;
for (const char *c = src; *c && cols < max_chars; c++) {
size_t len = 1;
while (c[len] && (c[len] & 0xC0) == 0x80)
len += 1;
if (used + len + 1 > dst_size) break;
memcpy(dst + used, c, len);
used += len;
cols += 1;
c += len - 1;
}
dst[used] = '\0';
}
// --- Flame graph -------------------------------------------------------------
// The call tree drawn as an image, written when FLAME_GRAPH names a path: one
// box per frame, as wide as its share of the run and stacked on the
// frame that called it. Where the table answers "which function is slow", this
// answers "reached from where", and a canvas has the resolution for it: every
// frame gets a box no matter how thin, carrying its details in a tooltip.
#define SVG_WIDTH 1200.0
#define SVG_ROW_HEIGHT 18.0
#define SVG_MARGIN 12.0
#define SVG_MIN_LABEL 40.0 // narrower boxes than this get no text
// `<` and `&` can't appear in a Tomo identifier, but a file path is under the
// user's control, so escape both anyway rather than emitting broken XML.
static void svg_escape(FILE *out, const char *text) {
for (const char *c = text; *c; c++) {
if (*c == '<') fputs("<", out);
else if (*c == '>') fputs(">", out);
else if (*c == '&') fputs("&", out);
else if (*c == '"') fputs(""", out);
else fputc(*c, out);
}
}
// The classic flame-graph palette: a warm hue picked from the function's name,
// so the same function keeps its color across runs and neighbouring boxes are
// easy to tell apart.
static void svg_color(const char *name, char *buf, size_t size) {
uint32_t hash = 2166136261u;
for (const char *c = name; *c; c++)
hash = (hash ^ (uint8_t)*c) * 16777619u;
int red = 205 + (int)(hash % 50);
int green = 50 + (int)((hash >> 8) % 160);
int blue = 20 + (int)((hash >> 16) % 45);
snprintf(buf, size, "#%02x%02x%02x", red, green, blue);
}
static int svg_depth(tomo_profile_node_t *node) {
int deepest = 0;
for (tomo_profile_node_t *child = node->children; child; child = child->next_sibling) {
int depth = 1 + svg_depth(child);
if (depth > deepest) deepest = depth;
}
return deepest;
}
// Frames too narrow to be worth a rect (and their subtrees), counted so the
// image can say so rather than quietly leaving them out.
static int64_t svg_skipped = 0;
static void svg_count_subtree(tomo_profile_node_t *node) {
for (tomo_profile_node_t *child = node->children; child; child = child->next_sibling) {
svg_skipped += 1;
svg_count_subtree(child);
}
}
static void svg_write_children(FILE *out, tomo_profile_node_t *parent, int row, double x, double width, double total,
double root_total, double height) {
if (width <= 0.0 || total <= 0.0) return;
double pos = x;
for (tomo_profile_node_t *child = parent->children; child; child = child->next_sibling) {
double w = width * (tick_seconds(child->total_ticks) / total);
if (w < 0.15) { // thinner than a hairline: not drawable, not hoverable
svg_skipped += 1;
svg_count_subtree(child);
continue;
}
double y = height - SVG_MARGIN - (double)(row + 1) * SVG_ROW_HEIGHT;
char color[16], time[32];
svg_color(child->site->name, color, sizeof(color));
format_time(tick_seconds(child->total_ticks), time, sizeof(time));
double pct = root_total > 0.0 ? 100.0 * tick_seconds(child->total_ticks) / root_total : 0.0;
fputs("<g>", out);
fputs("<title>", out);
svg_escape(out, child->site->name);
fprintf(out, " — %s, %lld call%s, %.2f%%", time, (long long)child->calls, child->calls == 1 ? "" : "s",
pct);
if (child->site->file) {
fputs(" (", out);
svg_escape(out, short_path(child->site->file));
fprintf(out, ":%lld)", (long long)child->site->line);
}
fputs("</title>", out);
// The 1px gap between neighbours only makes sense once a box is wide
// enough to still be visible without it:
fprintf(out, "<rect x=\"%.2f\" y=\"%.2f\" width=\"%.2f\" height=\"%.2f\" fill=\"%s\"/>", pos, y,
w > 1.5 ? w - 1.0 : w, SVG_ROW_HEIGHT - 1.0, color);
if (w >= SVG_MIN_LABEL) {
// ~6.2px per character at this font size, minus a little padding:
int fits = (int)((w - 8.0) / 6.2);
char label[512];
snprintf(label, sizeof(label), "%s (%s)", child->site->name, time);
if (utf8_length(label) > fits) copy_truncated(label, sizeof(label), child->site->name, fits);
if (utf8_length(label) <= fits) {
fprintf(out, "<text x=\"%.2f\" y=\"%.2f\">", pos + 4.0, y + SVG_ROW_HEIGHT - 6.0);
svg_escape(out, label);
fputs("</text>", out);
}
}
fputs("</g>\n", out);
svg_write_children(out, child, row + 1, pos, w, tick_seconds(child->total_ticks), root_total, height);
pos += w;
}
}
static void write_svg_flame(FILE *out, double root_total) {
int rows = svg_depth(&call_tree_root);
double height = 2.0 * SVG_MARGIN + (double)rows * SVG_ROW_HEIGHT + 24.0;
char total[32];
format_time(root_total, total, sizeof(total));
fprintf(out,
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"%.0f\" height=\"%.0f\" "
"viewBox=\"0 0 %.0f %.0f\">\n"
"<style>text{font:11px ui-monospace,SFMono-Regular,Menlo,monospace;fill:#fff;"
"pointer-events:none}rect{stroke:#00000022}g:hover rect{stroke:#fff;stroke-width:1}</style>\n"
"<rect width=\"100%%\" height=\"100%%\" fill=\"#1b1b1b\"/>\n"
"<text x=\"%.0f\" y=\"%.0f\" style=\"font-size:13px;fill:#ddd\">flame graph — %s total "
"(hover a frame for details)</text>\n",
SVG_WIDTH, height, SVG_WIDTH, height, SVG_MARGIN, SVG_MARGIN + 10.0, total);
svg_skipped = 0;
svg_write_children(out, &call_tree_root, 0, SVG_MARGIN, SVG_WIDTH - 2.0 * SVG_MARGIN, root_total, root_total,
height);
if (svg_skipped > 0)
fprintf(out, "<text x=\"%.0f\" y=\"%.0f\" style=\"fill:#888\">%lld frame%s too narrow to draw</text>\n",
SVG_WIDTH - SVG_MARGIN - 220.0, SVG_MARGIN + 10.0, (long long)svg_skipped, svg_skipped == 1 ? "" : "s");
fputs("</svg>\n", out);
}
// Sum the call tree into the per-function totals the table shows. Doing it
// here rather than per call is what keeps tomo_profile_end() down to a few
// stores: a million calls cost a million increments, while this costs one walk
// over the distinct call paths.
static void aggregate_sites(tomo_profile_node_t *parent) {
for (tomo_profile_node_t *node = parent->children; node; node = node->next_sibling) {
tomo_profile_site_t *site = node->site;
if (!site->registered) {
site->registered = true;
site->next = sites;
sites = site;
}
site->calls += node->calls;
// Self time is what this path spent outside the callees it reached:
uint64_t callees = 0;
for (tomo_profile_node_t *child = node->children; child; child = child->next_sibling)
callees += child->total_ticks;
site->self_ticks += node->total_ticks > callees ? node->total_ticks - callees : 0;
// Inclusive time is only counted where the function isn't already
// somewhere above it on the stack: an outer call's time already covers
// the inner ones, so counting both would count it twice. (Calling
// itself directly doesn't even reach here, since those frames fold
// into the one node, but a mutually recursive cycle does.)
bool nested_in_itself = false;
for (tomo_profile_node_t *ancestor = node->parent; ancestor; ancestor = ancestor->parent)
if (ancestor->site == site) {
nested_in_itself = true;
break;
}
if (!nested_in_itself) site->total_ticks += node->total_ticks;
aggregate_sites(node);
}
}
public
void tomo_profile_report(void) {
if (!tomo_profiling_enabled || reported) return;
reported = true;
double elapsed = now() - profiling_started;
// Calibrate: the counter ran `ticks` times over `elapsed` seconds of real
// time. A run too short to measure keeps the nanosecond default, which is
// what the fallback path counts in anyway.
uint64_t ticks_elapsed = ticks() - ticks_started;
if (ticks_elapsed > 0 && elapsed > 0.0) seconds_per_tick = elapsed / (double)ticks_elapsed;
// The program can exit from inside a call, whether `fail()`, an explicit
// exit, or a fatal signal, leaving frames on the stack whose timers never
// stopped. Stop them now, so the functions the program died inside are
// reported with the time they had used, instead of showing up with zero
// calls. This has to happen before the totals are summed, since it is what
// puts those frames' time into the tree:
while (current_frame)
tomo_profile_end(current_frame);
aggregate_sites(&call_tree_root);
// Nothing to report: the program exited before any instrumented function
// ran (`--help`, a rejected argument). Instrumented programs profile every
// run, so those runs would otherwise each print an empty report and,
// with PROFILE_FILE or FLAME_GRAPH set, write an empty file.
if (sites == NULL) return;
// The root of the call tree accounts for every outermost call, which is
// what the flame graph's full width represents:
double root_total = 0.0;
for (tomo_profile_node_t *child = call_tree_root.children; child; child = child->next_sibling)
root_total += tick_seconds(child->total_ticks);
// FLAME_GRAPH asks for the graph as well as the table, so both come out of
// one run. A line on stderr says where it went, since a program that seems
// to print nothing has otherwise quietly written a file:
if (flame_path) {
bool to_stdout = streq(flame_path, "-");
FILE *svg = to_stdout ? stdout : fopen(flame_path, "w");
if (svg) {
write_svg_flame(svg, root_total);
if (to_stdout) fflush(svg);
else {
fclose(svg);
fprint(stderr, USE_COLOR ? "\x1b[2m" : "", "Wrote flame graph to ", flame_path,
USE_COLOR ? "\x1b[m" : "");
}
} else {
fprint(stderr, "\x1b[31;1mCouldn't open the flame graph file: ", flame_path, "\x1b[m");
}
}
FILE *out = stderr;
bool close_out = false;
if (report_path && strcmp(report_path, "-") == 0) {
out = stdout;
} else if (report_path) {
out = fopen(report_path, "w");
if (!out) {
fprint(stderr, "\x1b[31;1mCouldn't open the profile file: ", report_path, "\x1b[m");
return;
}
close_out = true;
}
// Color only makes sense on a terminal we already decided to colorize:
bool color = USE_COLOR && !close_out;
const char *dim = color ? "\x1b[2m" : "", *bold = color ? "\x1b[1m" : "", *reset = color ? "\x1b[m" : "";
size_t count = 0;
for (tomo_profile_site_t *site = sites; site; site = site->next)
count += 1;
tomo_profile_site_t **sorted = calloc(count, sizeof(tomo_profile_site_t *));
if (!sorted) {
if (close_out) fclose(out);
return;
}
size_t i = 0;
double instrumented = 0.0;
int64_t total_calls = 0;
for (tomo_profile_site_t *site = sites; site; site = site->next) {
sorted[i++] = site;
instrumented += tick_seconds(site->self_ticks);
total_calls += site->calls;
}
qsort(sorted, count, sizeof(sorted[0]), by_self_time);
fprintf(out, "%s───── profile ─────%s\n", dim, reset);
// Column widths match the row format below: the value fields print
// their own units, so the widths are 8/9/9/10/10 plus one space each:
fprintf(out, "%s%8s %9s %9s %10s %10s %s%s\n", bold, "self%", "self", "total", "calls", "avg self",
tomo_profile_noun, reset);
for (i = 0; i < count; i++) {
tomo_profile_site_t *site = sorted[i];
double self = tick_seconds(site->self_ticks);
double pct = elapsed > 0.0 ? 100.0 * self / elapsed : 0.0;
char avg[32];
double avg_seconds = site->calls > 0 ? self / (double)site->calls : 0.0;
fprintf(out, "%7.2f%% %7.2fms %7.2fms %10lld %10s %s", pct, 1e3 * self, 1e3 * tick_seconds(site->total_ticks),
(long long)site->calls, format_time(avg_seconds, avg, sizeof(avg)), site->name);
// Entries merged from another process (see tomo_profile_merge) carry a
// name but no source location:
if (site->file) fprintf(out, " %s(%s:%lld)%s", dim, short_path(site->file), (long long)site->line, reset);
fputc('\n', out);
}
fprintf(out, "%s───────────────────%s\n", dim, reset);
fprintf(out, "%s%d %s%s, %lld call%s, %.2fms in instrumented code, %.2fms total runtime%s\n", dim, (int)count,
tomo_profile_noun, count == 1 ? "" : "s", (long long)total_calls, total_calls == 1 ? "" : "s",
1e3 * instrumented, 1e3 * elapsed, reset);
free(sorted);
if (close_out) fclose(out);
else fflush(out);
}
|