Crystal: GC Warning: Repeated allocation of very large block

Created on 5 Feb 2016  Â·  27Comments  Â·  Source: crystal-lang/crystal

I'm getting this warning sometimes when running my code:

GC Warning: Repeated allocation of very large block (appr. size 8392704):
    May lead to memory leak and poor performance.

I understand the error, but I don't know where in my code the allocation is happening. How can I track this down?

The size of the block changes each time, incidentally. Which is strange.

question

Most helpful comment

Issue tracker warning: Repeated bug reports without code to reproduce them.
    May result in no answer.

All 27 comments

Issue tracker warning: Repeated bug reports without code to reproduce them.
    May result in no answer.

@asterite: I know Crystal is free and you don't owe me anything, but is it strictly necessary to go out of your way to be sarcastic and unhelpful? I can't produce code to reproduce the problem without first tracking down the source. I was asking how - given that Crystal has ZERO debugging support, without even line numbers in exceptions - I should go about it.

Seriously, debugging Crystal code is like pulling teeth. And when I ask for help you just take the piss. Rather than criticising MY attitude, I suggest you spend some time considering how yours could be improved.

Incidentally, I found the problem. If a large chunk of data is returned from a recursive function, it is copied each time up the call stack. Maybe you should fix that.

Yes, sorry, instead of asking you again for some code I decided to reply in a more funny way, but my intention was not to offend you, although later I saw that that might have been the case. So then I sent that link showing that I did try to track those things down but was unlucky.

I'll try again later, maybe I was doing something wrong.

Could you still provide the code that returns a large chunk of data in the recursive function?

All good, not to worry. I'll dig out the code when I get a chance.

Could you make Crystal easier to debug, perhaps? Like a "break into debugger and single-step" command? With variable inspection?

I was able to set the warning callback from GC :-)

lib LibGC
  alias WarnProc = LibC::Char*, Word ->
  fun set_warn_proc = GC_set_warn_proc(WarnProc)
  $warn_proc = GC_current_warn_proc : WarnProc
end

LibGC.set_warn_proc ->(msg, word) {
  raise String.new(msg)
}

x = [] of Pointer(Int32)
loop do
  x << Pointer.malloc(10_000_000, 0)
end

This will show the stack trace of where the problem originates.

Now we need to decide what to do in such cases. We still don't understand what's the purpose of that warning: it's just a warning, might not be an error at all. We also don't know if it's safe to allocate memory there (raise allocates).

Right now the debugger is simple, and you can use it. For example:

# foo.cr
def foo
  a = 1
  debugger
  puts a
  a
end

foo

Then:

$ bin/crystal build foo.cr
(lldb) target create "./foo"
Current executable set to './foo' (x86_64).
(lldb) run
Process 48689 launched: './foo' (x86_64)
Process 48689 stopped
* thread #1: tid = 0x228e0e, 0x0000000100002ed0 foo`*foo:Int32 + 16, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0)
    frame #0: 0x0000000100002ed0 foo`*foo:Int32 + 16
foo`*foo:Int32:
->  0x100002ed0 <+16>: movl   -0x4(%rbp), %edi
    0x100002ed3 <+19>: callq  0x100002ef0               ; *puts<Int32>:Nil
    0x100002ed8 <+24>: movl   -0x4(%rbp), %eax
    0x100002edb <+27>: addq   $0x10, %rsp
(lldb) bt
* thread #1: tid = 0x228e0e, 0x0000000100002ed0 foo`*foo:Int32 + 16, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0)
  * frame #0: 0x0000000100002ed0 foo`*foo:Int32 + 16
    frame #1: 0x000000010000122a foo`__crystal_main + 2266
    frame #2: 0x0000000100002e28 foo`main + 40
    frame #3: 0x00007fff99d185ad libdyld.dylib`start + 1
(lldb) ^D

If you compile with debug info (-d):

$ bin/crystal build foo.cr -d
(lldb) target create "./foo"
Current executable set to './foo' (x86_64).
(lldb) run
Process 49171 launched: './foo' (x86_64)
Process 49171 stopped
* thread #1: tid = 0x22960c, 0x00000001000035a0 foo`foo + 16 at foo.cr:1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0)
    frame #0: 0x00000001000035a0 foo`foo + 16 at foo.cr:1
-> 1    def foo
   2      a = 1
   3      debugger
   4      puts a
   5      a
   6    end
   7    
(lldb) expr a
(int) $0 = 1

It's not working very well yet, and just a few types are supported. But it's a start :-)

Good job @asterite :+1:

@asterite The code was something like the following:

