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 GitHub - alexcastano/down: Elixir library for streaming, flexible and safe downloading of remote files · GitHub
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

_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: <h1>Create Post</h1> <%= ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 42920 311
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New

We're in Beta

About us Mission Statement