Skip to content

Commit 81cd382

Browse files
apapirovskiMayaLekova
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. PR-URL: nodejs#18139 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Franziska Hinkelmann <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent ec0bdda commit 81cd382

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
@@ -53,11 +53,14 @@ const trigger_async_id_symbol = timerInternals.trigger_async_id_symbol;
5353

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

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

62+
const kRefed = Symbol('refed');
63+
6164
// The Timeout class
6265
const Timeout = timerInternals.Timeout;
6366

@@ -656,42 +659,41 @@ function processImmediate() {
656659
const queue = outstandingQueue.head !== null ?
657660
outstandingQueue : immediateQueue;
658661
var immediate = queue.head;
659-
var tail = queue.tail;
662+
const tail = queue.tail;
660663

661664
// Clear the linked list early in case new `setImmediate()` calls occur while
662665
// immediate callbacks are executed
663666
queue.head = queue.tail = null;
664667

665-
while (immediate !== null) {
666-
if (!immediate._onImmediate) {
667-
immediate = immediate._idleNext;
668-
continue;
669-
}
668+
let count = 0;
669+
let refCount = 0;
670670

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

674674
const asyncId = immediate[async_id_symbol];
675675
emitBefore(asyncId, immediate[trigger_async_id_symbol]);
676676

677-
tryOnImmediate(immediate, next, tail);
677+
count++;
678+
if (immediate[kRefed])
679+
refCount++;
680+
immediate[kRefed] = undefined;
681+
682+
tryOnImmediate(immediate, tail, count, refCount);
678683

679684
emitAfter(asyncId);
680685

681-
// If `clearImmediate(immediate)` wasn't called from the callback, use the
682-
// `immediate`'s next item
683-
if (immediate._idleNext !== null)
684-
immediate = immediate._idleNext;
685-
else
686-
immediate = next;
686+
immediate = immediate._idleNext;
687687
}
688688

689+
immediateInfo[kCount] -= count;
690+
immediateInfo[kRefCount] -= refCount;
689691
immediateInfo[kHasOutstanding] = 0;
690692
}
691693

