Skip to content

Commit 2c0b249

Browse files
rustyconovercodebytere
authored andcommittedFeb 29, 2020
tls: reduce memory copying and number of BIO buffer allocations
Avoid copying buffers before passing to SSL_write if there are zero length buffers involved. Only copy the data when the buffer has a non zero length. Send a memory allocation hint to the crypto BIO about how much memory will likely be needed to be allocated by the next call to SSL_write. This makes a single allocation rather than the BIO allocating a buffer for each 16k TLS segment written. This solves a problem with large buffers written over TLS triggering V8's GC. PR-URL: #31499 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: David Carlier <devnexen@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent b6d33f6 commit 2c0b249

File tree

4 files changed

+50
-5
lines changed

4 files changed

+50
-5
lines changed
 

‎benchmark/tls/throughput.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const common = require('../common.js');
33
const bench = common.createBenchmark(main, {
44
dur: [5],
55
type: ['buf', 'asc', 'utf'],
6-
size: [2, 1024, 1024 * 1024]
6+
size: [2, 1024, 1024 * 1024, 4 * 1024 * 1024, 16 * 1024 * 1024]
77
});
88

99
const fixtures = require('../../test/common/fixtures');

‎src/node_crypto_bio.cc

+7
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,13 @@ void NodeBIO::TryAllocateForWrite(size_t hint) {
438438
kThroughputBufferLength;
439439
if (len < hint)
440440
len = hint;
441+
442+
// If there is a one time allocation size hint, use it.
443+
if (allocate_hint_ > len) {
444+
len = allocate_hint_;
445+
allocate_hint_ = 0;
446+
}
447+
441448
Buffer* next = new Buffer(env_, len);
442449

443450
if (w == nullptr) {

‎src/node_crypto_bio.h

+16
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ class NodeBIO : public MemoryRetainer {
9696
return length_;
9797
}
9898

99+
// Provide a hint about the size of the next pending set of writes. TLS
100+
// writes records of a maximum length of 16k of data plus a 5-byte header,
101+
// a MAC (up to 20 bytes for SSLv3, TLS 1.0, TLS 1.1, and up to 32 bytes
102+
// for TLS 1.2), and padding if a block cipher is used. If there is a
103+
// large write this will result in potentially many buffers being
104+
// allocated and gc'ed which can cause long pauses. By providing a
105+
// guess about the amount of buffer space that will be needed in the
106+
// next allocation this overhead is removed.
107+
inline void set_allocate_tls_hint(size_t size) {
108+
constexpr size_t kThreshold = 16 * 1024;
109+
if (size >= kThreshold) {
110+
allocate_hint_ = (size / kThreshold + 1) * (kThreshold + 5 + 32);
111+
}
112+
}
113+
99114
inline void set_eof_return(int num) {
100115
eof_return_ = num;
101116
}
@@ -164,6 +179,7 @@ class NodeBIO : public MemoryRetainer {
164179
Environment* env_ = nullptr;
165180
size_t initial_ = kInitialBufferLength;
166181
size_t length_ = 0;
182+
size_t allocate_hint_ = 0;
167183
int eof_return_ = -1;
168184
Buffer* read_head_ = nullptr;
169185
Buffer* write_head_ = nullptr;

‎src/tls_wrap.cc

+26-4
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ void TLSWrap::ClearIn() {
587587
AllocatedBuffer data = std::move(pending_cleartext_input_);
588588
crypto::MarkPopErrorOnReturn mark_pop_error_on_return;
589589

590+
crypto::NodeBIO::FromBIO(enc_out_)->set_allocate_tls_hint(data.size());
590591
int written = SSL_write(ssl_.get(), data.data(), data.size());
591592
Debug(this, "Writing %zu bytes, written = %d", data.size(), written);
592593
CHECK(written == -1 || written == static_cast<int>(data.size()));
@@ -701,8 +702,15 @@ int TLSWrap::DoWrite(WriteWrap* w,
701702

702703
size_t length = 0;
703704
size_t i;
704-
for (i = 0; i < count; i++)
705+
size_t nonempty_i = 0;
706+
size_t nonempty_count = 0;
707+
for (i = 0; i < count; i++) {
705708
length += bufs[i].len;
709+
if (bufs[i].len > 0) {
710+
nonempty_i = i;
711+
nonempty_count += 1;
712+
}
713+
}
706714

707715
// We want to trigger a Write() on the underlying stream to drive the stream
708716
// system, but don't want to encrypt empty buffers into a TLS frame, so see
@@ -747,20 +755,34 @@ int TLSWrap::DoWrite(WriteWrap* w,
747755
crypto::MarkPopErrorOnReturn mark_pop_error_on_return;
748756

749757
int written = 0;
750-
if (count != 1) {
758+
759+
// It is common for zero length buffers to be written,
760+
// don't copy data if there there is one buffer with data
761+
// and one or more zero length buffers.
762+
// _http_outgoing.js writes a zero length buffer in
763+
// in OutgoingMessage.prototype.end. If there was a large amount
764+
// of data supplied to end() there is no sense allocating
765+
// and copying it when it could just be used.
766+
767+
if (nonempty_count != 1) {
751768
data = env()->AllocateManaged(length);
752769
size_t offset = 0;
753770
for (i = 0; i < count; i++) {
754771
memcpy(data.data() + offset, bufs[i].base, bufs[i].len);
755772
offset += bufs[i].len;
756773
}
774+
775+
crypto::NodeBIO::FromBIO(enc_out_)->set_allocate_tls_hint(length);
757776
written = SSL_write(ssl_.get(), data.data(), length);
758777
} else {
759778
// Only one buffer: try to write directly, only store if it fails
760-
written = SSL_write(ssl_.get(), bufs[0].base, bufs[0].len);
779+
uv_buf_t* buf = &bufs[nonempty_i];
780+
crypto::NodeBIO::FromBIO(enc_out_)->set_allocate_tls_hint(buf->len);
781+
written = SSL_write(ssl_.get(), buf->base, buf->len);
782+
761783
if (written == -1) {
762784
data = env()->AllocateManaged(length);
763-
memcpy(data.data(), bufs[0].base, bufs[0].len);
785+
memcpy(data.data(), buf->base, buf->len);
764786
}
765787
}
766788

0 commit comments

Comments
 (0)
Please sign in to comment.