@@ -36,33 +36,30 @@ using v8::Value;
3636namespace quic {
3737
3838// ============================================================================
39- // Thread-local QUIC allocator.
39+ // QUIC allocator.
4040//
41- // Both ngtcp2 and nghttp3 take an allocator struct (ngtcp2_mem /
42- // nghttp3_mem) whose pointer is stored inside every object they
43- // allocate. Some of those objects — notably nghttp3 rcbufs backing
44- // V8 external strings — can outlive the BindingData that created them
45- // (freed during V8 isolate teardown, after Environment cleanup).
46- //
47- // To handle this safely, both allocators live in a thread-local static
48- // struct that is never destroyed. Memory tracking goes through the
49- // BindingData pointer when it is alive and is silently skipped during
50- // teardown (after ~BindingData nulls the pointer).
41+ // ngtcp2 and nghttp3 keep a pointer to their allocator struct in every object
42+ // they allocate, and nghttp3 rcbufs backing V8 external strings can be freed
43+ // after the BindingData is gone. A QuicAllocState is therefore deleted only
44+ // once its BindingData has been destroyed and its last allocation freed.
5145//
5246// The allocation functions use the same prepended-size-header scheme as
5347// NgLibMemoryManager (node_mem-inl.h) so that frees always know the
5448// allocation size regardless of whether BindingData is still around.
5549
56- namespace {
5750struct QuicAllocState {
58- BindingData* binding = nullptr ;
51+ BindingData* binding;
52+ size_t live_allocations = 0 ;
5953 ngtcp2_mem ngtcp2 = {};
6054 nghttp3_mem nghttp3 = {};
55+
56+ void OnFreed () {
57+ CHECK_GT (live_allocations, 0 );
58+ if (--live_allocations == 0 && binding == nullptr ) delete this ;
59+ }
6160};
62- thread_local QuicAllocState quic_alloc_state;
6361
64- // Core allocation functions shared by both ngtcp2 and nghttp3.
65- // user_data always points to the thread-local QuicAllocState.
62+ namespace {
6663
6764void * QuicRealloc (void * ptr, size_t size, void * user_data) {
6865 auto * state = static_cast <QuicAllocState*>(user_data);
@@ -77,6 +74,10 @@ void* QuicRealloc(void* ptr, size_t size, void* user_data) {
7774 previous_size = *reinterpret_cast <size_t *>(original_ptr);
7875 if (previous_size == 0 ) {
7976 char * ret = UncheckedRealloc (original_ptr, size);
77+ if (size == 0 ) {
78+ state->OnFreed ();
79+ return nullptr ;
80+ }
8081 if (ret != nullptr ) ret += kReserveSizeAndAlign ;
8182 return ret;
8283 }
@@ -95,6 +96,7 @@ void* QuicRealloc(void* ptr, size_t size, void* user_data) {
9596 state->binding ->env ()->external_memory_accounter ()->Update (
9697 state->binding ->env ()->isolate (), new_size);
9798 }
99+ if (ptr == nullptr ) state->live_allocations ++;
98100 *reinterpret_cast <size_t *>(mem) = size;
99101 mem += kReserveSizeAndAlign ;
100102 } else if (size == 0 ) {
@@ -103,6 +105,7 @@ void* QuicRealloc(void* ptr, size_t size, void* user_data) {
103105 state->binding ->env ()->external_memory_accounter ()->Decrease (
104106 state->binding ->env ()->isolate (), previous_size);
105107 }
108+ if (ptr != nullptr ) state->OnFreed ();
106109 }
107110 return mem;
108111}
@@ -231,35 +234,20 @@ BindingData& BindingData::Get(Environment* env) {
231234}
232235
233236BindingData::~BindingData () {
234- quic_alloc_state.binding = nullptr ;
237+ alloc_state_->binding = nullptr ;
238+ if (alloc_state_->live_allocations == 0 ) delete alloc_state_;
235239 // flush_check_ is cleaned up by ~CheckWrapHandle() after the destructor
236240 // body completes. The inner CheckWrap (and its uv_check_t) will be freed
237241 // later by the uv_close callback, after CleanupHandles() runs uv_run().
238242 pending_flush_sessions_.clear ();
239243}
240244
241245ngtcp2_mem* BindingData::ngtcp2_allocator () {
242- quic_alloc_state.binding = this ;
243- quic_alloc_state.ngtcp2 = {
244- &quic_alloc_state,
245- Ngtcp2Malloc,
246- Ngtcp2Free,
247- Ngtcp2Calloc,
248- Ngtcp2Realloc,
249- };
250- return &quic_alloc_state.ngtcp2 ;
246+ return &alloc_state_->ngtcp2 ;
251247}
252248
253249nghttp3_mem* BindingData::nghttp3_allocator () {
254- quic_alloc_state.binding = this ;
255- quic_alloc_state.nghttp3 = {
256- &quic_alloc_state,
257- Nghttp3Malloc,
258- Nghttp3Free,
259- Nghttp3Calloc,
260- Nghttp3Realloc,
261- };
262- return &quic_alloc_state.nghttp3 ;
250+ return &alloc_state_->nghttp3 ;
263251}
264252
265253void BindingData::CheckAllocatedSize (size_t previous_size) const {
@@ -348,7 +336,12 @@ JS_METHOD_IMPL(BindingData::SetHeadersInterest) {
348336
349337BindingData::BindingData (Realm* realm, Local<Object> object)
350338 : BaseObject(realm, object),
339+ alloc_state_ (new QuicAllocState{this }),
351340 flush_check_(env(), [this]() { OnFlushCheck (); }) {
341+ alloc_state_->ngtcp2 = {
342+ alloc_state_, Ngtcp2Malloc, Ngtcp2Free, Ngtcp2Calloc, Ngtcp2Realloc};
343+ alloc_state_->nghttp3 = {
344+ alloc_state_, Nghttp3Malloc, Nghttp3Free, Nghttp3Calloc, Nghttp3Realloc};
352345 MakeWeak ();
353346 // Unref so the check handle doesn't keep the event loop alive on its own.
354347 flush_check_.Unref ();
0 commit comments