alexcastano

alexcastano

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.

Showing Posts 1 to 4

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 OP

Thank you for your response.

Ok, this makes sense :slight_smile:

I think an HTTP download is a good case to use Stream. Don’t you?

In fact, one of the main features is to stop the download after a maximum size, because the remote server can:

  • send an invalid size header
  • don’t send the file size at all
  • send just junk data trying to fill our RAM or our disk

Ok, I agreed with this, until I found a big problem. I did the same test with a 300MB file and those are the results:

Benchmarking download...
Benchmarking stream...

Name               ips        average  deviation         median         99th %
stream            0.30         3.36 s     ±1.98%         3.36 s         3.40 s
download          0.29         3.46 s     ±5.99%         3.46 s         3.60 s

Comparison:
stream            0.30
download          0.29 - 1.03x slower

Memory usage statistics:

Name        Memory usage
stream          72.34 MB
download      0.00397 MB - 0.00x memory usage

So, when the library writes to the file the memory size is constant 4K. When I delegate to Stream it depends on the file size. It’s strange, isn’t it?

I’d like to investigate more about this, but I don’t know how to do it. Not enough knowledge about ErlangVM or debug tools.

Thank you!

OvermindDL1

OvermindDL1

Depends, can you use the data piecemeal or do you need all of the returned data en masse to work on it?

That all depends on the download API being used then, which one are you using?

I bet it’s storing to a binary and not being processed anywhere, binaries are extremely efficient especially cross-actors as they have their own global heap storage when above 64 bytes in size or so (as well as fast appending if only one owner, etc… etc… A stream will end up allocating a whole ton of binaries! But if your download API can write straight to a file then it can use the base BEAM calls, which use the kernel calls to pass the socket information straight over to the kernel so no real allocations need to be done. Need to see code to see what all is being done.

alexcastano

alexcastano OP

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

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews