Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/core_ext/gc.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% if flag?(:gc_none) || flag?(:wasm32) %}
require "./gc/none"
{% else %}
require "./gc/boehm"
{% end %}
37 changes: 37 additions & 0 deletions src/core_ext/gc/boehm.cr
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions src/core_ext/gc/none.cr
Original file line number Diff line number Diff line change
@@ -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
90 changes: 90 additions & 0 deletions src/perf_tools/dump_heap.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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 consists of 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: maybe call it #compact as the description states? It would be more aligned with the #full version.

GC.collect

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 consists of 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

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
8 changes: 1 addition & 7 deletions src/perf_tools/mem_prof.cr
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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*
Expand Down