Skip to content

Commit 83ff89b

Browse files
committed
timers: allow Immediates to be unrefed
Slightly refactor the 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.
1 parent c84582c commit 83ff89b

File tree

10 files changed

+210
-92
lines changed

10 files changed

+210
-92
lines changed

doc/api/timers.md

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

Lines changed: 69 additions & 49 deletions
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,7 +659,7 @@ 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
@@ -668,30 +671,24 @@ function processImmediate() {
668671
continue;
669672
}
670673

671-
// Save next in case `clearImmediate(immediate)` is called from callback
672-
const next = immediate._idleNext;
674+
immediate._destroyed = true;
673675

674676
const asyncId = immediate[async_id_symbol];
675677
emitBefore(asyncId, immediate[trigger_async_id_symbol]);
676678

677-
tryOnImmediate(immediate, next, tail);
679+
tryOnImmediate(immediate, tail);
678680

679681
emitAfter(asyncId);
680682

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;
683+
immediate = immediate._idleNext;
687684
}
688685

689686
immediateInfo[kHasOutstanding] = 0;
690687
}
691688

692689
// An optimization so that the try/finally only de-optimizes (since at least v8
693690
// 4.7) what is in this smaller function.
694-
function tryOnImmediate(immediate, next, oldTail) {
691+
function tryOnImmediate(immediate, oldTail) {
695692
var threw = true;
696693
try {
697694
// make the actual call outside the try/finally to allow it to be optimized
@@ -700,19 +697,21 @@ function tryOnImmediate(immediate, next, oldTail) {
700697
} finally {
701698
immediate._onImmediate = null;
702699

703-
if (!immediate._destroyed) {
704-
immediate._destroyed = true;
705-
immediateInfo[kCount]--;
700+
immediateInfo[kCount]--;
706701

707-
if (async_hook_fields[kDestroy] > 0) {
708-
emitDestroy(immediate[async_id_symbol]);
709-
}
702+
if (immediate[kRefed]) {
703+
immediate[kRefed] = false;
704+
immediateInfo[kRefCount]--;
710705
}
711706

712-
if (threw && (immediate._idleNext !== null || next !== null)) {
707+
if (async_hook_fields[kDestroy] > 0) {
708+
emitDestroy(immediate[async_id_symbol]);
709+
}
710+
711+
if (threw && immediate._idleNext !== null) {
713712
// Handle any remaining Immediates after error handling has resolved,
714713
// assuming we're still alive to do so.
715-
outstandingQueue.head = immediate._idleNext || next;
714+
outstandingQueue.head = immediate._idleNext;
716715
outstandingQueue.tail = oldTail;
717716
immediateInfo[kHasOutstanding] = 1;
718717
}
@@ -729,31 +728,51 @@ function runCallback(timer) {
729728
}
730729

731730

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;
731+
const Immediate = class Immediate {
732+
constructor(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;
741+
this[kRefed] = false;
742+
743+
this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter];
744+
this[trigger_async_id_symbol] = getDefaultTriggerAsyncId();
745+
if (async_hook_fields[kInit] > 0) {
746+
emitInit(this[async_id_symbol],
747+
'Immediate',
748+
this[trigger_async_id_symbol],
749+
this);
750+
}
751+
752+
this.ref();
753+
immediateInfo[kCount]++;
741754

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);
755+
immediateQueue.append(this);
749756
}
750757

751-
if (immediateInfo[kCount] === 0)
752-
activateImmediateCheck();
753-
immediateInfo[kCount]++;
758+
ref() {
759+
if (!this[kRefed]) {
760+
this[kRefed] = true;
761+
if (immediateInfo[kRefCount]++ === 0)
762+
toggleImmediateRef(true);
763+
}
764+
return this;
765+
}
754766

755-
immediateQueue.append(this);
756-
}
767+
unref() {
768+
if (this[kRefed]) {
769+
this[kRefed] = false;
770+
if (--immediateInfo[kRefCount] === 0)
771+
toggleImmediateRef(false);
772+
}
773+
return this;
774+
}
775+
};
757776

758777
function setImmediate(callback, arg1, arg2, arg3) {
759778
if (typeof callback !== 'function') {
@@ -793,15 +812,16 @@ exports.setImmediate = setImmediate;
793812

794813

795814
exports.clearImmediate = function(immediate) {
796-
if (!immediate) return;
815+
if (!immediate || immediate._destroyed)
816+
return;
797817

798-
if (!immediate._destroyed) {
799-
immediateInfo[kCount]--;
800-
immediate._destroyed = true;
818+
immediateInfo[kCount]--;
819+
immediate._destroyed = true;
801820

802-
if (async_hook_fields[kDestroy] > 0) {
803-
emitDestroy(immediate[async_id_symbol]);
804-
}
821+
immediate.unref();
822+
823+
if (async_hook_fields[kDestroy] > 0) {
824+
emitDestroy(immediate[async_id_symbol]);
805825
}
806826

807827
immediate._onImmediate = null;

src/env-inl.h

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

@@ -538,16 +550,27 @@ inline void Environment::set_fs_stats_field_array(double* fields) {
538550

539551
void Environment::SetImmediate(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);
563+
if (ref) {
564+
if (immediate_info()->ref_count() == 0)
565+
ToggleImmediateRef(true);
566+
immediate_info()->ref_count_inc(1);
567+
}
568+
}
569+
570+
void Environment::SetUnrefImmediate(native_immediate_callback cb,
571+
void* data,
572+
v8::Local<v8::Object> obj) {
573+
SetImmediate(cb, data, obj, false);
551574
}
552575

553576
inline performance::performance_state* Environment::performance_state() {

src/env.cc

Lines changed: 19 additions & 17 deletions
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)