require "http/client"

def download_data(url)
  puts "Downloading from " + url
  response = HTTP::Client.get(url)
  case response.status_code
    when 200
      puts "  Found it"
      return response.body
    when 301, 302, 303, 307, 308
      puts "  Redirecting"
      download_data response.headers["Location"]
    else
      raise Exception.new(url)
  end
end

download_data "http://cache.ruby-lang.org/pub/ruby/stable-snapshot.tar.gz"
puts
download_data "http://sourceforge.net/projects/hd-idle/files/latest/download"

It won't run on play - it just times out. But the problem presumably occurs when a redirection happens, and therefore recursion. I have since refactored it to use a loop. But wouldn't it be a better idea to avoid copying large chunks of data during recursion?

(Is that what is known as "tail-recursion"?)

@stugol perhaps also because cache.ruby-lang.org takes something between 10-15 seconds to send any header response and the file contents are 17783659 bytes (17MB) ?

That could explain a massive allocation of memory which could explain why GC returns the warning.

As you can see, the more code that you can provide at the beginning of the issue, the better people is going to be able to assist you and perhaps correct a bug (if there is one) or correct the code (if is wrong).

For future reference: http://yourbugreportneedsmore.info/

@luislavena Look, I'm a programmer myself. I know the difference between a good bug report and a bad one. But - for the third time! - _I didn't know what code was causing the problem at the time!_ I was asking for advice on how to narrow it down. I can't provide example code for an issue if I don't know what is causing it. If Crystal had proper debugging support, this wouldn't be so much of an issue. But it doesn't.

How can I download the file without allocating all the memory at once? Is there a way to download it in parts?

@stugol to quote yourself:

I'm getting this warning sometimes when running my code:

Then there is no mention what you're doing with the code (say, for example, download a file, a chat thing, anything).

Being a programmer doesn't entitle you to be a _good bug reporter_, is a learned technique, but you should learn from several attempts from different folks, on several reported issues, to provide more context about the issue you're reporting.

Crystal does have debugging and while limited in some places (say complex data type inspection) is enough to pinpoint the culprit of such issue.

Also, once again, the key point here are the details, which these bug reports are lacking and thus, is really impossible to assist you and figure out what is going on.

You might want to look at chunked downloads and stream that directly into a temporary file.

As you can see, if you provided this information at the beginning, we could have provided all this without the pointless back and forth and entitlements (and tones) around the conversation.

@luislavena For the FOURTH TIME, I didn't know which part of my code was causing it! It could have been the downloading part, the stream IO part, the logging part, or any other part. Telling you the purpose of my program as a whole would not have helped in any way whatsoever.

I don't know how to do chunked downloading in Crystal. This is why I asked for information a moment ago. Which you didn't provide, incidentally.

"Entitlements"!? I politely asked for help, and was met with sarcasm. If anyone's "tone" or attitude sucks in this thread, it is yours! With respect, @luislavena, please go perform a lewd action with yourself. You are not being helpful, and frankly are getting on my wick.

_cough_ calm down people _cough_ not the end of the world _cough_

Yes, let's calm down a bit, these are just bits on some machine in github, which, if you interpret them in some arbitrary way, might look like offensive text. But they are far from really hurting anyone.

@stugol As I said before, I'm sorry for my sarcastic comment. I didn't mean it to be sarcastic. My answer would have been the same: "Without some code we can't help you, we have no way to reproduce it". I just wanted to add some humour instead of saying that for the 10th time.

The problem you are having might be because you are downloading large chunks of data and building a big string for them. You need to use HTTP Client streaming. If you read the docs you'll see it's almost the only thing that's mentioned there: you pass a block, and the response's body is available as an IO in the body_io method.

As a side note, we have to add automatic (and configurable) redirect for HTTP::Client...

@asterite You and I are fine. We already cleared that up. It was @luislavena's remarks I was objecting to.

Yes, an automatic redirect would be helpful. I was going to ask if such a thing existed; but decided just to code it myself instead.

I vaguely recall seeing something about body_io. So I would just repeatedly pull from it until empty, I assume?

Yes. It's binary data so you'd have to use read(slice) or read bytes. But if you want to download a file to disk you can do:

require "http/client"

HTTP::Client.get("http://cache.ruby-lang.org/pub/ruby/stable-snapshot.tar.gz") do |response|
  File.open("./stable-snapshot.tar.gz", "w") do |file|
    IO.copy(response.body_io, file)
  end
end

But I just tried it and it's much slower than wget (maybe because it's a specialized program), we'd have to see what can be improved there.

