quda
I am building a json REST API with Phoenix that (among other services) has to convert static CSV files from a repository and “serve” them as response in json format; that will be used downstream by other services in our project. Some of these csv files are quite big, ranging for a several hundred kb to tens of MB.
So far so good, I made it work with several lines of code.
So, in my backend side:
defp csv(file) do
file
|> Path.expand(__DIR__)
|> File.stream!()
|> CSV.decode(headers: true)
|> Enum.map(fn {:ok, val} -> val end)
end
in the API controller:
def show(conn, bhandler) do
conn
|> Plug.Conn.put_resp_header("content-type", "application/json; charset=utf-8")
|> Plug.Conn.send_resp(200, Jason.encode!(bhandler, pretty: true))
end
end
The program is doing well what is supposed to do but on small files.
When the file gets bigger and response time gets exponentially bigger:
- for 69kb (csv file) → 124ms

- 234kb → 2.8s

- 1.1MB → 24s

- 22.4MB → 835s

Obviously something is very wrong with these implementation, or with its libraries (CSV, Jason). A similar (simple) API we have, in php, took a few of seconds to read/convert/send/receive 40MB csv file.
Do you have any suggestion ?
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #phoenix_html
- #iex
- #graphql
- #genstage
- #ai
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #performance











First 10 of 48 Posts
moogle19
Maybe try nimble_csv. It has
NimbleCSV.RFC4180build in, which should parse your file.seems also faster than streaming the file (at least for ~20MB sized files).
The only downside is, that
parse_string/1returns a list of lists and you have to map it to a map yourself.krasenyp
I see two possible solutions. First, as @moogle19 suggested, you can use
NumbleCSV’sparse_streamand send a chunked response using Plug.Conn.send_chunked/2, never loading the whole file into memory. Second, you can, again, useparse_streamto stream the contents into a temporary file and use Plug.Conn.send_file/5. I believe the second one is trickier because in case you have a lot of requests there should be rate limiting in order to not fill your disk memory.lud
Can you measure which code is slow ? Is it the parsing, the JSON encoding, etc.
Also you could:
Stream.mapinstead ofEnum.mapto avoid keeping too much data in memoryJason.Fragmenton each inStream.mapto encode to JSON line by lineJason.encode_to_iodatainstead ofJason.encodeto avoid converting the full JSON structure to a string before sending itquda
Good idea, to find out which is the “culprit” and isolate the problem.
But I don’t know how to do that.
dimitarvp
When input data is expected to be large, always use streaming and never keep all of it in memory.
kokolegorille
You can use Benchee…
moogle19
I tested it with a ~130MB CSV file and the default
bencheeconfig:Code is:
It seems
nimble_csvis much faster for large files.lud
You can wrap each part of the code with
:timer.tc/1:etc.
quda
Thank you all, I managed to do this implementation using NimbleCSV:
It does the job but not much improvement
)
3.5MB → 22.46s
21.4MB → 7.2min (tested this file on a similar API, written long time ago, in php5, calling a humble fgetcsv() with no streams and we got 22.22s
So still far for being usable.
I presume the issue is not streaming in the csv file but the second part - assembling the map and sending out the json.
LostKobrakai
Is there a reason to stream the data into memory? If not try using the non streamed API, which should be faster.