mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
Co-authored-by: 190n <7763597+190n@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
564 lines
21 KiB
Zig
564 lines
21 KiB
Zig
const enable_debug = bun.Environment.isDebug;
|
|
const enable_single_threaded_checks = enable_debug;
|
|
|
|
pub const RefCountOptions = struct {
|
|
/// Defaults to the type basename.
|
|
debug_name: ?[]const u8 = null,
|
|
destructor_ctx: ?type = null,
|
|
};
|
|
|
|
/// Add managed reference counting to a struct type. This implements a `ref()`
|
|
/// and `deref()` method to add to the struct itself. This mixin doesn't handle
|
|
/// memory management, but is very easy to integrate with bun.new + bun.destroy.
|
|
///
|
|
/// Avoid reference counting when an object only has one owner.
|
|
///
|
|
/// const Thing = struct {
|
|
/// const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{});
|
|
/// // expose `ref` and `deref` as public methods
|
|
/// pub const ref = RefCount.ref;
|
|
/// pub const deref = RefCount.deref;
|
|
///
|
|
/// ref_count: RefCount,
|
|
/// other_field: u32,
|
|
///
|
|
/// pub fn init(field: u32) *T {
|
|
/// return bun.new(T, .{ .ref_count = .init(), .other_field = field });
|
|
/// }
|
|
///
|
|
/// // Destructor should be private so it is only called once ref_count is 0
|
|
/// // When this is empty, `bun.destroy` can be passed directly to `RefCount`
|
|
/// fn deinit(thing: *T) void {
|
|
/// std.debug.print("deinit {d}\n", .{thing.other_field});
|
|
/// bun.destroy(thing);
|
|
/// }
|
|
/// };
|
|
///
|
|
/// When `RefCount` is implemented, it can be used with `RefPtr(T)` to track where
|
|
/// a reference leak may be happening.
|
|
///
|
|
/// const my_ptr = RefPtr(Thing).adoptRef(.init(123));
|
|
/// assert(my_ptr.data.other_field == 123);
|
|
///
|
|
/// const second_pointer = my_ptr.dupeRef();
|
|
/// assert(second_pointer.data == my_ptr.data);
|
|
///
|
|
/// my_ptr.deref(); // pointer stays valid
|
|
///
|
|
/// // my_ptr.data // INVALID ACCESS
|
|
/// assert(second_pointer.data.other_field == 123);
|
|
///
|
|
/// second_pointer.deref(); // pointer is destroyed
|
|
///
|
|
/// Code that migrates to using `RefPtr` would put the .adoptRef call in it's
|
|
/// initializer. To combine `bun.new` and `adoptRef`, you can use `RefPtr(T).new`.
|
|
///
|
|
/// pub fn init(field: u32) RefPtr(T) {
|
|
/// return .new(.{
|
|
/// .ref_count = .init(),
|
|
/// .other_field = field,
|
|
/// });
|
|
/// }
|
|
///
|
|
/// Code can incrementally migrate to `RefPtr`, using `.initRef` to increment the count
|
|
///
|
|
/// const ref_ptr = RefPtr(T).initRef(existing_raw_pointer);
|
|
///
|
|
/// If you want to enforce usage of RefPtr for memory management, you
|
|
/// can remove the forwarded `ref` and `deref` methods from `RefCount`.
|
|
/// If these methods are not forwarded, keep in mind that it should use the wrapper.
|
|
pub fn RefCount(T: type, field_name: []const u8, destructor_untyped: anytype, options: RefCountOptions) type {
|
|
return struct {
|
|
active_counts: u32,
|
|
thread: if (enable_single_threaded_checks) ?bun.DebugThreadLock else void,
|
|
debug: if (enable_debug) DebugData(false) else void,
|
|
|
|
const debug_name = options.debug_name orelse bun.meta.typeBaseName(@typeName(T));
|
|
pub const scope = bun.Output.Scoped(debug_name, true);
|
|
|
|
const Destructor = if (options.destructor_ctx) |ctx| fn (*T, ctx) void else fn (*T) void;
|
|
const destructor: Destructor = destructor_untyped;
|
|
|
|
pub fn init() @This() {
|
|
return .initExactRefs(1);
|
|
}
|
|
|
|
/// Caller will have to call `unref()` exactly `count` times to destroy.
|
|
pub fn initExactRefs(count: u32) @This() {
|
|
assert(count > 0);
|
|
return .{
|
|
.thread = if (enable_single_threaded_checks) if (@inComptime()) null else .initLocked(),
|
|
.active_counts = count,
|
|
.debug = if (enable_debug) .empty else undefined,
|
|
};
|
|
}
|
|
|
|
// trait implementation
|
|
|
|
pub fn ref(self: *T) void {
|
|
const counter = getCounter(self);
|
|
if (enable_debug) {
|
|
counter.debug.assertValid();
|
|
}
|
|
if (bun.Environment.enable_logs and scope.isVisible()) {
|
|
scope.log("0x{x} ref {d} -> {d}:", .{
|
|
@intFromPtr(self),
|
|
counter.active_counts,
|
|
counter.active_counts + 1,
|
|
});
|
|
bun.crash_handler.dumpCurrentStackTrace(@returnAddress(), .{
|
|
.frame_count = 2,
|
|
.skip_file_patterns = &.{"ptr/ref_count.zig"},
|
|
});
|
|
}
|
|
counter.assertNonThreadSafeCountIsSingleThreaded();
|
|
counter.active_counts += 1;
|
|
}
|
|
|
|
pub fn deref(self: *T) void {
|
|
derefWithContext(self, {});
|
|
}
|
|
|
|
pub fn derefWithContext(self: *T, ctx: (options.destructor_ctx orelse void)) void {
|
|
const counter = getCounter(self);
|
|
if (enable_debug) {
|
|
counter.debug.assertValid(); // Likely double deref.
|
|
}
|
|
if (bun.Environment.enable_logs and scope.isVisible()) {
|
|
scope.log("0x{x} deref {d} -> {d}:", .{
|
|
@intFromPtr(self),
|
|
counter.active_counts,
|
|
counter.active_counts - 1,
|
|
});
|
|
bun.crash_handler.dumpCurrentStackTrace(@returnAddress(), .{
|
|
.frame_count = 2,
|
|
.skip_file_patterns = &.{"ptr/ref_count.zig"},
|
|
});
|
|
}
|
|
counter.assertNonThreadSafeCountIsSingleThreaded();
|
|
counter.active_counts -= 1;
|
|
if (counter.active_counts == 0) {
|
|
if (enable_debug) {
|
|
counter.debug.deinit(std.mem.asBytes(self), @returnAddress());
|
|
}
|
|
if (comptime options.destructor_ctx != null) {
|
|
destructor(self, ctx);
|
|
} else {
|
|
destructor(self);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn dupeRef(self: anytype) RefPtr(@TypeOf(self)) {
|
|
_ = @as(*const T, self); // ensure ptr child is T
|
|
return .initRef(self);
|
|
}
|
|
|
|
// utility functions
|
|
|
|
pub fn hasOneRef(count: *@This()) bool {
|
|
count.assertNonThreadSafeCountIsSingleThreaded();
|
|
return count.active_counts == 1;
|
|
}
|
|
|
|
pub fn dumpActiveRefs(count: *@This()) void {
|
|
if (enable_debug) {
|
|
const ptr: *T = @fieldParentPtr(field_name, count);
|
|
count.debug.dump(@typeName(T), ptr, count.active_counts);
|
|
}
|
|
}
|
|
|
|
/// The active_counts value is 0 after the destructor is called.
|
|
pub fn assertNoRefs(count: *const @This()) void {
|
|
if (enable_debug) {
|
|
bun.assert(count.active_counts == 0);
|
|
}
|
|
}
|
|
|
|
fn assertNonThreadSafeCountIsSingleThreaded(count: *@This()) void {
|
|
if (enable_single_threaded_checks) {
|
|
const thread = if (count.thread) |*ptr| ptr else {
|
|
count.thread = .initLocked();
|
|
return;
|
|
};
|
|
thread.assertLocked(); // this counter is not thread-safe
|
|
}
|
|
}
|
|
|
|
fn getCounter(self: *T) *@This() {
|
|
return &@field(self, field_name);
|
|
}
|
|
|
|
/// Private, allows RefPtr to assert that ref_count is a valid RefCount type.
|
|
const is_ref_count = unique_symbol;
|
|
const ref_count_options = options;
|
|
};
|
|
}
|
|
|
|
/// Add thread-safe reference counting to a struct type. This implements a `ref()`
|
|
/// and `deref()` method to add to the struct itself. This mixin doesn't handle
|
|
/// memory management, but is very easy to integrate with bun.new + bun.destroy.
|
|
///
|
|
/// See `RefCount`'s comment defined above for examples & best practices.
|
|
///
|
|
/// Avoid reference counting when an object only has one owner.
|
|
/// Avoid thread-safe reference counting when only one thread allocates and frees.
|
|
pub fn ThreadSafeRefCount(T: type, field_name: []const u8, destructor: fn (*T) void, options: RefCountOptions) type {
|
|
return struct {
|
|
active_counts: std.atomic.Value(u32),
|
|
debug: if (enable_debug) DebugData(true) else void,
|
|
|
|
const debug_name = options.debug_name orelse bun.meta.typeBaseName(@typeName(T));
|
|
pub const scope = bun.Output.Scoped(debug_name, true);
|
|
|
|
pub fn init() @This() {
|
|
return .initExactRefs(1);
|
|
}
|
|
|
|
/// Caller will have to call `unref()` exactly `count` times to destroy.
|
|
pub fn initExactRefs(count: u32) @This() {
|
|
assert(count > 0);
|
|
return .{
|
|
.active_counts = .init(count),
|
|
.debug = if (enable_debug) .empty,
|
|
};
|
|
}
|
|
|
|
// trait implementation
|
|
|
|
pub fn ref(self: *T) void {
|
|
const counter = getCounter(self);
|
|
if (enable_debug) counter.debug.assertValid();
|
|
const old_count = counter.active_counts.fetchAdd(1, .seq_cst);
|
|
if (comptime bun.Environment.enable_logs) {
|
|
scope.log("0x{x} ref {d} -> {d}", .{
|
|
@intFromPtr(self),
|
|
old_count,
|
|
old_count + 1,
|
|
});
|
|
}
|
|
bun.debugAssert(old_count > 0);
|
|
}
|
|
|
|
pub fn deref(self: *T) void {
|
|
const counter = getCounter(self);
|
|
if (enable_debug) counter.debug.assertValid();
|
|
const old_count = counter.active_counts.fetchSub(1, .seq_cst);
|
|
if (comptime bun.Environment.enable_logs) {
|
|
scope.log("0x{x} deref {d} -> {d}", .{
|
|
@intFromPtr(self),
|
|
old_count,
|
|
old_count - 1,
|
|
});
|
|
}
|
|
bun.debugAssert(old_count > 0);
|
|
if (old_count == 1) {
|
|
if (enable_debug) {
|
|
counter.debug.deinit(std.mem.asBytes(self), @returnAddress());
|
|
}
|
|
destructor(self);
|
|
}
|
|
}
|
|
|
|
pub fn dupeRef(self: anytype) RefPtr(@TypeOf(self)) {
|
|
if (enable_debug) getCounter(self).debug.assertValid();
|
|
_ = @as(*const T, self); // ensure ptr child is T
|
|
return .initRef(self);
|
|
}
|
|
|
|
// utility functions
|
|
|
|
pub fn hasOneRef(counter: *const @This()) bool {
|
|
if (enable_debug) counter.debug.assertValid();
|
|
return counter.active_counts.load(.seq_cst) == 1;
|
|
}
|
|
|
|
pub fn dumpActiveRefs(count: *@This()) void {
|
|
if (enable_debug) {
|
|
const ptr: *T = @alignCast(@fieldParentPtr(field_name, count));
|
|
count.debug.dump(@typeName(T), ptr, count.active_counts.load(.seq_cst));
|
|
}
|
|
}
|
|
|
|
/// The active_counts value is 0 after the destructor is called.
|
|
pub fn assertNoRefs(count: *const @This()) void {
|
|
if (enable_debug) {
|
|
bun.assert(count.active_counts.load(.seq_cst) == 0);
|
|
}
|
|
}
|
|
|
|
fn getCounter(self: *T) *@This() {
|
|
return &@field(self, field_name);
|
|
}
|
|
|
|
/// Private, allows RefPtr to assert that ref_count is a valid RefCount type.
|
|
const is_ref_count = unique_symbol;
|
|
const ref_count_options = options;
|
|
};
|
|
}
|
|
|
|
/// A pointer to an object implementing `RefCount` or `ThreadSafeRefCount`
|
|
/// The benefit of this over `T*` is that instances of `RefPtr` are tracked.
|
|
///
|
|
/// By using this, you gain the following memory debugging tools:
|
|
///
|
|
/// - `T.ref_count.dump()` to dump all active references.
|
|
/// - AllocationScope integration via `.newTracked()` and `.trackAll()`
|
|
///
|
|
/// If you want to enforce usage of RefPtr for memory management, you
|
|
/// can remove the forwarded `ref` and `deref` methods from `RefCount`.
|
|
///
|
|
/// See `RefCount`'s comment defined above for examples & best practices.
|
|
pub fn RefPtr(T: type) type {
|
|
return struct {
|
|
data: *T,
|
|
debug: DebugId,
|
|
|
|
const RefCountMixin = @FieldType(T, "ref_count");
|
|
const options = RefCountMixin.ref_count_options;
|
|
comptime {
|
|
bun.assert(RefCountMixin.is_ref_count == unique_symbol);
|
|
}
|
|
const DebugId = if (enable_debug) TrackedRef.Id else void;
|
|
|
|
/// Increment the reference count, and return a structure boxing the pointer.
|
|
pub fn initRef(raw_ptr: *T) @This() {
|
|
RefCountMixin.ref(raw_ptr);
|
|
bun.assert(RefCountMixin.is_ref_count == unique_symbol);
|
|
return uncheckedAndUnsafeInit(raw_ptr, @returnAddress());
|
|
}
|
|
|
|
// NOTE: would be nice to use an if for deref.
|
|
//
|
|
// pub const deref = if (options.destructor_ctx == null) derefWithoutContext else derefWithContext;
|
|
//
|
|
// but ZLS doesn't realize it is function so the semantic tokens look bad.
|
|
|
|
/// Decrement the reference count, and destroy the object if the count is 0.
|
|
pub fn deref(self: *const @This()) void {
|
|
derefWithContext(self, {});
|
|
}
|
|
|
|
/// Decrement the reference count, and destroy the object if the count is 0.
|
|
pub fn derefWithContext(self: *const @This(), ctx: (options.destructor_ctx orelse void)) void {
|
|
if (enable_debug) {
|
|
self.data.ref_count.debug.release(self.debug, @returnAddress());
|
|
}
|
|
if (comptime options.destructor_ctx != null) {
|
|
RefCountMixin.derefWithContext(self.data, ctx);
|
|
} else {
|
|
RefCountMixin.deref(self.data);
|
|
}
|
|
if (bun.Environment.isDebug) {
|
|
// make UAF fail faster (ideally integrate this with ASAN)
|
|
// this @constCast is "okay" because it makes no sense to store
|
|
// an object with a heap pointer in the read only segment
|
|
@constCast(self).data = undefined;
|
|
}
|
|
}
|
|
|
|
pub fn dupeRef(ref: @This()) @This() {
|
|
return .initRef(ref.data);
|
|
}
|
|
|
|
// Allocate a new object, returning a RefPtr to it.
|
|
pub fn new(init_data: T) @This() {
|
|
return .adoptRef(bun.new(T, init_data));
|
|
}
|
|
|
|
/// Initialize a newly allocated pointer, returning a RefPtr to it.
|
|
/// Care must be taken when using non-default allocators.
|
|
pub fn adoptRef(raw_ptr: *T) @This() {
|
|
if (enable_debug) {
|
|
bun.assert(raw_ptr.ref_count.hasOneRef());
|
|
raw_ptr.ref_count.debug.assertValid();
|
|
}
|
|
return uncheckedAndUnsafeInit(raw_ptr, @returnAddress());
|
|
}
|
|
|
|
/// This will assert that ALL references are cleaned up by the time the allocation scope ends.
|
|
pub fn newTracked(scope: *AllocationScope, init_data: T) @This() {
|
|
const ptr: @This() = .new(init_data);
|
|
ptr.trackImpl(scope, @returnAddress());
|
|
return ptr;
|
|
}
|
|
|
|
/// This will assert that ALL references are cleaned up by the time the allocation scope ends.
|
|
pub fn trackAll(ref: @This(), scope: *AllocationScope) void {
|
|
ref.trackImpl(scope, @returnAddress());
|
|
}
|
|
|
|
fn trackImpl(ref: @This(), scope: *AllocationScope, ret_addr: usize) void {
|
|
const debug = &ref.data.ref_count.debug;
|
|
debug.allocation_scope = &scope;
|
|
scope.trackExternalAllocation(
|
|
std.mem.asBytes(ref.data),
|
|
ret_addr,
|
|
.{ .ref_count = debug },
|
|
);
|
|
}
|
|
|
|
pub fn uncheckedAndUnsafeInit(raw_ptr: *T, ret_addr: ?usize) @This() {
|
|
return .{
|
|
.data = raw_ptr,
|
|
.debug = if (enable_debug) raw_ptr.ref_count.debug.acquire(
|
|
&raw_ptr.ref_count.active_counts,
|
|
ret_addr orelse @returnAddress(),
|
|
),
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
const TrackedRef = struct {
|
|
acquired_at: bun.crash_handler.StoredTrace,
|
|
|
|
/// Not an index, just a unique identifier for the debug data
|
|
pub const Id = bun.GenericIndex(u32, TrackedRef);
|
|
};
|
|
|
|
const TrackedDeref = struct {
|
|
acquired_at: bun.crash_handler.StoredTrace,
|
|
released_at: bun.crash_handler.StoredTrace,
|
|
};
|
|
|
|
/// Provides Ref tracking. This is not generic over the pointer T to reduce analysis complexity.
|
|
pub fn DebugData(thread_safe: bool) type {
|
|
return struct {
|
|
const Debug = @This();
|
|
const Count = if (thread_safe) std.atomic.Value(u32) else u32;
|
|
|
|
magic: enum(u128) { valid = 0x2f84e51d } align(@alignOf(u32)),
|
|
lock: if (thread_safe) std.debug.SafetyLock else bun.Mutex,
|
|
next_id: u32,
|
|
map: std.AutoHashMapUnmanaged(TrackedRef.Id, TrackedRef),
|
|
frees: std.AutoArrayHashMapUnmanaged(TrackedRef.Id, TrackedDeref),
|
|
// Allocation Scope integration
|
|
allocation_scope: ?*AllocationScope,
|
|
count_pointer: ?*Count,
|
|
|
|
pub const empty: @This() = .{
|
|
.magic = .valid,
|
|
.lock = .{},
|
|
.next_id = 0,
|
|
.map = .empty,
|
|
.frees = .empty,
|
|
.allocation_scope = null,
|
|
.count_pointer = null,
|
|
};
|
|
|
|
fn assertValid(debug: *const @This()) void {
|
|
bun.assert(debug.magic == .valid);
|
|
}
|
|
|
|
fn dump(debug: *@This(), type_name: ?[]const u8, ptr: *anyopaque, ref_count: u32) void {
|
|
debug.lock.lock();
|
|
defer debug.lock.unlock();
|
|
|
|
genericDump(type_name, ptr, ref_count, &debug.map);
|
|
}
|
|
|
|
fn nextId(debug: *@This()) TrackedRef.Id {
|
|
if (thread_safe) {
|
|
return .init(@atomicRmw(u32, &debug.next_id, .Add, 1, .seq_cst));
|
|
} else {
|
|
defer debug.next_id += 1;
|
|
return .init(debug.next_id);
|
|
}
|
|
}
|
|
|
|
fn acquire(debug: *@This(), count_pointer: *Count, return_address: usize) TrackedRef.Id {
|
|
debug.lock.lock();
|
|
defer debug.lock.unlock();
|
|
debug.count_pointer = count_pointer;
|
|
const id = nextId(debug);
|
|
debug.map.put(bun.default_allocator, id, .{
|
|
.acquired_at = .capture(return_address),
|
|
}) catch bun.outOfMemory();
|
|
return id;
|
|
}
|
|
|
|
fn release(debug: *@This(), id: TrackedRef.Id, return_address: usize) void {
|
|
debug.lock.lock(); // If this triggers ASAN, the RefCounted object is double-freed.
|
|
defer debug.lock.unlock();
|
|
const entry = debug.map.fetchRemove(id) orelse {
|
|
return;
|
|
};
|
|
debug.frees.put(bun.default_allocator, id, .{
|
|
.acquired_at = entry.value.acquired_at,
|
|
.released_at = .capture(return_address),
|
|
}) catch bun.outOfMemory();
|
|
}
|
|
|
|
fn deinit(debug: *@This(), data: []const u8, ret_addr: usize) void {
|
|
assertValid(debug);
|
|
debug.magic = undefined;
|
|
debug.lock.lock();
|
|
debug.map.clearAndFree(bun.default_allocator);
|
|
debug.frees.clearAndFree(bun.default_allocator);
|
|
if (debug.allocation_scope) |scope| {
|
|
_ = scope.trackExternalFree(data, ret_addr);
|
|
}
|
|
}
|
|
|
|
// Trait function for AllocationScope
|
|
pub fn onAllocationLeak(debug: *@This(), data: []u8) void {
|
|
debug.lock.lock();
|
|
defer debug.lock.unlock();
|
|
const count = debug.count_pointer.?;
|
|
debug.dump(null, data.ptr, if (thread_safe) count.load(.seq_cst) else count.*);
|
|
}
|
|
};
|
|
}
|
|
|
|
fn genericDump(
|
|
type_name: ?[]const u8,
|
|
ptr: *anyopaque,
|
|
total_ref_count: usize,
|
|
map: *std.AutoHashMapUnmanaged(TrackedRef.Id, TrackedRef),
|
|
) void {
|
|
const tracked_refs = map.count();
|
|
const untracked_refs = total_ref_count - tracked_refs;
|
|
bun.Output.prettyError("<blue>{s}{s}{x} has ", .{
|
|
type_name orelse "",
|
|
if (type_name != null) "@" else "",
|
|
@intFromPtr(ptr),
|
|
});
|
|
if (tracked_refs > 0) {
|
|
bun.Output.prettyError("{d} tracked{s}", .{ tracked_refs, if (untracked_refs > 0) ", " else "" });
|
|
}
|
|
if (untracked_refs > 0) {
|
|
bun.Output.prettyError("{d} untracked refs<r>\n", .{untracked_refs});
|
|
} else {
|
|
bun.Output.prettyError("refs<r>\n", .{});
|
|
}
|
|
var i: usize = 0;
|
|
var it = map.iterator();
|
|
while (it.next()) |entry| {
|
|
bun.Output.prettyError("<b>RefPtr acquired at:<r>\n", .{});
|
|
bun.crash_handler.dumpStackTrace(entry.value_ptr.acquired_at.trace(), AllocationScope.trace_limits);
|
|
i += 1;
|
|
if (i >= 3) {
|
|
bun.Output.prettyError(" {d} omitted ...\n", .{map.count() - i});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn maybeAssertNoRefs(T: type, ptr: *const T) void {
|
|
if (!@hasField(T, "ref_count")) return;
|
|
const Rc = @FieldType(T, "ref_count");
|
|
switch (@typeInfo(Rc)) {
|
|
.@"struct" => if (@hasDecl(Rc, "assertNoRefs"))
|
|
ptr.ref_count.assertNoRefs(),
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
const unique_symbol = opaque {};
|
|
|
|
const std = @import("std");
|
|
const bun = @import("bun");
|
|
const assert = bun.assert;
|
|
const AllocationScope = bun.AllocationScope;
|