-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize IO::Delimited
#11242
Optimize IO::Delimited
#11242
Conversation
@straight-shoota wow, thanks for the through review! ❤️ I'll send fixes later today or the next week. |
167cacd
to
71d8dc5
Compare
As a follow up, I just realized |
@straight-shoota I think I missed it or after so much time I forgot... what's the potential bug? |
4375c0b
to
810a52f
Compare
Oops, now Windows CI suddenly exits. Maybe something is broken with this code... |
Should we eventually merge this? It's a good optimization. |
Yes, I think it's a good change. But it's also delicate. We need to be careful about vetting the behaviour for correctness. This concern from my last review is still unaddressed: #11242 (comment) |
Ah, I think I didn't address it because I didn't understand it. Too much time has passed now that I lost context, so I don't think I'll be able to continue working on this. |
This PR just came up on the forum and it turns out to be a massive performance boost for large multipart uploads being processed on the server. With this code: require "http"
http = HTTP::Server.new do |context|
MIME::Multipart.parse context.request do |headers, io|
file_path = "public"
File.open(file_path, "w") do |f|
IO.copy(io, f)
end
end
end
http.listen 3000 Using the latest release of Crystal, I uploaded a ~2GB file to it, and it transfers at 162MB/sec, taking about 10 seconds:
With this patch, it transfers at 1.7GB/sec in barely over a second:
EDIT: I'd forgotten to compile the app in release mode. |
Really cool improvement, we should merge it to avoid it become stale again. |
@straight-shoota I just pushed another commit that addresses your concern. |
IO::Delimited
will now use the underlying'sIO#peek
if availableWhen
IO#peek
is available we can do much better!Benchmark:
Output before this PR:
Output after this PR:
IO::Delimited
now implementspeek
As it turns out,
IO::Delimited
is used in the standard library for multipart parsing:crystal/src/mime/multipart/parser.cr
Lines 64 to 65 in 7a68793
crystal/src/mime/multipart/parser.cr
Line 89 in 7a68793
But, as you can see, it calls
gets
on theIO::Delimited
.gets
works best when theIO
is peekable, because then it can quickly scan the delimiter (say, "\n" if no arguments are given) in the peek buffer.So, this PR also implements
peek
forIO::Delimited
, sogets
can use the optimized version.Output before this PR:
Output after this PR:
An extra fix
IO#gets
, whenpeek
was available, would use that. However, when it exhausted the peek buffer it would callpeek
again. Ifpeek
returnednil
, it would just stop consuming input right there. That's wrong! Whenpeek
returnsnil
it means theIO
can't be peeked. In reality this could probably never happen again because there was noIO
wherepeek
would sometimes returnnil
and sometimes a slice. But inIO::Delimited
's case that can happen: if thepeek
buffer contains a portion of the delimiter right at the beginning of the peek buffer, but not in its entirety, we can't know if the rest of the delimiter will come next or not. So we can't peek, andnil
is returned.How to review
I recommend going commit by commit.