Implementation of the Erlang Timer Module

The timer module provides useful functions related to time. For example:

send_after: send a message to a process after some time
send_interval: send a message to a process repeatedly after some time
cancel: cancels a previously requested timeout
sleep: suspend the current process for some time
tc: measure the execution time for an MFA
now_diff: calculate the difference of two erlang:now() timestamps

The timer module is a gen_server callback module. timer:start_link/0
spawns a gen_server process and registers it as ‘timer_server’.

The timer_server process maintains 2 ETS tables:
– timer_tab
An ordered set table holding timing requests and timer objects.
– timer_interval_tab:
A set table holding the timer objects.

When timer_server is started, it creates the 2 ETS tables and then waits
for requests infinitely.

We only concerns about 2 timing request: apply_after and apply_interval.
send_after, exit_after, and kill_after are implemented over apply_after.

send_after(T, Pid, Msg) => apply_after(T, timer, send, [Pid, Msg])
exit_after(T, Pid, Reason) => apply_after(T, erlang, exit, [Pid, Reason])
kill_after(T, Pid) => exit_after(T, Pid, kill)

Similarly, send_interval is implemented using apply_interval.

When handle_call receives an apply_after request, it stores the request in
timer_tab. When it receives an apply_interval request, it stores the request
in timer_tab and timer_interval_tab. Then handle_call looks up timer_tab for
a nearest timeout value using private function timer_timeout, and returns
that value as Timeout to gen_server module. After Timeout amount of time has
elapsed, handle_info(timeout,_) will be called.

