Memory monitoring/debugging

Hi,

what do ppl do to monitor memory? Or debug memory allocations.

In Ruby we got ObjectSpace and the GC modules.

Found that you can give environment variables to Boehm GC, has that been helpful to someone?

1 Like

For now you can use GC.stats. This isn’t documented and the API is subject to change… but it works: https://play.crystal-lang.org/#/r/6c3n

3 Likes

Nice! Thanks!

What a coincidence, I’ve been playing with this right now and I went home mostly empty-handed. I see no way to monitor/debug individual allocations with GC.stats, so I went ahead and naively patched module GC like this:

module ::GC
def self.malloc(size : LibC::SizeT) : Void*
  @@metrics.n_malloc += 1
  previous_def
end # (same for malloc_atomic, realloc, free and collect)
end

Unfortunately it doesn’t help much, because although the counter ticks up, the bytes_free counter from GC.stats doesn’t change. I see the temp objects sit on the same addresses like

#<Channel::Unbuffered(Int32):0x1106e80>
#<Channel::Unbuffered(Int32):0x1106e60>
#<Channel::Unbuffered(Int32):0x1106e80>
#<Channel::Unbuffered(Int32):0x1106e60>

but there are 3 allocs for each. I’ve also played with only bumping the counter on conditions that bytes_free or bytes_since_gc change, but both are also not something I can rely on as it seems.

The questions that I want answered are along the line of “leaking” memory (allocating refs that will never be collected) and it seems to me there is no help to pinpoint those from Crystal runtime.

I understand that the memory model is nontrivial, so maybe there is no easy way out at all. This is not a huge issue, just something I was thinking about, having a goal to develop long running daemons that would not eat up memory, we have enough problems with OOM already.

It also doesn’t help that many stdlib components (Channel, HTTP::Client::Response etc.) are declared classes, not structs, and demand heap space even if I want them to live on stack. Is there a way to control allocation that I’m not aware of?

going off-topic but yeah, HTTP::Client::Response looks like a good candidate for a struct, while Channel is not as you modify its state all the time and typically is long lived.

So on the one hand as channels are used to sync fibes, each with its own stack, it can’t live in a stack inside one of those fibers, however if I have a “parent” fiber, that spawns “child” fibers, it can live there. I guess that harkens back to structured concurrency discussion, because fibers do not have a scope currently.

Having said that, “long lived” point is especially interesting, because they don’t have to be. There is a number of patterns that can seriously benefit from disposable channels that are created and discarded constantly.

No. Allocation is controlled by declaring a type as a class or struct.

That might actually be possible. The downside is that you can’t inherit from a struct, so there could be no specialized subtypes.

Channels need to be allocated on the heap. Their very purpose is to be accessible from different execution stacks and it couldn’t be guaranteed that the stack where the channel lives is still valid when it is accessed from different contexts.

it couldn’t be guaranteed that the stack where the channel lives is still valid

Yes, and I think it needs to change in some distant future. I think this is a bit of an aside though, there are many more classes in Crystal that are not used to sync fibers.