692694
// An optimization so that the try/finally only de-optimizes (since at least v8
693695
// 4.7) what is in this smaller function.
694-
function tryOnImmediate(immediate, next, oldTail) {
696+
function tryOnImmediate(immediate, oldTail, count, refCount) {
695697
var threw = true;
696698
try {
697699
// make the actual call outside the try/finally to allow it to be optimized
@@ -700,21 +702,21 @@ function tryOnImmediate(immediate, next, oldTail) {
700702
} finally {
701703
immediate._onImmediate = null;
702704

703-
if (!immediate._destroyed) {
704-
immediate._destroyed = true;
705-
immediateInfo[kCount]--;
706-
707-
if (async_hook_fields[kDestroy] > 0) {
708-
emitDestroy(immediate[async_id_symbol]);
709-
}
705+
if (async_hook_fields[kDestroy] > 0) {
706+
emitDestroy(immediate[async_id_symbol]);
710707
}
711708

712-
if (threw && (immediate._idleNext !== null || next !== null)) {
713-
// Handle any remaining Immediates after error handling has resolved,
714-
// assuming we're still alive to do so.
715-
outstandingQueue.head = immediate._idleNext || next;
716-
outstandingQueue.tail = oldTail;
717-
immediateInfo[kHasOutstanding] = 1;
709+
if (threw) {
710+
immediateInfo[kCount] -= count;
711+
immediateInfo[kRefCount] -= refCount;
712+
713+
if (immediate._idleNext !== null) {
714+
// Handle any remaining Immediates after error handling has resolved,
715+
// assuming we're still alive to do so.
716+
outstandingQueue.head = immediate._idleNext;
717+
outstandingQueue.tail = oldTail;
718+
immediateInfo[kHasOutstanding] = 1;
719+
}
718720
}
719721
}
720722
}
@@ -729,31 +731,51 @@ function runCallback(timer) {
729731
}
730732

731733

732-
function Immediate(callback, args) {
733-
this._idleNext = null;
734-
this._idlePrev = null;
735-
// this must be set to null first to avoid function tracking
736-
// on the hidden class, revisit in V8 versions after 6.2
737-
this._onImmediate = null;
738-
this._onImmediate = callback;
739-
this._argv = args;
740-
this._destroyed = false;
734+
const Immediate = class Immediate {
735+
constructor(callback, args) {
736+
this._idleNext = null;
737+
this._idlePrev = null;
738+
// this must be set to null first to avoid function tracking
739+
// on the hidden class, revisit in V8 versions after 6.2
740+
this._onImmediate = null;
741+
this._onImmediate = callback;
742+
this._argv = args;
743+
this._destroyed = false;
744+
this[kRefed] = false;
745+
746+
this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter];
747+
this[trigger_async_id_symbol] = getDefaultTriggerAsyncId();
748+
if (async_hook_fields[kInit] > 0) {
749+
emitInit(this[async_id_symbol],
750+
'Immediate',
751+
this[trigger_async_id_symbol],
752+
this);
753+
}
754+
755+
this.ref();
756+
immediateInfo[kCount]++;
741757

742-
this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter];
743-
this[trigger_async_id_symbol] = getDefaultTriggerAsyncId();
744-
if (async_hook_fields[kInit] > 0) {
745-
emitInit(this[async_id_symbol],
746-
'Immediate',
747-
this[trigger_async_id_symbol],
748-
this);
758+
immediateQueue.append(this);
749759
}
750760

751-
if (immediateInfo[kCount] === 0)
752-
activateImmediateCheck();
753-
immediateInfo[kCount]++;
761+
ref() {
762+
if (this[kRefed] === false) {
763+
this[kRefed] = true;
764+
if (immediateInfo[kRefCount]++ === 0)
765+
toggleImmediateRef(true);
766+
}
767+
return this;
768+
}
754769

755-
immediateQueue.append(this);
756-
}
770+
unref() {
771+
if (this[kRefed] === true) {
772+
this[kRefed] = false;
773+
if (--immediateInfo[kRefCount] === 0)
774+
toggleImmediateRef(false);
775+
}
776+
return this;
777+
}
778+
};
757779

758780
function setImmediate(callback, arg1, arg2, arg3) {
759781
if (typeof callback !== 'function') {
@@ -793,15 +815,18 @@ exports.setImmediate = setImmediate;
793815

794816

795817
exports.clearImmediate = function(immediate) {
796-
if (!immediate) return;
818+
if (!immediate || immediate._destroyed)
819+
return;
797820

798-
if (!immediate._destroyed) {
799-
immediateInfo[kCount]--;
800-
immediate._destroyed = true;
821+
immediateInfo[kCount]--;
822+
immediate._destroyed = true;
801823

802-
if (async_hook_fields[kDestroy] > 0) {
803-
emitDestroy(immediate[async_id_symbol]);
804-
}
824+
if (immediate[kRefed] && --immediateInfo[kRefCount] === 0)
825+
toggleImmediateRef(false);
826+
immediate[kRefed] = undefined;
827+
828+
if (async_hook_fields[kDestroy] > 0) {
829+
emitDestroy(immediate[async_id_symbol]);
805830
}
806831

807832
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

@@ -536,20 +548,36 @@ inline void Environment::set_fs_stats_field_array(double* fields) {
536548
fs_stats_field_array_ = fields;
537549
}
538550

539-
void Environment::SetImmediate(native_immediate_callback cb,
551+
void Environment::CreateImmediate(native_immediate_callback cb,
540552
void* data,
541-
v8::Local<v8::Object> obj) {
553+
v8::Local<v8::Object> obj,
554+
bool ref) {
542555
native_immediate_callbacks_.push_back({
543556
cb,
544557
data,
545-
std::unique_ptr<v8::Persistent<v8::Object>>(
546-
obj.IsEmpty() ? nullptr : new v8::Persistent<v8::Object>(isolate_, obj))
558+
std::unique_ptr<v8::Persistent<v8::Object>>(obj.IsEmpty() ?
559+
nullptr : new v8::Persistent<v8::Object>(isolate_, obj)),
560+
ref
547561
});
548-
if (immediate_info()->count() == 0)
549-
ActivateImmediateCheck();
550562
immediate_info()->count_inc(1);
551563
}
552564

565+
void Environment::SetImmediate(native_immediate_callback cb,
566+
void* data,
567+
v8::Local<v8::Object> obj) {
568+
CreateImmediate(cb, data, obj, true);
569+
570+
if (immediate_info()->ref_count() == 0)
571+
ToggleImmediateRef(true);
572+
immediate_info()->ref_count_inc(1);
573+
}
574+
575+
void Environment::SetUnrefImmediate(native_immediate_callback cb,
576+
void* data,
577+
v8::Local<v8::Object> obj) {
578+
CreateImmediate(cb, data, obj, false);
579+
}
580+
553581
inline performance::performance_state* Environment::performance_state() {
554582
return performance_state_;
555583
}

src/env.cc

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

8383
uv_idle_init(event_loop(), immediate_idle_handle());
8484

85+
uv_check_start(immediate_check_handle(), CheckImmediate);
86+
8587
// Inform V8's CPU profiler when we're idle. The profiler is sampling-based
8688
// but not all samples are created equal; mark the wall clock time spent in
8789
// epoll_wait() and friends so profiling tools can filter it out. The samples
@@ -274,39 +276,35 @@ void Environment::EnvPromiseHook(v8::PromiseHookType type,
274276
void Environment::RunAndClearNativeImmediates() {
275277
size_t count = native_immediate_callbacks_.size();
276278
if (count > 0) {
279+
size_t ref_count = 0;
277280
std::vector<NativeImmediateCallback> list;
278281
native_immediate_callbacks_.swap(list);
279282
for (const auto& cb : list) {
280283
cb.cb_(this, cb.data_);
281284
if (cb.keep_alive_)
282285
cb.keep_alive_->Reset();
286+
if (cb.refed_)
287+
ref_count++;
283288
}
284289

285290
#ifdef DEBUG
286291
CHECK_GE(immediate_info()->count(), count);
287292
#endif
288293
immediate_info()->count_dec(count);
294+
immediate_info()->ref_count_dec(ref_count);
289295
}
290296
}
291297

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

302299
void Environment::CheckImmediate(uv_check_t* handle) {
303300
Environment* env = Environment::from_immediate_check_handle(handle);
304-
HandleScope scope(env->isolate());
305-
Context::Scope context_scope(env->context());
306301

307-
if (MaybeStopImmediate(env))
302+
if (env->immediate_info()->count() == 0)
308303
return;
309304

305+
HandleScope scope(env->isolate());
306+
Context::Scope context_scope(env->context());
307+
310308
env->RunAndClearNativeImmediates();
311309

312310
do {
@@ -318,13 +316,17 @@ void Environment::CheckImmediate(uv_check_t* handle) {
318316
{0, 0}).ToLocalChecked();
319317
} while (env->immediate_info()->has_outstanding());
320318

321-
MaybeStopImmediate(env);
319+
if (env->immediate_info()->ref_count() == 0)
320+
env->ToggleImmediateRef(false);
322321
}
323322

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

330332

0 commit comments

Comments
 (0)