diff --git a/Docs/net-http-over-tls.md b/Docs/net-http-over-tls.md index 513217dc1..51d3b9690 100644 --- a/Docs/net-http-over-tls.md +++ b/Docs/net-http-over-tls.md @@ -158,8 +158,10 @@ As built: register their waiter with it. nio4r masks off the kernel's answer for what a hook owns. - `Transport__` is the hook of the TCP socket it runs on. Every read, write and handshake is one of SslStream's asynchronous operations, at most one of each kind in flight: - `#read_nonblock`, `#write_nonblock` (at most 16 KB handed over at a time, so what it reports - written is about what a kernel buffer holds) and `#connect_nonblock` start one and answer + `#read_nonblock`, `#write_nonblock` (16 KB records handed over for as long as each goes out + at once, up to 1 MB, so what it reports written is about what a kernel buffer holds; net/protocol + copies the rest of the body after every partial write, so 16 KB a call made a 64 MB body cost + gigabytes of copying) and `#connect_nonblock` start one and answer `:wait_readable`/`:wait_writable` while it runs; `#read`, `#write` and `#connect` start one or take over the one in flight, and wait for it. - Asking the hook whether a read would return starts that read. SslStream may hold bytes, diff --git a/Src/Libraries/OpenSSL/OpenSSL.cs b/Src/Libraries/OpenSSL/OpenSSL.cs index 022703dbe..9d90a30a6 100644 --- a/Src/Libraries/OpenSSL/OpenSSL.cs +++ b/Src/Libraries/OpenSSL/OpenSSL.cs @@ -592,10 +592,16 @@ public class Transport : IReadinessHook { private bool _closed; private readonly List/*!*/ _waiters = new List(); - // A TLS record carries at most 16 KB; a non-blocking write hands over no more than - // that at a time, so that what it reports as written is about what a kernel buffer holds. + // A TLS record carries at most 16 KB; a non-blocking write hands over one at a time, + // and goes on with the next only while the last one went out at once, so that what it + // reports as written is about what a kernel buffer holds. private const int ChunkSize = 16 * 1024; + // How much a non-blocking write hands over at most. net/protocol slices off what was + // written and passes the rest back, a copy of the rest every time: 16 KB a call made + // a 64 MB body cost gigabytes of copying before the kernel buffers were even full. + private const int NonBlockingWriteLimit = 1024 * 1024; + // The context's own CAs (ca_file, ca_path, cert_store): the self-signed ones // are the roots, the others may complete the chain. Null means the system // store's; _systemTrust says whether the system's count as well. @@ -1140,8 +1146,9 @@ public static int Write(Transport/*!*/ self, [DefaultProtocol, NotNull]MutableSt } /// - /// How much of data was handed over - at most 16 KB, which may still be on its way, - /// as bytes in a kernel buffer are - or :wait_writable while the last one is. + /// How much of data was handed over - 16 KB records for as long as they go out at once, + /// up to 1 MB; the last may still be on its way, as bytes in a kernel buffer are - or + /// :wait_writable while the last one is. /// [RubyMethod("write_nonblock")] public static object WriteNonBlocking(Transport/*!*/ self, [DefaultProtocol, NotNull]MutableString/*!*/ data) { @@ -1156,24 +1163,29 @@ public static object WriteNonBlocking(Transport/*!*/ self, [DefaultProtocol, Not if (pending != null) { self.FinishWrite(pending); } - byte[] bytes = data.ConvertToBytes(); - int count = Math.Min(bytes.Length, ChunkSize); - Task task; - lock (self._sync) { - var stream = self.Stream; - try { - task = stream.WriteAsync(bytes, 0, count); - } catch (Exception error) { - throw self.Translate(error); - } - if (!task.IsCompleted) { - self._pendingWrite = task; - self.NotifyOnCompletion(task); - return count; + // only what may go out: converting all of a large body on every call is a copy of it + byte[] bytes = data.GetBinarySlice(0, Math.Min(data.GetByteCount(), NonBlockingWriteLimit)); + int written = 0; + while (written < bytes.Length) { + int count = Math.Min(bytes.Length - written, ChunkSize); + Task task; + lock (self._sync) { + var stream = self.Stream; + try { + task = stream.WriteAsync(bytes, written, count); + } catch (Exception error) { + throw self.Translate(error); + } + if (!task.IsCompleted) { + self._pendingWrite = task; + self.NotifyOnCompletion(task); + return written + count; + } } + self.FinishWrite(task); + written += count; } - self.FinishWrite(task); - return count; + return written; } [RubyMethod("close")] diff --git a/Src/StdLib/ironruby/openssl/ssl.rb b/Src/StdLib/ironruby/openssl/ssl.rb index 1e2528df0..cc95af5dc 100644 --- a/Src/StdLib/ironruby/openssl/ssl.rb +++ b/Src/StdLib/ironruby/openssl/ssl.rb @@ -277,7 +277,8 @@ def write(*data) data.inject(0) { |total, item| total + syswrite(item.to_s) } end - # How much of +data+ went out - at most 16 KB at a time - or SSLErrorWaitWritable + # How much of +data+ went out - 16 KB records while they go out at once, at most + # 1 MB - or SSLErrorWaitWritable # (:wait_writable with exception: false) while the last part is still going. def write_nonblock(data, exception: true) connect unless @connected