Skip to content

Commit 54395c6

Browse files
rustyconovercodebytere
authored andcommitted
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 <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: David Carlier <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 2bf9a2d commit 54395c6

File tree

4 files changed

+50
-5
lines changed

4 files changed

+50
-5
lines changed

benchmark/tls/throughput.js

Lines changed: 1 addition & 1 deletion
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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 16 additions & 0 deletions
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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,7 @@ void TLSWrap::ClearIn() {
585585
AllocatedBuffer data = std::move(pending_cleartext_input_);
586586
crypto::MarkPopErrorOnReturn mark_pop_error_on_return;
587587

588+
crypto::NodeBIO::FromBIO(enc_out_)->set_allocate_tls_hint(data.size());
588589
int written = SSL_write(ssl_.get(), data.data(), data.size());
589590
Debug(this, "Writing %zu bytes, written = %d", data.size(), written);
590591
CHECK(written == -1 || written == static_cast<int>(data.size()));
@@ -699,8 +700,15 @@ int TLSWrap::DoWrite(WriteWrap* w,
699700

700701
size_t length = 0;
701702
size_t i;
702-
for (i = 0; i < count; i++)
703+
size_t nonempty_i = 0;
704+
size_t nonempty_count = 0;
705+
for (i = 0; i < count; i++) {
703706
length += bufs[i].len;
707+
if (bufs[i].len > 0) {
708+
nonempty_i = i;
709+
nonempty_count += 1;
710+
}
711+
}
704712

705713
// We want to trigger a Write() on the underlying stream to drive the stream
706714
// system, but don't want to encrypt empty buffers into a TLS frame, so see
@@ -744,20 +752,34 @@ int TLSWrap::DoWrite(WriteWrap* w,
744752
crypto::MarkPopErrorOnReturn mark_pop_error_on_return;
745753

746754
int written = 0;
747-
if (count != 1) {
755+
756+
// It is common for zero length buffers to be written,
757+
// don't copy data if there there is one buffer with data
758+
// and one or more zero length buffers.
759+
// _http_outgoing.js writes a zero length buffer in
760+
// in OutgoingMessage.prototype.end. If there was a large amount
761+
// of data supplied to end() there is no sense allocating
762+
// and copying it when it could just be used.
763+
764+
if (nonempty_count != 1) {
748765
data = env()->AllocateManaged(length);
749766
size_t offset = 0;
750767
for (i = 0; i < count; i++) {
751768
memcpy(data.data() + offset, bufs[i].base, bufs[i].len);
752769
offset += bufs[i].len;
753770
}
771+
772+
crypto::NodeBIO::FromBIO(enc_out_)->set_allocate_tls_hint(length);
754773
written = SSL_write(ssl_.get(), data.data(), length);
755774
} else {
756775
// Only one buffer: try to write directly, only store if it fails
757-
written = SSL_write(ssl_.get(), bufs[0].base, bufs[0].len);
776+
uv_buf_t* buf = &bufs[nonempty_i];
777+
crypto::NodeBIO::FromBIO(enc_out_)->set_allocate_tls_hint(buf->len);
778+
written = SSL_write(ssl_.get(), buf->base, buf->len);
779+
758780
if (written == -1) {
759781
data = env()->AllocateManaged(length);
760-
memcpy(data.data(), bufs[0].base, bufs[0].len);
782+
memcpy(data.data(), buf->base, buf->len);
761783
}
762784
}
763785

0 commit comments

Comments
 (0)