343 lines
12 KiB
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();
|
|
}
|