alexcastano

alexcastano

Stream consumes much more memory

Hello everybody,

I’m creating an HTTP wrapper to download files Stream. Currently, it works with :httpc, :hackney and :ibrowse and has the following functions:

  • stream/2 which creates an Elixir Stream.
  • read/2 which get the content and returns a string.
  • download/2 which downloads to a file.

I realized that I can use just stream/2 to implement the other two functions. This would simplify a lot the library code, but before making the change I just wanted to benchmark. I’m surprised with the results for a simple test with a 1MB file. If I use the stream/2 function the memory consumption is much higher (with the same speed):

$  mix run benchs/download.exs
...

Memory usage statistics:

Name                     average  deviation         median         99th %
download                 3.18 KB     ±1.00%        3.20 KB        3.20 KB
stream                  32.58 KB     ±0.40%       32.62 KB       32.67 KB
stream_file_open        41.06 KB     ±0.44%       41.12 KB       41.17 KB

Comparison:
download                 3.20 KB
stream                  32.62 KB - 10.21x memory usage
stream_file_open        41.12 KB - 12.87x memory usage

$ mix run benchs/read.exs
...

Memory usage statistics:

Name           Memory usage
read_stream        26.39 KB
read                3.29 KB - 0.12x memory usage

The read benchmark is something like this:

read = fn -> {:ok, r} = Down.read(url, backend: :httpc); r end
read_stream = fn ->
  url
  |> Down.stream(backend: :httpc)
  |> Enum.into([])
  |> IO.iodata_to_binary()
end

The Down.read/2 function internally uses a very similar strategy to the read_stream function. Every time it receives a new chunk it appends to a list and when it finishes, it calls to the same IO.iodata_to_binary/1:

With the download function, the situation is very similar. Instead of inserting chunks in a list, I just write them to a file.

The Down.stream/2 function just sends received chunks to the PID using Stream.resource/3:

The benchmarks are here:

The full code can be found in https://github.com/alexcastano/down
I don’t use any buffer or any caching system.

If someone wants to make some test I use the following docker command to have an HTTP test server:

$ docker run --name httpbin --restart=unless-stopped -p 6080:80 -d kennethreitz/httpbin

I cannot find an explanation for this, at the moment. So my questions are:

  • Do you know why is this happening?
  • Should I be worried about this or 33K is acceptable? Of course, it would be nicer 3KB :slight_smile:
  • Are Benchee memory benchmarks accurate?

Thanks in advance.

Most Liked

OvermindDL1

OvermindDL1

Stream uses function thunks, which will use more memory, plus there is more passing of data, so the GC will need to run more often, though it doesn’t run often as it so it can reclaim it all en masse later, so it is using available RAM to make the operation faster. Remember, only use Stream when you need true unbounded or unknown bound operations or when your overall structure exceeds available memory, else keep with immediate constructs. :slight_smile:

And yeah, 33k is basically nothing for that kind of stuff.

alexcastano

alexcastano

Well, I’m trying to do a generic library to stream HTTP request. So the main reason to use it is that you can use the data piece by piece. Another reason to use it could be to avoid an attack of huge files.

I’m not sure if I follow you here. The library can use the streaming options of :hackney, :ibrowse and :httpc. It checks the size of every chunk is received. When the sum of the chunk sizes is bigger than the given limit, it stops the download. It also checks the header size.

Exactly, you’re right. When the library receives a new chunk and it is written straight to the file, it only consumes 4KB. When I use a stream like this:

file = Temp.path!() |> File.open!([:write, :delayed])

  url
  |> Down.stream(backend: :httpc)
  |> Stream.each(fn c -> IO.binwrite(file, c) end)
  |> Stream.run()

File.close(file)

or like this:

  file_stream = Temp.path!() |> File.stream!([:write, :delayed])

  url
  |> Down.stream(backend: :httpc)
  |> Stream.into(file_stream)
  |> Stream.run()

is when it consumes so much memory.

It is open source: https://github.com/alexcastano/down/blob/master/benchs/download.exs

But of course, I know it is too much to ask you to take a look :slight_smile:

If you can give me any advice to debug where the memory is consumed would be great.

Thank you for your time

Where Next?

Popular in Questions Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

Other popular topics Top

vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New

We're in Beta

About us Mission Statement