commit ea7ea2bdedb2a918da1550a32e7362d9c21e3706
parent c62703fb3078209bc4464af2d062c2623810fd48
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 15 Jun 2026 18:00:45 -0700
driver: opt-in heap allocation counters via KIT_METRICS=1
Wire heap_libc_{alloc,realloc,free} to a KitProfiler whose counters occupy the
embedder external-id range, leaving kit-core's counter enum untouched. When
KIT_METRICS is unset, the path is a single NULL check with zero overhead. At
exit, prints:
kit heap metrics:
heap.allocs=… heap.alloc_kib=… heap.large_allocs=…
heap.reallocs=… heap.realloc_moves=… heap.frees=…
Large allocs are those >=32 KiB (~one default arena block). The KitProfiler is
allocated via raw calloc so it does not count itself and there is no reentrancy
through the vtable. Document the recipe in PERF.md §6 and update §5 item 5 to
reflect the arena churn work landed.
Diffstat:
| M | doc/plan/PERF.md | | | 163 | +++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------- |
| M | driver/env/common.c | | | 94 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- |
2 files changed, 200 insertions(+), 57 deletions(-)
diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md
@@ -30,30 +30,35 @@ release kit at `build/release/kit` (`make bin RELEASE=1`).
## 1. Current standings
-### Compile speed — the open frontier (~2.7× tcc)
+### Compile speed — the open frontier (~2.6× tcc)
Apple-silicon arm64 / Darwin 25.3, refreshed 2026-06-15. **`instructions` is the
metric to trust** — load-independent (`/usr/bin/time -l`, best-of-7);
cycles/wall are low-load readings shown only for context. The kit row is **after
-the CG-type-interaction pass** (see §3); the object is byte-identical to the
-pre-pass build (1.70 MB unchanged), only the compiler's own instruction count moved.
+the CG-type-interaction pass + the arena geometric/retain rework** (see §3). Each
+perf step left the sqlite object byte-identical, but the object has grown to
+1.75 MB (from ~1.70 MB) because aa64 codegen *correctness* fixes landed in the
+same window — the far-slot address-build fallback and the aggregate-return place
+fix — which add a fixed per-function reserved patch region (see *Code size* below).
| compiler | instructions | cycles † | wall † | object |
|---|--:|--:|--:|--:|
-| **tcc 0.9.28rc** | 0.662 B | 0.193 B | 0.06 s | 2.11 MB |
-| **kit** | 1.760 B | 0.578 B | 0.18 s | 1.70 MB |
-| Apple clang 21 | 8.818 B | 2.799 B | 0.91 s | 1.50 MB |
+| **tcc 0.9.28rc** | 0.663 B | 0.190 B | 0.06 s | 2.11 MB |
+| **kit** | 1.744 B | 0.522 B | 0.16 s | 1.75 MB |
+| Apple clang 21 | 8.824 B | 2.775 B | 0.88 s | 1.50 MB |
† low-load; instructions is the figure to trust. kit instructions over the
CG-type-interaction pass: **1.939 B → 1.864 B** (slot-query dedup + memory-align
contract + builtin ABI-layout cache + resolve/predicate fast paths) **→ 1.760 B**
(`abi_cg_type_info` inline builtin fast path + by-pointer layout cache) =
-**−179 M total (−9.2 %)**, all byte-identical (the sqlite object is bit-for-bit
-unchanged).
+**−179 M total (−9.2 %)**; then the arena geometric-block/retain-on-reset rework
++ the delayed-arena block-size fix shaved another **−16 M → 1.744 B**, all
+byte-identical (each perf step left the sqlite object bit-for-bit unchanged — the
+object's growth to 1.75 MB is the separate codegen correctness fixes noted above).
-kit beats clang and is the fastest *general* backend, but **tcc is ~2.7×
+kit beats clang and is the fastest *general* backend, but **tcc is ~2.6×
(instructions) ahead** — this is the whole game. **The gap is instructions, not
-cache:** kit's IPC (~3.4) is on par with tcc's (~3.4), so both are compute-bound
+cache:** kit's IPC (~3.3) is on par with tcc's (~3.5), so both are compute-bound
on a wide core and there is no hidden cache-miss penalty to claw back. The payoff
mechanism is **fewer retired instructions** (fewer copies, indirect calls,
redundant recomputations) — denser structures help only because they cost fewer
@@ -76,37 +81,45 @@ load/store/move *instructions*.
> (per-statement loc lookups the old eager-on-token loc got for free) is the
> optional Stage-2 target in §4.1.
-### Code size — below tcc (0.951×)
+### Code size — below tcc (0.986×)
The honest metric is **`.text` machine code** (the object file is format-skewed —
Mach-O vs tcc's ELF — and not comparable; kit's *object* is actually smaller).
| metric | kit | tcc | ratio |
|---|--:|--:|--:|
-| **`.text`** | **1,303,352 B / 325,838 4-byte slots** | **1,370,940 B / 342,735 4-byte slots** | **0.951×** (−16,897 slots) |
+| **`.text`** | **1,352,356 B / 338,089 4-byte slots** | **1,370,940 B / 342,735 4-byte slots** | **0.986×** (−4,646 slots) |
-kit now clears the code-size bar overall. The remaining local excess is still in
-the same instruction families (per-mnemonic, kit − tcc on sqlite):
+kit still clears the code-size bar overall, but the margin shrank from 0.951×:
+the recent aa64 far-slot/large-frame fix reserves a **branched-over 5-instruction
+patch region** in every qualifying function's prologue (a `b` over five `nop`s
+when the long address-build path is unused), and ~2,587 functions pay it. That
+alone adds **+12,897 `nop`s (~52 KB)** — i.e. the *entire* `.text` growth is this
+padding; net of nops, real instructions actually *shrank* slightly. The genuine
+excess remains the same instruction families as before (per-mnemonic, kit − tcc):
| heavier in kit | Δ | what it is |
|---|--:|---|
| `stur`+`ldur`+`str` (spills) | **+47,095** | **#1: register pressure** — the single-pass NDT spills more than tcc keeps resident across statements / control-flow joins |
| `mov` | **+13,666** | residual arg / value-stack copies the arg0-first placement can't reach (nested-call results already in x0, pressure spills) |
+| `nop` | **+12,897** | **new** — branched-over 5-`nop` patch region reserved per qualifying function by the far-slot/large-frame fallback; likely over-reserved (see §4.2) |
| `sub`+`movk` | +3,474 | residual far-frame addressing (byte/half slots, `&local` in big frames) |
…offset by where **kit already beats tcc** (structural wins — do not touch):
-`ldr` −24,214 (far slots fold into `[sp,#scaled]`), `add` −20,007 (folds offsets
+`ldr` −24,214 (far slots fold into `[sp,#scaled]`), `add` −20,899 (folds offsets
into displacements), `cset` −17,226 / `cbnz` −10,817 / `cmp` −9,421 (kit fuses
-compares into `cbz`/`b.cc`; tcc materializes a bool then tests), `movn` −6,720 /
-`movz` −4,644 / `stp` −3,722, and `nop` −246. The mnemonic histogram nets
-−17,309 decoded instructions; kit also has 412 4-byte padding slots, so the byte
-metric nets −16,897 slots.
+compares straight into `cbz`/`b.cc`; tcc materializes a bool then tests — the cost
+side is `cbz` +6,294 and the `b.eq`/`b.ne`/`b.ge`/`b.le`/`b.lt` family +6,936,
+still a large net win), `movn` −6,720 / `movz` −4,644 / `stp` −3,722. The mnemonic
+histogram nets **−5,058 decoded instructions**; kit also has 412 4-byte padding
+slots, so the byte metric nets **−4,646 slots** — the +12,897 reserved-patch
+`nop`s are what eroded the old −16,897.
---
## 2. Where the cost is (current profile)
-### Compile is frontend-bound — lex+pp ~2.0× tcc, post-PP ~4.3× tcc
+### Compile is frontend-bound — lex+pp ~2.0× tcc, post-PP ~3.6× tcc
Phase split measured directly (instructions, best-of-7), not estimated from `-E`.
`-E` is a **bad** lex+pp proxy: it re-serializes the token stream to text, work
@@ -120,15 +133,15 @@ compilers (tcc: `-bench` hook, patched to use `-c` `parse_flags`; kit:
| phase | kit `-c` | tcc `-c` | kit / tcc |
|---|--:|--:|--:|
-| **lex + pp** | **0.82 B** | **0.40 B** | **~2.0×** |
-| parse + sema + types + CG-drive | 0.86 B | ┐ 0.26 B | — |
-| native emit + object write | 0.26 B | ┘ (post-PP) | — |
-| **post-PP total** | **1.12 B** | **0.26 B** | **~4.3×** |
-| **total** | **1.94 B** | **0.66 B** | **~2.9×** |
+| **lex + pp** | **0.80 B** | **0.40 B** | **~2.0×** |
+| parse + sema + types + CG-drive | 0.71 B | ┐ 0.26 B | — |
+| native emit + object write | 0.24 B | ┘ (post-PP) | — |
+| **post-PP total** | **0.94 B** | **0.26 B** | **~3.6×** |
+| **total** | **1.74 B** | **0.66 B** | **~2.6×** |
So **both halves are real frontiers** — lex+pp is *not* at parity (an earlier
claim from the misleading `-E` proxy was wrong). The post-PP ratio is larger, but
-lex+pp is ~42 % of kit's `-c` and a clean 2.0× behind. Note kit's lex+pp drain
+lex+pp is ~46 % of kit's `-c` and a clean 2.0× behind. Note kit's lex+pp drain
does *less* than tcc's — kit defers number/string decode to the parser (tcc decodes
in the lexer; bare tcc scan+expand is 0.38 B, +decode 0.40 B) — yet is still ~2.0×
heavier, so the gap is pure per-token engine overhead, not extra work:
@@ -151,7 +164,7 @@ heavier, so the gap is pure per-token engine overhead, not extra work:
state per token. This is the [[frontend-instruction-halving-pathb]] lever: fewer
per-token ops, not less work.
-The post-PP 4.3× is the bigger slice: tcc drives a thin `SValue[]` straight into a
+The post-PP 3.6× is the bigger slice: tcc drives a thin `SValue[]` straight into a
one-pass emitter, while kit routes through the `CgTarget`→`NativeTarget`→`MCEmitter`
seam and a richer type/ABI layer (§4). Closing either is a campaign, not one hot
function.
@@ -163,28 +176,30 @@ cursor lookup at `pcg_set_loc` the old eager-on-token loc got for free), emit
flat → **−24 M total (−1.3 %)**.
**Linux callgrind** (inclusive, instruction-grounded — the tool that sees what
-wall-clock `sample` hides; total **1.566 B `Ir`**, glibc/ELF; self-`Ir` summed
+wall-clock `sample` hides; total **1.547 B `Ir`**, glibc/ELF; self-`Ir` summed
across callgrind's `'2` symbol splits). This is **after the CG-type-interaction
pass** (slot-query dedup + memory-align contract + builtin ABI-layout cache +
-resolve/predicate fast paths + the `api_type_layout_get` follow-up): the type
-cluster went **1.741 B → 1.608 B → 1.566 B (−175 M, −10.1 %)**, all
-byte-identical (perf-identity gate green on every non-`-g` category, and the
-sqlite object is bit-for-bit unchanged; the `-g` delta is the separate
-debug-for-globals feature).
+resolve/predicate fast paths + the `api_type_layout_get` follow-up) **and the
+arena geometric/retain rework**: the type cluster went **1.741 B → 1.608 B →
+1.566 B (−175 M, −10.1 %)**, then the arena change shaved heap/`memset` churn to
+**1.547 B (−19 M)**, all byte-identical for the perf steps (perf-identity gate
+green on every non-`-g` category; the Linux/ELF total is not comparable to the
+macOS hardware count — different libc/format — but the *distribution* is the point).
| self % | function(s) | subsystem |
|--:|---|---|
| **12.3** | `lex_next` | scanner |
-| 6.3 | `malloc` | hosted heap |
-| 4.7 / 4.3 | `src_next_raw_into` / `pool_intern_slice` | pp + interning |
-| 4.1 | `pp_pull_into` | preprocessor |
-| 2.7 | `finish_ident` | scanner |
-| 1.9 / 1.6 / 0.3 | `__GI_memset` / `__GI_memchr` / `__GI_memcpy` | libc memory |
+| 6.4 | `malloc` | hosted heap |
+| 4.5 / 4.4 | `src_next_raw_into` / `pool_intern_slice` | pp + interning |
+| 3.9 | `pp_pull_into` | preprocessor |
+| 2.8 | `finish_ident` | scanner |
+| 2.0 / 1.6 / 0.3 | `__GI_memset` / `__GI_memchr` / `__GI_memcpy` | libc memory |
| 1.9 / 1.8 | `abi_cg_type_info` / `api_type_pred_bits` | types/ABI |
-| 1.1 / 1.1 / 1.0 | `api_const_from_sv` / `pp_materialize_loc` / `api_sv_adjust_refs` | value-stack / lazy-loc |
-| 1.1 / 1.0 / 0.97 / 0.4 | `cg_type_get` / `api_type_layout_ref` / `api_type_class` / `resolve_type` | types |
-| 1.0 | `aa_emit_mem` | codegen |
-| 0.75 (Σ) | `kit_cg_slot_lang_type` 0.52 / `kit_cg_slot_lang_flags` 0.15 / `kit_cg_slot_cg_type` 0.09 | CG slot queries (zero-copy accessors) |
+| 1.6 / 1.3 / 1.2 | `pp_materialize_loc` / `api_const_from_sv` / `api_sv_adjust_refs` | lazy-loc / value-stack |
+| 1.2 | `arena_alloc` | arena bump path |
+| 1.1 / 1.0 / 1.0 / 0.4 | `cg_type_get` / `api_type_layout_ref` / `api_type_class` / `resolve_type` | types |
+| 1.1 | `aa_emit_mem` | codegen |
+| 0.78 (Σ) | `kit_cg_slot_lang_type` 0.53 / `kit_cg_slot_lang_flags` 0.16 / `kit_cg_slot_cg_type` 0.09 | CG slot queries (zero-copy accessors) |
**What moved (vs the pre-pass profile above each arrow):**
`kit_cg_slot_info` **3.2 % → gone** (the by-value struct copy is retired; the C
@@ -205,12 +220,31 @@ leaves: **15.0 % → ~9 %**.
**Subsystem rollup:** scanner ~15 % (`lex_next`+`finish_ident`), pp+interning
~13 % (`src_next_raw_into`+`pp_pull_into`+`pool_intern_slice`), CG/types/ABI
metadata ~9 % (down from ~15 %), libc allocation/memory helpers ~10 %, lazy-loc
-(`pp_materialize_loc`) ~1 %, direct codegen emit ~1 %. The type layer is no longer
-a top-tier frontier — the remaining leaves (`abi_cg_type_info` register-struct
-returns, `api_type_pred_bits`, `cg_type_get`) are already at/under ~2 % and were
-explicitly *not* micro-optimized. The next frontiers are the scanner (`lex_next`
-~15 %) and the hosted heap (`malloc` 6 %). Trust callgrind for *where*; trust
-macOS instructions for *how much*.
+(`pp_materialize_loc`) ~1.6 %, arena bump path (`arena_alloc`) ~1.2 %, direct
+codegen emit ~1 %. The type layer is no longer a top-tier frontier — the remaining
+leaves (`abi_cg_type_info` register-struct returns, `api_type_pred_bits`,
+`cg_type_get`) are already at/under ~2 % and were explicitly *not* micro-optimized.
+The next frontiers are the scanner (`lex_next` ~15 %) and the hosted heap (`malloc`
+~6 %, call frequency already addressed by the arena rework — see below). Trust
+callgrind for *where*; trust macOS instructions for *how much*.
+
+**Allocator (hosted heap).** `malloc` is ~6 % of `Ir` but only **~4,278 calls**
+for the whole sqlite TU (`KIT_METRICS=1` heap counters): the cost is in large,
+churned blocks, **not** call frequency. Most allocation never reaches the heap —
+arenas bump-allocate, and the variable-count structures (interner table/entries,
+vectors, segvecs) already grow geometrically (**351 reallocs total**, all grows).
+The one gap was **fixed-size 64 KiB arena blocks**. The arena now (a) grows
+blocks **geometrically** (64 KiB doubling to a 1 MiB cap, so a large arena needs
+O(log n) heap calls not O(n)) and (b) `arena_reset` **retains the high-water
+blocks** — it rewinds the bump cursor and frees nothing; only `arena_fini`
+returns memory, so reset/refill cycles (per-statement fold, per-function MC,
+per-expansion pp scratch) reuse their blocks with zero heap traffic. Effect on
+the sqlite compile (`KIT_METRICS`): large (≥32 KiB) block allocs **440 → 124
+(−72 %)**, total allocs 4,278 → 3,967, frees 4,571 → 4,260; **macOS instructions
+−0.22 %, byte-identical** (isolated golden-vs-candidate). Small on the
+instruction metric (Apple malloc is cheap) but it corrects the reset-vs-free
+semantics and cuts heap pressure/fragmentation. Heap counters reuse the
+`KitProfiler` machinery (embedder counter range), opt-in via `KIT_METRICS=1`.
### Code size is still locally spill-bound
@@ -302,6 +336,15 @@ makes `--inclusive=yes` double-count to absurd numbers). The Linux/ELF total is
not comparable to the macOS/Mach-O hardware figure (different libc, sysroot,
format; counts glibc + loader + the `-lc` probe) — the *distribution* is the point.
+**Heap allocation counts** — `KIT_METRICS=1` prints the hosted-heap counters
+(allocs / large allocs ≥32 KiB / reallocs / frees) at exit; the counts are
+host-independent (kit issues the same `h->alloc` calls everywhere):
+
+```sh
+KIT_METRICS=1 build/release/kit cc -c sqlite3.c --sysroot "$SDK" -o /tmp/k.o
+# -> kit heap metrics: heap.allocs=… heap.large_allocs=… heap.reallocs=… heap.frees=…
+```
+
**Code size** (`.text` machine code, the honest metric):
```sh
@@ -416,11 +459,14 @@ as a small follow-on); #5 ○ open.
sentinel array (`94e642e3`). *Open (Cut C):* the dead paste file-id
registration could become a bare `nfiles++` after auditing no diagnostic ever
queries a paste file-id.
-5. ○ **[OPEN]** **`memset` / arena churn (~2–3 %).** Right-size per-expression / per-emit struct
- zeroing (designated-init the per-op clears); audit per-statement/per-temp arena
- allocation vs reuse. (`memset` here is explicit zero-init, **not**
- `-ftrivial-auto-var-init` — proven by rebuilding with the flag off; attack call
- sites, keep the hardening flag.)
+5. ◑ **[arena churn LANDED; memset OPEN]** **`memset` / arena churn (~2–3 %).**
+ Arena churn done: blocks now grow geometrically and `arena_reset` retains the
+ high-water capacity instead of freeing all-but-head (large block allocs
+ **−72 %**, byte-identical) — see §2 *Allocator*. *Still open:* right-size
+ per-expression / per-emit struct zeroing (designated-init the per-op clears).
+ (`memset` here is explicit zero-init, **not** `-ftrivial-auto-var-init` —
+ proven by rebuilding with the flag off; attack call sites, keep the hardening
+ flag.)
Near-dead-ends for *instructions* (revisit only under a cache-stall study, not
expected to pay): the `pool_intern_slice` probe/insert side (the self-sufficient
@@ -429,6 +475,17 @@ optimal); `aa_emit_mem` typed-store micro-levers.
### 4.2 Code size (run-correctness-gated; multiplicative — each byte cut shrinks emit + objwrite + assemble together)
+0. **Trim the over-reserved far-slot/large-frame patch region (new, +12,897 `nop`
+ / ~52 KB — the whole 0.951×→0.986× erosion).** The aa64 far-slot address-build
+ / large-frame fallback now reserves a fixed **branched-over 5-instruction patch
+ region** in ~2,587 functions' prologues (a `b` over five `nop`s when the long
+ path is unused), so every qualifying function pays 24 B of padding even when
+ the short path suffices. This is correctness-first and *probably* over-reserved:
+ reserve only when the frame/slot actually needs the long form, or shrink the
+ reservation to the real worst case (and patch the branch away when unused).
+ Investigate before touching spills — it's likely the cheapest byte win on the
+ board. Gate: run-correctness + determinism (this is the codegen path the recent
+ correctness fixes added).
1. **Spill reduction — the #1 remaining local code-size excess (+47 K).** The
single-pass NDT spills more than tcc keeps resident across statements and
control-flow joins. The no-new-analysis wins (dead-operand drop,
diff --git a/driver/env/common.c b/driver/env/common.c
@@ -2,6 +2,7 @@
* stderr diag sink, stdout/fd writers, and the small printf/errf/alloc
* helpers that route through stdio + malloc. Compiled on every host. */
+#include <kit/profile.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
@@ -12,23 +13,108 @@
/* ---------------- heap (libc-backed) ---------------- */
+/* Heap allocator metrics, reusing the KitProfiler counter machinery. The heap
+ * is the process-wide allocation chokepoint (arenas and pools all bottom out
+ * here), so it counts into the profiler pointed to by KitHeap.user — driver_env
+ * wires that to a profiler when KIT_METRICS is set, leaving it NULL (one
+ * branch, no work) otherwise. Counters live in the embedder-owned external id
+ * range so the kit-core counter enum stays untouched; names are attached once
+ * via kit_profiler_define_counter and read back by the generic counter dump.
+ * The ~32 KiB threshold approximates an arena block (default block is 64 KiB),
+ * the dominant large-allocation source. */
+enum {
+ HEAP_C_ALLOCS = KIT_PROFILE_COUNTER_EXTERNAL_FIRST,
+ HEAP_C_ALLOC_KIB,
+ HEAP_C_LARGE_ALLOCS, /* >= 32 KiB, ~arena blocks */
+ HEAP_C_REALLOCS, /* grows (the only realloc kit issues) */
+ HEAP_C_REALLOC_MOVES,
+ HEAP_C_FREES,
+};
+
+static void driver_heap_metrics_define(KitProfiler* pr) {
+ if (!pr) return;
+ kit_profiler_define_counter(pr, (KitProfileCounter)HEAP_C_ALLOCS,
+ "heap.allocs");
+ kit_profiler_define_counter(pr, (KitProfileCounter)HEAP_C_ALLOC_KIB,
+ "heap.alloc_kib");
+ kit_profiler_define_counter(pr, (KitProfileCounter)HEAP_C_LARGE_ALLOCS,
+ "heap.large_allocs");
+ kit_profiler_define_counter(pr, (KitProfileCounter)HEAP_C_REALLOCS,
+ "heap.reallocs");
+ kit_profiler_define_counter(pr, (KitProfileCounter)HEAP_C_REALLOC_MOVES,
+ "heap.realloc_moves");
+ kit_profiler_define_counter(pr, (KitProfileCounter)HEAP_C_FREES,
+ "heap.frees");
+}
+
+/* Process-wide heap metrics, opt-in via KIT_METRICS. The heap is global, so its
+ * stats are too; this stays NULL/inert unless asked. When an external profiler
+ * is already attached (e.g. a future `kit run --metrics` pointing KitHeap.user
+ * at its per-run profiler), we leave it alone and merge into that instead. */
+static KitProfiler* g_heap_metrics_prof;
+static int g_heap_metrics_inited;
+
+static void driver_heap_metrics_dump(void) {
+ KitProfiler* pr = g_heap_metrics_prof;
+ uint32_t id;
+ if (!pr) return;
+ fprintf(stderr, "kit heap metrics:\n");
+ for (id = KIT_PROFILE_COUNTER_EXTERNAL_FIRST; id <= (uint32_t)HEAP_C_FREES;
+ ++id) {
+ const char* name = kit_profiler_counter_name(pr, (KitProfileCounter)id);
+ fprintf(stderr, " %s=%llu\n", name ? name : "heap.?",
+ (unsigned long long)kit_profiler_counter_value(
+ pr, (KitProfileCounter)id));
+ }
+}
+
+static void driver_heap_metrics_maybe_begin(KitHeap* h) {
+ const char* e;
+ if (g_heap_metrics_inited) return;
+ g_heap_metrics_inited = 1;
+ if (!h || h->user) return; /* already wired to an external profiler */
+ e = getenv("KIT_METRICS");
+ if (!(e && e[0] && e[0] != '0')) return;
+ /* Raw libc alloc (not through the vtable) so the profiler storage itself is
+ * not counted and there is no reentrancy. */
+ g_heap_metrics_prof = (KitProfiler*)calloc(1, sizeof(*g_heap_metrics_prof));
+ if (!g_heap_metrics_prof) return;
+ driver_heap_metrics_define(g_heap_metrics_prof);
+ h->user = g_heap_metrics_prof;
+ atexit(driver_heap_metrics_dump);
+}
+
static void* heap_libc_alloc(KitHeap* h, size_t size, size_t align) {
- (void)h;
+ KitProfiler* pr;
(void)align; /* malloc satisfies all max_align_t alignments */
+ if (!g_heap_metrics_inited) driver_heap_metrics_maybe_begin(h);
+ pr = h ? (KitProfiler*)h->user : NULL;
+ if (pr && size) {
+ kit_profiler_count(pr, (KitProfileCounter)HEAP_C_ALLOCS, 1);
+ kit_profiler_count(pr, (KitProfileCounter)HEAP_C_ALLOC_KIB, size >> 10);
+ if (size >= 32u * 1024u)
+ kit_profiler_count(pr, (KitProfileCounter)HEAP_C_LARGE_ALLOCS, 1);
+ }
return size ? malloc(size) : NULL;
}
static void* heap_libc_realloc(KitHeap* h, void* p, size_t old_size,
size_t new_size, size_t align) {
- (void)h;
+ KitProfiler* pr = h ? (KitProfiler*)h->user : NULL;
+ void* np;
(void)old_size;
(void)align;
- return realloc(p, new_size);
+ if (pr) kit_profiler_count(pr, (KitProfileCounter)HEAP_C_REALLOCS, 1);
+ np = realloc(p, new_size);
+ if (pr && p && np && np != p)
+ kit_profiler_count(pr, (KitProfileCounter)HEAP_C_REALLOC_MOVES, 1);
+ return np;
}
static void heap_libc_free(KitHeap* h, void* p, size_t size) {
- (void)h;
+ KitProfiler* pr = h ? (KitProfiler*)h->user : NULL;
(void)size;
+ if (pr && p) kit_profiler_count(pr, (KitProfileCounter)HEAP_C_FREES, 1);
free(p);
}