Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
45 changes: 45 additions & 0 deletions src/core_ext/gc/boehm.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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
{% if flag?(:gc_none) %}
false
{% else %}
LibGC.get_kind_and_size(ptr, nil) == LibGC::GC_I_PTRFREE
{% end %}
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
# FIXME: this is necessary to bring `block` in scope until
# crystal-lang/crystal#15940 is resolved
typeof(block)

{% unless flag?(:gc_none) %}
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
end
{% end %}
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/boehm"

# 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 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/boehm"

# 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