Skip to content

Commit dae7130

Browse files
addaleaxtargos
authored andcommitted
zlib: track memory allocated by zlib
Provide a custom memory allocator for zlib, and track memory allocated by the library with it. Right now this “only” makes reported external memory usage (much) more accurate and protects against zlib memory leaks, but it generally will enable us to give more accurate memory usage statistics for zlib. PR-URL: #21608 Reviewed-By: Daniel Bevenius <[email protected]> Reviewed-By: Michael Dawson <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 07160cd commit dae7130

File tree

1 file changed

+64
-13
lines changed

1 file changed

+64
-13
lines changed

src/node_zlib.cc

+64-13
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <stdlib.h>
3535
#include <string.h>
3636
#include <sys/types.h>
37+
#include <atomic>
3738

3839
namespace node {
3940

@@ -97,6 +98,8 @@ class ZCtx : public AsyncWrap, public ThreadPoolWork {
9798
~ZCtx() override {
9899
CHECK_EQ(false, write_in_progress_ && "write in progress");
99100
Close();
101+
CHECK_EQ(zlib_memory_, 0);
102+
CHECK_EQ(unreported_allocations_, 0);
100103
}
101104

102105
void Close() {
@@ -109,17 +112,15 @@ class ZCtx : public AsyncWrap, public ThreadPoolWork {
109112
CHECK(init_done_ && "close before init");
110113
CHECK_LE(mode_, UNZIP);
111114

115+
AllocScope alloc_scope(this);
112116
int status = Z_OK;
113117
if (mode_ == DEFLATE || mode_ == GZIP || mode_ == DEFLATERAW) {
114118
status = deflateEnd(&strm_);
115-
int64_t change_in_bytes = -static_cast<int64_t>(kDeflateContextSize);
116-
env()->isolate()->AdjustAmountOfExternalAllocatedMemory(change_in_bytes);
117119
} else if (mode_ == INFLATE || mode_ == GUNZIP || mode_ == INFLATERAW ||
118120
mode_ == UNZIP) {
119121
status = inflateEnd(&strm_);
120-
int64_t change_in_bytes = -static_cast<int64_t>(kInflateContextSize);
121-
env()->isolate()->AdjustAmountOfExternalAllocatedMemory(change_in_bytes);
122122
}
123+
123124
CHECK(status == Z_OK || status == Z_DATA_ERROR);
124125
mode_ = NONE;
125126

@@ -165,6 +166,8 @@ class ZCtx : public AsyncWrap, public ThreadPoolWork {
165166
CHECK(0 && "Invalid flush value");
166167
}
167168

169+
AllocScope alloc_scope(ctx);
170+
168171
Bytef* in;
169172
Bytef* out;
170173
size_t in_off, in_len, out_off, out_len;
@@ -355,6 +358,8 @@ class ZCtx : public AsyncWrap, public ThreadPoolWork {
355358

356359
// v8 land!
357360
void AfterThreadPoolWork(int status) override {
361+
AllocScope alloc_scope(this);
362+
358363
write_in_progress_ = false;
359364

360365
if (status == UV_ECANCELED) {
@@ -505,14 +510,15 @@ class ZCtx : public AsyncWrap, public ThreadPoolWork {
505510
int strategy, uint32_t* write_result,
506511
Local<Function> write_js_callback, char* dictionary,
507512
size_t dictionary_len) {
513+
AllocScope alloc_scope(ctx);
508514
ctx->level_ = level;
509515
ctx->windowBits_ = windowBits;
510516
ctx->memLevel_ = memLevel;
511517
ctx->strategy_ = strategy;
512518

513-
ctx->strm_.zalloc = Z_NULL;
514-
ctx->strm_.zfree = Z_NULL;
515-
ctx->strm_.opaque = Z_NULL;
519+
ctx->strm_.zalloc = AllocForZlib;
520+
ctx->strm_.zfree = FreeForZlib;
521+
ctx->strm_.opaque = static_cast<void*>(ctx);
516522

517523
ctx->flush_ = Z_NO_FLUSH;
518524

@@ -540,16 +546,12 @@ class ZCtx : public AsyncWrap, public ThreadPoolWork {
540546
ctx->windowBits_,
541547
ctx->memLevel_,
542548
ctx->strategy_);
543-
ctx->env()->isolate()
544-
->AdjustAmountOfExternalAllocatedMemory(kDeflateContextSize);
545549
break;
546550
case INFLATE:
547551
case GUNZIP:
548552
case INFLATERAW:
549553
case UNZIP:
550554
ctx->err_ = inflateInit2(&ctx->strm_, ctx->windowBits_);
551-
ctx->env()->isolate()
552-
->AdjustAmountOfExternalAllocatedMemory(kInflateContextSize);
553555
break;
554556
default:
555557
UNREACHABLE();
@@ -605,6 +607,8 @@ class ZCtx : public AsyncWrap, public ThreadPoolWork {
605607
}
606608

607609
static void Params(ZCtx* ctx, int level, int strategy) {
610+
AllocScope alloc_scope(ctx);
611+
608612
ctx->err_ = Z_OK;
609613

610614
switch (ctx->mode_) {
@@ -622,6 +626,8 @@ class ZCtx : public AsyncWrap, public ThreadPoolWork {
622626
}
623627

624628
void Reset() {
629+
AllocScope alloc_scope(this);
630+
625631
err_ = Z_OK;
626632

627633
switch (mode_) {
@@ -660,8 +666,51 @@ class ZCtx : public AsyncWrap, public ThreadPoolWork {
660666
}
661667
}
662668

663-
static const int kDeflateContextSize = 16384; // approximate
664-
static const int kInflateContextSize = 10240; // approximate
669+
// Allocation functions provided to zlib itself. We store the real size of
670+
// the allocated memory chunk just before the "payload" memory we return
671+
// to zlib.
672+
// Because we use zlib off the thread pool, we can not report memory directly
673+
// to V8; rather, we first store it as "unreported" memory in a separate
674+
// field and later report it back from the main thread.
675+
static void* AllocForZlib(void* data, uInt items, uInt size) {
676+
ZCtx* ctx = static_cast<ZCtx*>(data);
677+
size_t real_size =
678+
MultiplyWithOverflowCheck(static_cast<size_t>(items),
679+
static_cast<size_t>(size)) + sizeof(size_t);
680+
char* memory = UncheckedMalloc(real_size);
681+
if (UNLIKELY(memory == nullptr)) return nullptr;
682+
*reinterpret_cast<size_t*>(memory) = real_size;
683+
ctx->unreported_allocations_.fetch_add(real_size,
684+
std::memory_order_relaxed);
685+
return memory + sizeof(size_t);
686+
}
687+
688+
static void FreeForZlib(void* data, void* pointer) {
689+
if (UNLIKELY(pointer == nullptr)) return;
690+
ZCtx* ctx = static_cast<ZCtx*>(data);
691+
char* real_pointer = static_cast<char*>(pointer) - sizeof(size_t);
692+
size_t real_size = *reinterpret_cast<size_t*>(real_pointer);
693+
ctx->unreported_allocations_.fetch_sub(real_size,
694+
std::memory_order_relaxed);
695+
free(real_pointer);
696+
}
697+
698+
// This is called on the main thread after zlib may have allocated something
699+
// in order to report it back to V8.
700+
void AdjustAmountOfExternalAllocatedMemory() {
701+
ssize_t report =
702+
unreported_allocations_.exchange(0, std::memory_order_relaxed);
703+
if (report == 0) return;
704+
CHECK_IMPLIES(report < 0, zlib_memory_ >= static_cast<size_t>(-report));
705+
zlib_memory_ += report;
706+
env()->isolate()->AdjustAmountOfExternalAllocatedMemory(report);
707+
}
708+
709+
struct AllocScope {
710+
explicit AllocScope(ZCtx* ctx) : ctx(ctx) {}
711+
~AllocScope() { ctx->AdjustAmountOfExternalAllocatedMemory(); }
712+
ZCtx* ctx;
713+
};
665714

666715
Bytef* dictionary_;
667716
size_t dictionary_len_;
@@ -680,6 +729,8 @@ class ZCtx : public AsyncWrap, public ThreadPoolWork {
680729
unsigned int gzip_id_bytes_read_;
681730
uint32_t* write_result_;
682731
Persistent<Function> write_js_callback_;
732+
std::atomic<ssize_t> unreported_allocations_{0};
733+
size_t zlib_memory_ = 0;
683734
};
684735

685736

0 commit comments

Comments
 (0)