Backlog/engine/core/src/MemoryTracker.zig

343 lines
12 KiB
Zig

// memory tracker
//
// todo.. rename this to memory.zig
const std = @import("std");
const core = @import("core.zig");
const builtin = @import("builtin");
lock: std.Thread.Mutex = .{},
backingAllocator: std.mem.Allocator,
allocationsCount: u32 = 0,
totalAllocSize: usize = 0,
eventsCount: usize = 0,
untrackedAllocationsCount: u32 = 0,
untrackedAllocationsSize: usize = 0,
peakAllocations: u32 = 0,
peakAllocSize: usize = 0,
stackCompactor: ?*core.StackCompactor = undefined,
timeline: ?EventTimeline,
//const EventTimeline = core.algorithm.PagedVector(AllocEvent);
const EventTimeline = struct {
timelineAllocator: std.mem.Allocator,
events: core.algorithm.PagedVector(AllocEvent),
timestamps: core.algorithm.PagedVector(f64), // timestamp in milliseconds
pub fn init(timelineAllocator: std.mem.Allocator) !@This() {
return .{
.timelineAllocator = timelineAllocator,
.events = try core.algorithm.PagedVector(AllocEvent).init(timelineAllocator),
.timestamps = try core.algorithm.PagedVector(f64).init(timelineAllocator),
};
}
pub inline fn pushAlloc(self: *@This(), compactor: *core.StackCompactor, ptr: usize, size: usize) void {
const callstackId = compactor.getCallStack();
self.timestamps.append(core.getEngineTime()) catch unreachable;
self.events.append(.{
.callstackId = callstackId,
.address = ptr,
.event = .{ .alloc = .{ .size = size } },
}) catch unreachable;
}
pub inline fn pushResize(self: *@This(), compactor: *core.StackCompactor, ptr: usize, old_len: usize, new_len: usize) void {
const callstackId = compactor.getCallStack();
self.timestamps.append(core.getEngineTime()) catch unreachable;
self.events.append(.{
.callstackId = callstackId,
.address = ptr,
.event = .{ .resize = .{ .oldSize = old_len, .newSize = new_len } },
}) catch unreachable;
}
pub inline fn pushFree(self: *@This(), compactor: *core.StackCompactor, ptr: usize, size: usize) void {
const callstackId = compactor.getCallStack();
self.timestamps.append(core.getEngineTime()) catch unreachable;
self.events.append(.{
.callstackId = callstackId,
.address = ptr,
.event = .{ .free = .{ .size = size } },
}) catch unreachable;
}
};
// warning, due to laziness this uses timelineAllocator, which will leak
pub fn dumpTimeline(filename: []const u8) !void {
if (MTGet()) |tracker| {
core.engine_log("dumping timeline to file {s}", .{filename});
tracker.lock.lock();
defer tracker.lock.unlock();
if (tracker.stackCompactor) |compactor| {
if (tracker.timeline) |timeline| {
core.engine_log("2 {s}", .{filename});
const cwd = std.fs.cwd();
const ofile = try std.fmt.allocPrint(timeline.timelineAllocator, core.DefaultSavePath ++ "/{s}", .{filename});
defer timeline.timelineAllocator.free(ofile);
core.engine_log("1 {s}", .{filename});
var obuf = std.ArrayList(u8){};
defer obuf.deinit(timeline.timelineAllocator);
var i: usize = 0;
while (i < timeline.events.len()) : (i += 1) {
const event = timeline.events.get(i);
const timestamp = timeline.timestamps.get(i);
try obuf.print(timeline.timelineAllocator, "{d}: callstack: {x} @0x{x} ", .{
timestamp.*,
event.callstackId,
event.address,
});
switch (event.event) {
.alloc => |x| {
try obuf.print(timeline.timelineAllocator, "alloc {d} bytes\n", .{x.size});
},
.free => |x| {
try obuf.print(timeline.timelineAllocator, "free {d} bytes\n", .{x.size});
},
.resize => |x| {
try obuf.print(timeline.timelineAllocator, "resize {d} -> {d} bytes\n", .{ x.oldSize, x.newSize });
},
}
}
var iter = compactor.stackMap.iterator();
while (iter.next()) |x| {
const callstackId = x.key_ptr.*;
const stack = x.value_ptr.*;
try obuf.print(timeline.timelineAllocator, "{x}:\n", .{callstackId});
for (stack.debugStr) |frame| {
if (frame) |f| {
try obuf.print(timeline.timelineAllocator, "{s}\n", .{f});
} else {
try obuf.print(timeline.timelineAllocator, "no file\n", .{});
}
}
}
core.engine_log("3 {s}", .{filename});
try cwd.makePath(core.DefaultSavePath);
try cwd.writeFile(.{
.sub_path = ofile,
.data = obuf.items,
});
core.engine_log("finished writing", .{});
} else {
core.engine_log("timeline missing", .{});
}
} else {
core.engine_logs("skipping writing memory timeline as compactor is not enabled");
}
} else {
core.engine_logs("skipping writing memory timeline as tracker is not enabled");
}
}
const AllocEventType = enum { alloc, free, resize };
const AllocEvent = struct {
callstackId: u32,
address: usize,
event: union(AllocEventType) {
alloc: struct { size: usize },
free: struct { size: usize },
resize: struct { oldSize: usize, newSize: usize },
},
};
pub var vtable: std.mem.Allocator.VTable = .{
.alloc = alloc,
.free = free,
.resize = resize,
.remap = std.mem.Allocator.noRemap,
};
pub fn init(backingAllocator: std.mem.Allocator, settings: SetupSettings) @This() {
// use the ansi allocator
const EnableMemoryTimeline = settings.timeline and builtin.os.tag == .windows;
if (EnableMemoryTimeline) {
core.engine_logs("[dmt] Enabling Detailed Memory Tracking");
}
const stackCompactor = if (EnableMemoryTimeline) core.StackCompactor.create(std.heap.c_allocator) catch null else null;
return .{
.backingAllocator = backingAllocator,
.stackCompactor = stackCompactor,
.timeline = if (EnableMemoryTimeline) EventTimeline.init(std.heap.c_allocator) catch null else null,
};
}
pub fn allocator(self: *@This()) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &vtable,
};
}
pub fn alloc(ctx: *anyopaque, len: usize, ptr_align: std.mem.Alignment, ret_addr: usize) ?[*]u8 {
var self: *@This() = @ptrCast(@alignCast(ctx));
const rv = self.backingAllocator.vtable.alloc(self.backingAllocator.ptr, len, ptr_align, ret_addr);
{
self.lock.lock();
defer self.lock.unlock();
self.allocationsCount += 1;
self.totalAllocSize += len;
self.eventsCount += 1;
if (self.totalAllocSize > self.peakAllocSize) {
self.peakAllocSize = self.totalAllocSize;
}
if (self.allocationsCount > self.peakAllocations) {
self.peakAllocations = self.allocationsCount;
}
if (self.timeline) |*timeline| {
timeline.pushAlloc(self.stackCompactor.?, @intFromPtr(rv), len);
}
}
return rv;
}
pub fn resize(ctx: *anyopaque, buf: []u8, buf_align: std.mem.Alignment, new_len: usize, ret_addr: usize) bool {
var self: *@This() = @ptrCast(@alignCast(ctx));
{
self.lock.lock();
defer self.lock.unlock();
self.totalAllocSize = self.totalAllocSize - buf.len + new_len;
self.eventsCount += 1;
if (self.timeline) |*timeline| {
timeline.pushResize(self.stackCompactor.?, @intFromPtr(buf.ptr), buf.len, new_len);
}
}
return self.backingAllocator.vtable.resize(
self.backingAllocator.ptr,
buf,
buf_align,
new_len,
ret_addr,
);
}
pub fn free(ctx: *anyopaque, buf: []u8, buf_align: std.mem.Alignment, ret_addr: usize) void {
var self: *@This() = @ptrCast(@alignCast(ctx));
{
self.lock.lock();
defer self.lock.unlock();
self.allocationsCount -= 1;
self.totalAllocSize -= buf.len;
self.eventsCount += 1;
if (self.timeline) |*timeline| {
timeline.pushFree(self.stackCompactor.?, @intFromPtr(buf.ptr), buf.len);
}
}
self.backingAllocator.vtable.free(self.backingAllocator.ptr, buf, buf_align, ret_addr);
}
pub fn printStats(self: @This()) void {
std.debug.print("allocations: {d}\n", .{self.allocationsCount});
std.debug.print("memory committed: {d}\n", .{self.totalAllocSize});
}
pub fn deinit(self: *@This()) void {
_ = self;
}
pub fn addUntrackedAllocation(self: *@This(), allocatedSize: usize) void {
self.lock.lock();
self.untrackedAllocationsCount += 1;
self.untrackedAllocationsSize += allocatedSize;
if (self.timeline) |*timeline| {
timeline.pushAlloc(self.stackCompactor.?, 0, allocatedSize);
}
self.lock.unlock();
}
pub fn removeUntrackedAllocation(self: *@This(), allocatedSize: usize) void {
self.lock.lock();
self.untrackedAllocationsCount -= 1;
self.untrackedAllocationsSize -= allocatedSize;
self.lock.unlock();
}
pub fn getTotalMemoryUsed(self: *@This()) usize {
return self.totalAllocSize + self.untrackedAllocationsSize + self.getTrackerUsage();
}
pub fn getTrackerUsage(self: *@This()) usize {
if (self.timeline) |timeline| {
return timeline.events.capacity() + self.stackCompactor.?.stackArena.usage();
}
return 0;
}
var gMemTracker: ?*@This() = null;
pub const SetupSettings = struct {
timeline: bool = false,
};
pub fn MTSetup(backingAllocator: std.mem.Allocator, settings: SetupSettings) void {
gMemTracker = backingAllocator.create(@This()) catch unreachable;
gMemTracker.?.* = @This().init(backingAllocator, settings);
}
pub fn MTShutdown() void {
var backingAllocator = gMemTracker.?.backingAllocator;
gMemTracker.?.deinit();
backingAllocator.destroy(gMemTracker.?);
gMemTracker = null;
}
pub fn MTGet() ?*@This() {
return gMemTracker;
}
// todo replace all these functions with a virtual table
pub fn MTAddUntrackedAllocation(allocatedSize: usize) void {
if (gMemTracker) |mt|
mt.addUntrackedAllocation(allocatedSize);
}
pub fn MTRemoveAllocation(allocatedSize: usize) void {
if (gMemTracker) |mt|
mt.removeUntrackedAllocation(allocatedSize);
}
pub fn MTPrintStatsDelta() void {
if (gMemTracker) |mt| {
core.engine_log("allocated size: {d} ({d:.3} MiB)", .{
mt.totalAllocSize,
@as(f64, @floatFromInt(mt.totalAllocSize)) / 1024 / 1024,
});
core.engine_log("peak allocated size size: {d} ({d:.3} MiB) ({d} peak allocations)", .{
mt.peakAllocSize,
@as(f64, @floatFromInt(mt.peakAllocSize)) / 1024 / 1024,
mt.peakAllocations,
});
}
}
pub fn PrintStatsWithTag(comptime tag: []const u8) void {
core.engine_logs(tag);
MTPrintStatsDelta();
}