1> timer:send_interval(10000, self(), foo).
{ok,{interval,#Ref}}
2> timer:send_interval(15000, self(), bar).
{ok,{interval,#Ref}}
3> ets:i().
 id              name                  type  size   mem      owner
 ----------------------------------------------------------------------------
 timer_interval_tab timer_interval_tab set   2      358      timer_server
 timer_tab       timer_tab          ordered_set 2      160      timer_server
ok
4> ets:i(timer_interval_tab).               
 {{interval,#Ref},{1302232385670900,#Ref},}
 {{interval,#Ref},{1302232384215437,#Ref},}
EOT  (q)uit (p)Digits (k)ill /Regexp -->q
ok
5> ets:i(timer_tab).                        
 {{1302232394215437,#Ref},
 {repeat,10000000,},
 {  ...
 {{1302232400670900,#Ref},
 {repeat,15000000,},
 {  ...
EOT  (q)uit (p)Digits (k)ill /Regexp -->q
ok
%% 
%% timer_timeout(SysTime)
%%
%% Apply and remove already timed-out timers. A timer is a tuple
%% {Time, BRef, Op, MFA}, where Time is in microseconds.
%% Returns {Timeout, Timers}, where Timeout is in milliseconds.
%%
timer_timeout(SysTime) ->
    case ets:first(?TIMER_TAB) of
    '$end_of_table' -> 
        infinity;
    {Time, _Ref} when Time > SysTime ->
        Timeout = (Time - SysTime) div 1000,
        %% Returned timeout must fit in a small int
        erlang:min(Timeout, ?MAX_TIMEOUT);
    Key ->
        case ets:lookup(?TIMER_TAB, Key) of
        [{Key, timeout, MFA}] ->
            ets:delete(?TIMER_TAB,Key),
            do_apply(MFA),
            timer_timeout(SysTime);
        [{{Time, Ref}, Repeat = {repeat, Interv, To}, MFA}] ->
            ets:delete(?TIMER_TAB,Key),
            NewTime = Time + Interv,
            %% Update the interval entry (last in table)
            ets:insert(?INTERVAL_TAB,{{interval,Ref},{NewTime,Ref},To}),
            do_apply(MFA),
            ets:insert(?TIMER_TAB, {{NewTime, Ref}, Repeat, MFA}),
            timer_timeout(SysTime)
        end
    end.

In fact, timer_timeout/1 is called in 3 situations:
1) handle_call(apply_after, …)
1) handle_call(apply_interval, …)
1) handle_info(timeout, …)

timer_timeout looks up timer_tab for a timeouted request, do what the
request wanted, delete the request if it’s apply_after or update it if
it’s apply_interval, and returns the next timeout point.

The function cancel just deletes a timing request from timer_tab (and
timer_interval_tab) using delete_ref.

delete_ref(BRef = {interval, _}) ->
    case ets:lookup(?INTERVAL_TAB, BRef) of
    [{_, BRef2, _Pid}] ->
        ets:delete(?INTERVAL_TAB, BRef),
        ets:delete(?TIMER_TAB, BRef2);
    _ -> % TimerReference does not exist, do nothing
        ok
    end;
delete_ref(BRef) ->
    ets:delete(?TIMER_TAB, BRef).

When you create a repeatable timer for a process using apply_interval,
timer_server will make a link to the target process. If the process
dies, timer_server will receive an ‘EXIT’ message, and then it calls
private function pid_delete/1 to delete all timer objects related with
that process from timer_tab and timer_interval_tab.

pid_delete(Pid) ->
    IntervalTimerList = ets:select(?INTERVAL_TAB,
                                   [{{'_', '_','$1'},
                                     [{'==','$1',Pid}],
                                     ['$_']}]),
    lists:foreach(fun({IntKey, TimerKey, _ }) ->
              ets:delete(?INTERVAL_TAB, IntKey),
              ets:delete(?TIMER_TAB, TimerKey)
          end, IntervalTimerList).

The implementation of sleep/1 is very simple. It’s exactly the same as
that in Joe Armstrong’s book Programming Erlang.

sleep(T) ->
    receive
    after T -> ok
    end.

timer_server is a child of supervisor kernel_safe_sup. When timer_server
exits abnormaly it will be restarted. timer_server doesn’t exit even if
there exists no timing requests any more.

For more details about timer_server, please refer to the source code.

We can see that timer_server doesn’t use erlang:start_timer to create
a timer object. Instead, it uses gen_server’s timing ability to achieve
its goals.

How does gen_server generate timeout messages? The answer is after a
message is handled and the handlers (handle_call, handle_cast, etc.)
returns a Timeout value (the last element of the tuple returned from
handle_* in normal case), gen_server uses “receive … after …” clause
to wait for the next message. If the next message doesn’t come within
Timeout milliseconds, a timeout event is generated internally. It’s not
real message but it goes through the same routine as a real message does,
finally GenMod:handle_info/2 is called with the argument of ‘timeout’.

The manual of gen_server says:

Module:init(Args) -> Result
Module:handle_call(Request, From, State) -> Result
Module:handle_cast(Request, State) -> Result
Module:handle_info(Info, State) -> Result

Types Result = {ok,State} | {ok,State,Timeout} | {ok,State,hibernate}
| {stop,Reason} | ignore
Timeout = int()>=0 | infinity

If an integer timeout value is provided, a timeout will occur unless
a request or a message is received within Timeout milliseconds. A timeout
is represented by the atom timeout which should be handled by the
handle_info/2 callback function. The atom infinity can be used to wait
indefinitely, this is the default value.

And the source of gen_server:

%%% ---------------------------------------------------
%%% The MAIN loop of gen_server
%%% ---------------------------------------------------
loop(Parent, Name, State, Mod, hibernate, Debug) ->
    proc_lib:hibernate(?MODULE,wake_hib,[Parent, Name, State, Mod, Debug]);
loop(Parent, Name, State, Mod, Time, Debug) ->
    Msg = receive
              Input ->
                  Input
          after Time ->
              timeout
          end,
    decode_msg(Msg, Parent, Name, State, Mod, Time, Debug, false).

The gen_server module will be studied in another article.

Advertisements
This entry was posted in erlang and tagged . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s