@asterite It's probably fluctuations in your internet connection. Try with a local HTTP server and a really big file. In my machine I tried and it works faster than wget and I didn't even have to build in release mode :wink:

I didn't know about IO.copy. I notice it uses a buffer size of 1kb. Is this optimal?

We could probably redesign the api and use IO#copy_to(target) instead. Most IO are already buffered so we could write from the internal buffer to the target instead of using intermediate memory.

Sounds like a decent idea. Should be pretty simple to do. Maybe if an IO is unbuffered it could automatically do what it currently does; whereas if it's buffered it copies directly?

I'm still getting this warning, despite using IO.copy. My program is quite large, but here are the functions I think are relevant:

def download_data(url, filename)
    $logger << "Downloading ⟨W#{url}⟩ to ⟨W#{filename}⟩"
    response = HTTP::Client.goto(url)
    $logger << "web server returned status " + response.status_code.to_s
    case response.status_code
        when 200
            File.open(filename, "w") do |file|
                IO.copy(response.body_io, file)
            end
        else
            raise FailedDownloadException.new(url)
    end
end

def get_and_extract(url, dirname, archivefile)
    I::FS::Tempdir.create(dirname) do |workingdir|
        $logger << "Created temporary directory ⟨W#{workingdir}⟩"
        local_archivefile = pathname(Dir.current) + archivefile
        Dir.cd workingdir do
            if File.file? local_archivefile
                archivefile = local_archivefile
            else
                download_data url, archivefile
            end
            execute0 %{tar -xzf "#{archivefile}"}
            yield
        end
    end
end

class HTTP::Client
    def self.goto(url)
        loop {
            response = HTTP::Client.get(url)
            case response.status_code
                when 301, 302, 303, 307, 308
                    url = response.headers["Location"]
                else
                    return response
            end
        }
    end
end

def execute0(cmd)
    execute2(cmd) { }
end

def execute3(cmd)
    begin
        $logger << "Executing ⟨b" + cmd + "⟩"
        $logger.with_indent do
            I::FS.execute!(cmd) do |input, output, err|
                yield input, output, err
            end
            Fiber.yield; Fiber.yield
        end
    rescue e
        log_error(e)
        raise e
    end
end

def execute2(cmd, &block : Symbol,String->)
    execute3(cmd) do |input, output, err|
        spawn { while !output.closed?; o = output.read_line; $logger << o; block.call :output, o; end rescue nil }
        spawn { while !err.closed?; o = err.read_line; log_error o; block.call :error, o end rescue nil }
    end
end

module I::FS
    def self.execute!(cmd, echo = false)
        puts "> " + cmd if echo
        args = shellsplit(cmd); cmd = args.shift
        ::IO.pipe { |input_io_read, input_io_write|
            ::IO.pipe { |output_io_read, output_io_write|
                ::IO.pipe { |error_io_read, error_io_write|
                    raise FileNotFoundException.new(cmd) unless (pathname(cmd).is_fully_qualified? ? File.exists?(cmd) : does_command_exist?(cmd))
                    exitcode = 0
                    result = Process.run(cmd, args, nil, false, false, input_io_read, output_io_write, error_io_write) do |p|
                        begin
                            yield input_io_write, output_io_read, error_io_read
                        ensure
                            exitcode = p.wait.exit_code
                        end
                    end
                    raise ProcessRunException.new(cmd, exitcode) unless exitcode == 0
                    return exitcode
                }
            }
        }
    end
end

I doubt it's the $logger at fault; as I use it elsewhere and it works fine there.

The "large block" is reported to be somewhere in the vicinity of 33MB. I don't know why.

It seems you aren't using the block version of http client get, so the whole body is downloaded as a big string.

I wasn't aware that the two versions of the function worked in different ways. Why is this?

Because dealing with a string is easier than dealing with an IO. Also, if we want to reuse a connection for multiple requests we must be sure to read the response before doing a next request, and the block form does that at the end of the block, so you can't forget to close it. For example in Go it's your responsibility to close it, in our case you can't forget to do it.

But couldn't the body property call body_io behind the scenes; and when you do the next request, it can clear the response automatically? Then the block-form and return-form functions would be identical.

That could work, I guess. The client would have to remember the last response and close it before doing a next one. There's always a tradeoff, I'll discuss this with @waj

Was this page helpful?
0 / 5 - 0 ratings

Related issues

RX14 picture RX14  Â·  3Comments

costajob picture costajob  Â·  3Comments

will picture will  Â·  3Comments

asterite picture asterite  Â·  3Comments

lbguilherme picture lbguilherme  Â·  3Comments