Zprof is a zero-dependency memory profiler that wraps any allocator written in Zig. It minimizes overhead by compiling away any metric you don't enable β you pay only for what you measure. Tracks allocations, detects memory leaks, and logs memory changes with optional thread-safe mode.
Developed for use in Debug or official modes, it guarantees nearly the same performance as the wrapped allocator. Zprof's development is based on a primary priority: ease of use, improved efficiency, readability, clean, minimal, and well-documented code.
Run the test suite with:
zig build testRun the benchmark with:
zig build benchmarkBenchmarked with cpu=i7-12700H (all configs enabled)
Alloc/free benchmark [bytes=16 ops=10000000]
Raw allocator: tot=80274902ns
Zprof allocator: tot=102280564ns
Baseline overhead (Wrapper): +7.87%
Bookkeeping overhead: +19.54%
Total overhead: +27.41%
Add Zprof to your project's build.zig.zon:
.{
.name = "my-project",
.version = "4.0.0",
.dependencies = .{
.zprof = .{
.url = "https://github.com/ANDRVV/zprof/archive/v4.0.0.zip",
.hash = "...",
},
},
}Then in your build.zig, add:
const zprof_dep = b.dependency("zprof", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zprof", zprof_dep.module("zprof"));Else you can put zprof.zig in your project's path and import it.
Zig version 0.15.1 or newer is required to compile Zprof.
Here's how to use Zprof in three easy steps:
const std = @import("std");
const Zprof = @import("zprof.zig").Zprof;
pub fn main() !void {
// 1. Create a profiler by wrapping your allocator with a Config
var zprof: Zprof(.{}) = .init(std.heap.page_allocator, undefined);
// .{} uses the default config (thread_safe = false, all metrics enabled)
// 2. Use the profiler's allocator instead of your original one
const allocator = zprof.allocator();
// 3. Use the allocator as normal
const data = try allocator.alloc(u8, 1024);
defer allocator.free(data);
std.debug.print("Has leaks: {}\n", .{zprof.profiler.hasLeaks()});
}To start profiling memory usage, simply wrap your allocator with Zprof:
var zprof: Zprof(.{}) = .init(allocator, undefined); // .{} uses default config
const tracked_allocator = zprof.allocator();To use Zprof with mutex protection on the child allocator, enable thread-safe mode via Config:
var zprof: Zprof(.{ .thread_safe = true }) = .init(allocator, undefined);
const tracked_allocator = zprof.allocator();Logging is configured by providing a writerFn in the Config struct. The function receives the writer, a boolean indicating allocation or deallocation, and the size in bytes.
fn myLogger(writer: *std.Io.Writer, is_alloc: bool, size: usize) void {
writer.print("{s}={d};\n", .{ if (is_alloc) "alloc" else "free", size }) catch {};
}
var zprof: Zprof(.{ .writerFn = myLogger }) = .init(allocator, writer);
const tracked_allocator = zprof.allocator();
const data = try tracked_allocator.alloc(u8, 1024); // prints: alloc=1024;
tracked_allocator.free(data); // prints: free=1024;Zprof makes it easy to detect memory leaks in your application:
if (zprof.profiler.hasLeaks()) {
std.debug.print("Memory leak detected!\n", .{});
return error.MemoryLeak;
}Zprof accepts a comptime Config struct that lets you enable or disable individual metrics and features, reducing overhead for metrics you don't need:
pub const Config = struct {
thread_safe: bool = false,
writerFn: ?*const fn (*std.Io.Writer, bool, usize) void = null,
allocated: bool = true, // tracks total bytes allocated
freed: bool = true, // tracks total bytes deallocated
alloc_count: bool = true, // tracks number of allocations
free_count: bool = true, // tracks number of deallocations
peak_requested: bool = true, // tracks peak of requested bytes
live_requested: bool = true, // tracks non-freed requested bytes
};Example β only track live bytes and leaks, with thread safety:
var zprof: Zprof(.{
.thread_safe = true,
.allocated = false,
.freed = .false,
.alloc_count = false,
.free_count = false,
.peak_requested = false,
}) = .init(allocator, undefined);Access profiling data through zprof.profiler. Each metric is a Counter and exposes a .get() method.
| Field | Type | Description |
|---|---|---|
allocated |
Counter |
Total bytes allocated since initialization |
freed |
Counter |
Total bytes deallocated since initialization |
alloc_count |
Counter |
Number of allocation operations |
free_count |
Counter |
Number of deallocation operations |
peak_requested |
Counter |
Maximum memory usage at any point |
live_requested |
Counter |
Current memory usage |
std.debug.print("Allocated: {d}\n", .{zprof.profiler.allocated.get()});
std.debug.print("Freed: {d}\n", .{zprof.profiler.freed.get()});
std.debug.print("Live bytes: {d}\n", .{zprof.profiler.live_requested.get()});
std.debug.print("Peak: {d}\n", .{zprof.profiler.peak_requested.get()});
std.debug.print("Allocs: {d}\n", .{zprof.profiler.alloc_count.get()});
std.debug.print("Frees: {d}\n", .{zprof.profiler.free_count.get()});| Method | Return type | Description |
|---|---|---|
hasLeaks() |
bool |
Returns true if there are active memory leaks |
reset() |
void |
Resets all profiling statistics |
test "no memory leaks" {
var zprof: Zprof(.{}) = .init(std.testing.allocator, undefined);
const allocator = zprof.allocator();
const data = try allocator.alloc(u8, 1024);
defer allocator.free(data);
try std.testing.expect(!zprof.profiler.hasLeaks());
}Made with β€οΈ for the Zig community
Copyright (c) 2026 Andrea Vaccaro