Skip to content

Commit f2dd17b

Browse files
apapirovskiMylesBorins
authored andcommitted
timers: allow Immediates to be unrefed
Refactor Immediates handling to allow for them to be unrefed, similar to setTimeout, but without extra handles. Document the new `immediate.ref()` and `immediate.unref()` methods. Add SetImmediateUnref on the C++ side. Backport-PR-URL: #19006 PR-URL: #18139 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Franziska Hinkelmann <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 37f253e commit f2dd17b

10 files changed

+249
-102
lines changed

doc/api/timers.md

+32
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,38 @@ This object is created internally and is returned from [`setImmediate()`][]. It
1818
can be passed to [`clearImmediate()`][] in order to cancel the scheduled
1919
actions.
2020

21+
By default, when an immediate is scheduled, the Node.js event loop will continue
22+
running as long as the immediate is active. The `Immediate` object returned by
23+
[`setImmediate()`][] exports both `immediate.ref()` and `immediate.unref()`
24+
functions that can be used to control this default behavior.
25+
26+
### immediate.ref()
27+
<!-- YAML
28+
added: REPLACEME
29+
-->
30+
31+
When called, requests that the Node.js event loop *not* exit so long as the
32+
`Immediate` is active. Calling `immediate.ref()` multiple times will have no
33+
effect.
34+
35+
*Note*: By default, all `Immediate` objects are "ref'd", making it normally
36+
unnecessary to call `immediate.ref()` unless `immediate.unref()` had been called
37+
previously.
38+
39+
Returns a reference to the `Immediate`.
40+
41+
### immediate.unref()
42+
<!-- YAML
43+
added: REPLACEME
44+
-->
45+
46+
When called, the active `Immediate` object will not require the Node.js event
47+
loop to remain active. If there is no other activity keeping the event loop
48+
running, the process may exit before the `Immediate` object's callback is
49+
invoked. Calling `immediate.unref()` multiple times will have no effect.
50+
51+
Returns a reference to the `Immediate`.
52+
2153
## Class: Timeout
2254

2355
This object is created internally and is returned from [`setTimeout()`][] and

lib/timers.js

+84-59
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,14 @@ const trigger_async_id_symbol = Symbol('triggerAsyncId');
5252

5353
// *Must* match Environment::ImmediateInfo::Fields in src/env.h.
5454
const kCount = 0;
55-
const kHasOutstanding = 1;
55+
const kRefCount = 1;
56+
const kHasOutstanding = 2;
5657

57-
const [activateImmediateCheck, immediateInfo] =
58+
const [immediateInfo, toggleImmediateRef] =
5859
setImmediateCallback(processImmediate);
5960

61+
const kRefed = Symbol('refed');
62+
6063
// Timeout values > TIMEOUT_MAX are set to 1.
6164
const TIMEOUT_MAX = 2 ** 31 - 1;
6265

