diff --git a/src/core_ext/gc.cr b/src/core_ext/gc.cr new file mode 100644 index 0000000..90c7708 --- /dev/null +++ b/src/core_ext/gc.cr @@ -0,0 +1,5 @@ +{% if flag?(:gc_none) || flag?(:wasm32) %} + require "./gc/none" +{% else %} + require "./gc/boehm" +{% end %} diff --git a/src/core_ext/gc/boehm.cr b/src/core_ext/gc/boehm.cr new file mode 100644 index 0000000..9f2a835 --- /dev/null +++ b/src/core_ext/gc/boehm.cr @@ -0,0 +1,37 @@ +lib LibGC + GC_I_PTRFREE = 0 + GC_I_NORMAL = 1 + + fun get_kind_and_size = GC_get_kind_and_size(p : Void*, psize : SizeT*) : Int + + alias ReachableObjectFunc = Void*, SizeT, Void* -> Void + + fun enumerate_reachable_objects_inner = GC_enumerate_reachable_objects_inner(proc : ReachableObjectFunc, client_data : Void*) + + fun register_disclaim_proc = GC_register_disclaim_proc(kind : Int, proc : Void* -> Int, mark_from_all : Int) +end + +module GC + # Returns whether *ptr* is a pointer to the base of an atomic allocation. + def self.atomic?(ptr : Pointer) : Bool + LibGC.get_kind_and_size(ptr, nil) == LibGC::GC_I_PTRFREE + end + + # Walks the entire GC heap, yielding each allocation's base address and size + # to the given *block*. + # + # The *block* must not allocate memory using the GC. + def self.each_reachable_object(&block : Void*, UInt64 ->) : Nil + Thread.stop_world + GC.lock_write + begin + LibGC.enumerate_reachable_objects_inner(LibGC::ReachableObjectFunc.new do |obj, bytes, client_data| + fn = client_data.as(typeof(pointerof(block))).value + fn.call(obj, bytes.to_u64!) + end, pointerof(block)) + ensure + GC.unlock_write + Thread.start_world + end + end +end diff --git a/src/core_ext/gc/none.cr b/src/core_ext/gc/none.cr new file mode 100644 index 0000000..beffc3a --- /dev/null +++ b/src/core_ext/gc/none.cr @@ -0,0 +1,13 @@ +module GC + # Returns whether *ptr* is a pointer to the base of an atomic allocation. + def self.atomic?(ptr : Pointer) : Bool + false + end + + # Walks the entire GC heap, yielding each allocation's base address and size + # to the given *block*. + # + # The *block* must not allocate memory using the GC. + def self.each_reachable_object(&block : Void*, UInt64 ->) : Nil + end +end diff --git a/src/perf_tools/dump_heap.cr b/src/perf_tools/dump_heap.cr new file mode 100644 index 0000000..134fe38 --- /dev/null +++ b/src/perf_tools/dump_heap.cr @@ -0,0 +1,96 @@ +require "./common" +require "../core_ext/gc" + +# Functions to produce binary dumps of the running process's GC heap, useful for +# out-of-memory analysis. +# +# Methods in this module are independent from other tools like `MemProf`. +module PerfTools::DumpHeap + # Dumps a compact representation of the GC heap to the given *io*, sufficient + # to reconstruct all pointers between allocations. + # + # All writes to *io* must not allocate memory using the GC. For `IO::Buffered` + # this can be achieved by disabling write buffering (`io.sync = true`). + # + # The binary dump begins with the 8-byte header `"GCGRPH\x01\x00"`, then it is + # followed by a sequential list of allocation records. Each record contains + # the following fields, all 64-bit little-endian integers, unless otherwise + # noted: + # + # * The base address of the allocation. + # * The byte size of the allocation. This may be larger than the size + # originally passed to `GC.malloc` or a similar method, as the GC may + # reserve trailing padding bytes for alignment. Additionally, for an atomic + # allocation, the most significant bit of this field is set as well. + # * The pointer-sized word at the start of the allocation. If this allocation + # corresponds to an instance of a Crystal reference type, the lower bytes + # will contain that type's ID. + # * If the allocation is non-atomic, then for each inner pointer, the field + # offset relative to the allocation base and the pointer value are written; + # this list is terminated by a single `UInt64::MAX` field. Atomic records do + # not have this list. + # + # All the records are then terminated by a single `UInt64::MAX` field. + def self.graph(io : IO) : Nil + GC.collect + + io << "GCGRPH\x01\x00" + + GC.each_reachable_object do |obj, bytes| + is_atomic = GC.atomic?(obj) + io.write_bytes(obj.address.to_u64!) + io.write_bytes(bytes.to_u64! | (is_atomic ? Int64::MIN : 0_i64)) + + ptr = obj.as(Void**) + io.write_bytes(ptr.value.address.to_u64!) + + unless is_atomic + b = ptr + e = (obj + bytes).as(Void**) + while ptr < e + inner = ptr.value + if GC.is_heap_ptr(inner) + io.write_bytes((ptr.address &- b.address).to_u64!) + io.write_bytes(inner.address.to_u64!) + end + ptr += 1 + end + io.write_bytes(UInt64::MAX) + end + end + + io.write_bytes(UInt64::MAX) + end + + # Dumps the contents of the GC heap to the given *io*. + # + # All writes to *io* must not allocate memory using the GC. For `IO::Buffered` + # this can be achieved by disabling write buffering (`io.sync = true`). + # + # The binary dump begins with the 8-byte header `"GCFULL\x01\x00"`, then it is + # followed by a sequential list of allocation records. Each record contains + # the following fields, all 64-bit little-endian integers, unless otherwise + # noted: + # + # * The base address of the allocation. + # * The byte size of the allocation. This may be larger than the size + # originally passed to `GC.malloc` or a similar method, as the GC may + # reserve trailing padding bytes for alignment. Additionally, for an atomic + # allocation, the most significant bit of this field is set as well. + # * The full contents of the allocation. + # + # All the records are then terminated by a single `UInt64::MAX` field. + def self.full(io : IO) : Nil + GC.collect + + io << "GCFULL\x01\x00" + + GC.each_reachable_object do |obj, bytes| + io.write_bytes(obj.address.to_u64!) + io.write_bytes(bytes.to_u64! | (GC.atomic?(obj) ? Int64::MIN : 0_i64)) + io.write(obj.as(UInt8*).to_slice(bytes)) # TODO: 32-bit overflow? + end + + io.write_bytes(UInt64::MAX) + end +end diff --git a/src/perf_tools/mem_prof.cr b/src/perf_tools/mem_prof.cr index 578edd2..90f2009 100644 --- a/src/perf_tools/mem_prof.cr +++ b/src/perf_tools/mem_prof.cr @@ -1,4 +1,5 @@ require "./common" +require "../core_ext/gc" # A simple in-memory memory profiler that tracks all allocations and # deallocations by the garbage-collecting allocator. @@ -650,13 +651,6 @@ module PerfTools::MemProf init end -lib LibGC - GC_I_PTRFREE = 0 - GC_I_NORMAL = 1 - - fun register_disclaim_proc = GC_register_disclaim_proc(kind : Int, proc : Void* -> Int, mark_from_all : Int) -end - module GC # :nodoc: def self.malloc(size : LibC::SizeT) : Void*