the cost of erlang:send_after/3¶
When you want to do something “after N seconds” in Erlang, the best way to do it is said to be erlang:send_after/3
. But how many memory does it take? How many computation cost does it take?
# Entry point
erlang:send_after/3
can be found as bif at [bif.tab](https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/beam/bif.tab#L203) . The entry point is at [erl_bif_timer.c](https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/beam/erl_bif_timer.c#L493) . This function just calls setup_bif_timer()
, which is [in the same file](https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/beam/erl_bif_timer.c#L390). This allocates ErtsBifTimer
btm = (ErtsBifTimer *) erts_alloc(ERTS_ALC_T_LL_BIF_TIMER,
sizeof(ErtsBifTimer));
which is defined in [the same file](https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/beam/erl_bif_timer.c#L41) . This does not include such a big struct other than ErlTimer tm;
( [defined in erl_time.h](https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/beam/erl_time.h#L33) ). Given the struct size is 256B, then registering 10^6 timers costs about 256MB (re-calculate it, please!) . Needs calculation, but and, it is put into timer wheel.:
tab_insert(btm);
ASSERT(btm == tab_find(ref));
btm->tm.active = 0; /* MUST be initalized */
erts_set_timer(&btm->tm,
(ErlTimeoutProc) bif_timer_timeout,
(ErlCancelProc) bif_timer_cleanup,
(void *) btm,
timeout);
bif_timer_timeout
is a callback of timeout, and btm->tm
is ErlTimer
struct.
# Before/After setting timer
[erts_set_timer in time.c](https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/beam/time.c#L397) is very interesting. Just before setting timer, it moves clock forward by calling erts_deliver_time()
.
void
erts_set_timer(ErlTimer* p, ErlTimeoutProc timeout, ErlCancelProc cancel,
void* arg, Uint t)
{
erts_deliver_time();
erts_smp_mtx_lock(&tiw_lock);
if (p->active) { /* XXX assert ? */
erts_smp_mtx_unlock(&tiw_lock);
return;
}
p->timeout = timeout;
p->cancel = cancel;
p->arg = arg;
p->active = 1;
insert_timer(p, t);
erts_smp_mtx_unlock(&tiw_lock);
#if defined(ERTS_SMP)
if (t <= (Uint) ERTS_SHORT_TIME_T_MAX)
erts_sys_schedule_interrupt_timed(1, (erts_short_time_t) t);
#endif
}
In insert_timer(p, t);
actually timer is registered, but right after it the erts scheduler is interrupted by [writing “!”](https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/sys/common/erl_poll.c#L472) write(2)
with into scheduler interruption pipe.
# Setting timer actually
insert_timer
is in [time.c](https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/beam/time.c#L347) . tiw
might be read as “timer wheel” ? maybe. It’s actually timer wheel, which has 65536 slots (actually TIW_SLOTS
) . Each slot is for single millisecond and keeps all timers that should be invoked in the single milliseconds. Each slot is just a linked list via pointer and generic prev/next pointer linking.
/* calculate slot */
tm = (ticks + tiw_pos) % TIW_SIZE;
p->slot = (Uint) tm;
p->count = (Uint) (ticks / TIW_SIZE);
/* insert at head of list at slot */
p->next = tiw[tm];
p->prev = NULL;
if (p->next != NULL)
p->next->prev = p;
tiw[tm] = p;
# Invoking the timer
erts_bump_timer
is the function where these timers are triggered. This function is called by several scheduling logic incling schedule()
like [here](https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/beam/erl_process.c#L3012):
dt = erts_do_time_read_and_reset();
if (dt) erts_bump_timer(dt);
erts_bump_timer_internal
is the core part of popping out timed out timers from the timer wheel slot [timer.c](https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/beam/time.c#L227). Popping out all timer entries to timeout_head
and calls all callbacks.:
/* Call timedout timers callbacks */
while (timeout_head) {
p = timeout_head;
timeout_head = p->next;
/* Here comes hairy use of the timer fields!
* They are reset without having the lock.
* It is assumed that no code but this will
* accesses any field until the ->timeout
* callback is called.
*/
p->next = NULL;
p->prev = NULL;
p->slot = 0;
(*p->timeout)(p->arg);
}
For erlang:send_after/3
the callback is [bif_timer_timeout](https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/beam/erl_bif_timer.c#L302) . This function calls erts_queue_message
, which finally stacks the message into target process’s msg_q.
# Conclusion
The memory consumption is in order of ~100Bytes per timer. The computation cost is in order of traversing the linked list, whose length is in the same order with Timers stored in each milliseconds.
Suppose 10K timers continuously stored on memory then the length of each queue is 10K / 65536 by average. This is because each timer slot can be accessed by offset.
## Note
In *
nix [gettimeofday(2) is used for clock.] (https://github.com/erlang/otp/blob/OTP-17.0/erts/emulator/sys/unix/erl_unix_sys.h#L162) This clock cannot be trusted so much because it leaps back but Erlang remembers the former answer of it and if the time diff is negative, the wrong time is ignored and nothing changes.