@@ -690,42 +693,41 @@ function processImmediate() {
690693
const queue = outstandingQueue.head !== null ?
691694
outstandingQueue : immediateQueue;
692695
var immediate = queue.head;
693-
var tail = queue.tail;
696+
const tail = queue.tail;
694697

695698
// Clear the linked list early in case new `setImmediate()` calls occur while
696699
// immediate callbacks are executed
697700
queue.head = queue.tail = null;
698701

699-
while (immediate !== null) {
700-
if (!immediate._onImmediate) {
701-
immediate = immediate._idleNext;
702-
continue;
703-
}
702+
let count = 0;
703+
let refCount = 0;
704704

705-
// Save next in case `clearImmediate(immediate)` is called from callback
706-
const next = immediate._idleNext;
705+
while (immediate !== null) {
706+
immediate._destroyed = true;
707707

708708
const asyncId = immediate[async_id_symbol];
709709
emitBefore(asyncId, immediate[trigger_async_id_symbol]);
710710

711-
tryOnImmediate(immediate, next, tail);
711+
count++;
712+
if (immediate[kRefed])
713+
refCount++;
714+
immediate[kRefed] = undefined;
715+
716+
tryOnImmediate(immediate, tail, count, refCount);
712717

713718
emitAfter(asyncId);
714719

715-
// If `clearImmediate(immediate)` wasn't called from the callback, use the
716-
// `immediate`'s next item
717-
if (immediate._idleNext !== null)
718-
immediate = immediate._idleNext;
719-
else
720-
immediate = next;
720+
immediate = immediate._idleNext;
721721
}
722722

723+
immediateInfo[kCount] -= count;
724+
immediateInfo[kRefCount] -= refCount;
723725
immediateInfo[kHasOutstanding] = 0;
724726
}
725727

726728
// An optimization so that the try/finally only de-optimizes (since at least v8
727729
// 4.7) what is in this smaller function.
728-
function tryOnImmediate(immediate, next, oldTail) {
730+
function tryOnImmediate(immediate, oldTail, count, refCount) {
729731
var threw = true;
730732
try {
731733
// make the actual call outside the try/finally to allow it to be optimized
@@ -734,21 +736,21 @@ function tryOnImmediate(immediate, next, oldTail) {
734736
} finally {
735737
immediate._onImmediate = null;
736738

737-
if (!immediate._destroyed) {
738-
immediate._destroyed = true;
739-
immediateInfo[kCount]--;
740-
741-
if (async_hook_fields[kDestroy] > 0) {
742-
emitDestroy(immediate[async_id_symbol]);
743-
}
739+
if (async_hook_fields[kDestroy] > 0) {
740+
emitDestroy(immediate[async_id_symbol]);
744741
}
745742

746-
if (threw && (immediate._idleNext !== null || next !== null)) {
747-
// Handle any remaining Immediates after error handling has resolved,
748-
// assuming we're still alive to do so.
749-
outstandingQueue.head = immediate._idleNext || next;
750-
outstandingQueue.tail = oldTail;
751-
immediateInfo[kHasOutstanding] = 1;
743+
if (threw) {
744+
immediateInfo[kCount] -= count;
745+
immediateInfo[kRefCount] -= refCount;
746+
747+
if (immediate._idleNext !== null) {
748+
// Handle any remaining Immediates after error handling has resolved,
749+
// assuming we're still alive to do so.
750+
outstandingQueue.head = immediate._idleNext;
751+
outstandingQueue.tail = oldTail;
752+
immediateInfo[kHasOutstanding] = 1;
753+
}
752754
}
753755
}
754756
}
@@ -763,31 +765,51 @@ function runCallback(timer) {
763765
}
764766

765767

766-
function Immediate(callback, args) {
767-
this._idleNext = null;
768-
this._idlePrev = null;
769-
// this must be set to null first to avoid function tracking
770-
// on the hidden class, revisit in V8 versions after 6.2
771-
this._onImmediate = null;
772-
this._onImmediate = callback;
773-
this._argv = args;
774-
this._destroyed = false;
768+
const Immediate = class Immediate {
769+
constructor(callback, args) {
770+
this._idleNext = null;
771+
this._idlePrev = null;
772+
// this must be set to null first to avoid function tracking
773+
// on the hidden class, revisit in V8 versions after 6.2
774+
this._onImmediate = null;
775+
this._onImmediate = callback;
776+
this._argv = args;
777+
this._destroyed = false;
778+
this[kRefed] = false;
779+
780+
this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter];
781+
this[trigger_async_id_symbol] = getDefaultTriggerAsyncId();
782+
if (async_hook_fields[kInit] > 0) {
783+
emitInit(this[async_id_symbol],
784+
'Immediate',
785+
this[trigger_async_id_symbol],
786+
this);
787+
}
775788

776-
this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter];
777-
this[trigger_async_id_symbol] = getDefaultTriggerAsyncId();
778-
if (async_hook_fields[kInit] > 0) {
779-
emitInit(this[async_id_symbol],
780-
'Immediate',
781-
this[trigger_async_id_symbol],
782-
this);
789+
this.ref();
790+
immediateInfo[kCount]++;
791+
792+
immediateQueue.append(this);
783793
}
784794

785-
if (immediateInfo[kCount] === 0)
786-
activateImmediateCheck();
787-
immediateInfo[kCount]++;
795+
ref() {
796+
if (this[kRefed] === false) {
797+
this[kRefed] = true;
798+
if (immediateInfo[kRefCount]++ === 0)
799+
toggleImmediateRef(true);
800+
}
801+
return this;
802+
}
788803

789-
immediateQueue.append(this);
790-
}
804+
unref() {
805+
if (this[kRefed] === true) {
806+
this[kRefed] = false;
807+
if (--immediateInfo[kRefCount] === 0)
808+
toggleImmediateRef(false);
809+
}
810+
return this;
811+
}
812+
};
791813

792814
function setImmediate(callback, arg1, arg2, arg3) {
793815
if (typeof callback !== 'function') {
@@ -827,15 +849,18 @@ exports.setImmediate = setImmediate;
827849

828850

829851
exports.clearImmediate = function(immediate) {
830-
if (!immediate) return;
852+
if (!immediate || immediate._destroyed)
853+
return;
831854

832-
if (!immediate._destroyed) {
833-
immediateInfo[kCount]--;
834-
immediate._destroyed = true;
855+
immediateInfo[kCount]--;
856+
immediate._destroyed = true;
835857

836-
if (async_hook_fields[kDestroy] > 0) {
837-
emitDestroy(immediate[async_id_symbol]);
838-
}
858+
if (immediate[kRefed] && --immediateInfo[kRefCount] === 0)
859+
toggleImmediateRef(false);
860+
immediate[kRefed] = undefined;
861+
862+
if (async_hook_fields[kDestroy] > 0) {
863+
emitDestroy(immediate[async_id_symbol]);
839864
}
840865

841866
immediate._onImmediate = null;

src/env-inl.h

+34-6
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ inline uint32_t Environment::ImmediateInfo::count() const {
229229
return fields_[kCount];
230230
}
231231

232+
inline uint32_t Environment::ImmediateInfo::ref_count() const {
233+
return fields_[kRefCount];
234+
}
235+
232236
inline bool Environment::ImmediateInfo::has_outstanding() const {
233237
return fields_[kHasOutstanding] == 1;
234238
}
@@ -241,6 +245,14 @@ inline void Environment::ImmediateInfo::count_dec(uint32_t decrement) {
241245
fields_[kCount] = fields_[kCount] - decrement;
242246
}
243247

248+
inline void Environment::ImmediateInfo::ref_count_inc(uint32_t increment) {
249+
fields_[kRefCount] = fields_[kRefCount] + increment;
250+
}
251+
252+
inline void Environment::ImmediateInfo::ref_count_dec(uint32_t decrement) {
253+
fields_[kRefCount] = fields_[kRefCount] - decrement;
254+
}
255+
244256
inline Environment::TickInfo::TickInfo(v8::Isolate* isolate)
245257
: fields_(isolate, kFieldsCount) {}
246258

@@ -514,20 +526,36 @@ inline void Environment::set_fs_stats_field_array(double* fields) {
514526
fs_stats_field_array_ = fields;
515527
}
516528

517-
void Environment::SetImmediate(native_immediate_callback cb,
529+
void Environment::CreateImmediate(native_immediate_callback cb,
518530
void* data,
519-
v8::Local<v8::Object> obj) {
531+
v8::Local<v8::Object> obj,
532+
bool ref) {
520533
native_immediate_callbacks_.push_back({
521534
cb,
522535
data,
523-
std::unique_ptr<v8::Persistent<v8::Object>>(
524-
obj.IsEmpty() ? nullptr : new v8::Persistent<v8::Object>(isolate_, obj))
536+
std::unique_ptr<v8::Persistent<v8::Object>>(obj.IsEmpty() ?
537+
nullptr : new v8::Persistent<v8::Object>(isolate_, obj)),
538+
ref
525539
});
526-
if (immediate_info()->count() == 0)
527-
ActivateImmediateCheck();
528540
immediate_info()->count_inc(1);
529541
}
530542

543+
void Environment::SetImmediate(native_immediate_callback cb,
544+
void* data,
545+
v8::Local<v8::Object> obj) {
546+
CreateImmediate(cb, data, obj, true);
547+
548+
if (immediate_info()->ref_count() == 0)
549+
ToggleImmediateRef(true);
550+
immediate_info()->ref_count_inc(1);
551+
}
552+
553+
void Environment::SetUnrefImmediate(native_immediate_callback cb,
554+
void* data,
555+
v8::Local<v8::Object> obj) {
556+
CreateImmediate(cb, data, obj, false);
557+
}
558+
531559
inline performance::performance_state* Environment::performance_state() {
532560
return performance_state_.get();
533561
}

src/env.cc

+19-17
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ void Environment::Start(int argc,
8080

8181
uv_idle_init(event_loop(), immediate_idle_handle());
8282

83+
uv_check_start(immediate_check_handle(), CheckImmediate);
84+
8385
// Inform V8's CPU profiler when we're idle. The profiler is sampling-based
8486
// but not all samples are created equal; mark the wall clock time spent in
8587
// epoll_wait() and friends so profiling tools can filter it out. The samples
@@ -272,39 +274,35 @@ void Environment::EnvPromiseHook(v8::PromiseHookType type,
272274
void Environment::RunAndClearNativeImmediates() {
273275
size_t count = native_immediate_callbacks_.size();
274276
if (count > 0) {
277+
size_t ref_count = 0;
275278
std::vector<NativeImmediateCallback> list;
276279
native_immediate_callbacks_.swap(list);
277280
for (const auto& cb : list) {
278281
cb.cb_(this, cb.data_);
279282
if (cb.keep_alive_)
280283
cb.keep_alive_->Reset();
284+
if (cb.refed_)
285+
ref_count++;
281286
}
282287

283288
#ifdef DEBUG
284289
CHECK_GE(immediate_info()->count(), count);
285290
#endif
286291
immediate_info()->count_dec(count);
292+
immediate_info()->ref_count_dec(ref_count);
287293
}
288294
}
289295

290-
static bool MaybeStopImmediate(Environment* env) {
291-
if (env->immediate_info()->count() == 0) {
292-
uv_check_stop(env->immediate_check_handle());
293-
uv_idle_stop(env->immediate_idle_handle());
294-
return true;
295-
}
296-
return false;
297-
}
298-
299296

300297
void Environment::CheckImmediate(uv_check_t* handle) {
301298
Environment* env = Environment::from_immediate_check_handle(handle);
302-
HandleScope scope(env->isolate());
303-
Context::Scope context_scope(env->context());
304299

305-
if (MaybeStopImmediate(env))
300+
if (env->immediate_info()->count() == 0)
306301
return;
307302

303+
HandleScope scope(env->isolate());
304+
Context::Scope context_scope(env->context());
305+
308306
env->RunAndClearNativeImmediates();
309307

310308
do {
@@ -316,13 +314,17 @@ void Environment::CheckImmediate(uv_check_t* handle) {
316314
{0, 0}).ToLocalChecked();
317315
} while (env->immediate_info()->has_outstanding());
318316

319-
MaybeStopImmediate(env);
317+
if (env->immediate_info()->ref_count() == 0)
318+
env->ToggleImmediateRef(false);
320319
}
321320

322-
void Environment::ActivateImmediateCheck() {
323-
uv_check_start(&immediate_check_handle_, CheckImmediate);
324-
// Idle handle is needed only to stop the event loop from blocking in poll.
325-
uv_idle_start(&immediate_idle_handle_, [](uv_idle_t*){ });
321+
void Environment::ToggleImmediateRef(bool ref) {
322+
if (ref) {
323+
// Idle handle is needed only to stop the event loop from blocking in poll.
324+
uv_idle_start(immediate_idle_handle(), [](uv_idle_t*){ });
325+
} else {
326+
uv_idle_stop(immediate_idle_handle());
327+
}
326328
}
327329

328330
void Environment::AsyncHooks::grow_async_ids_stack() {

0 commit comments

Comments
 (0)