Refactor time server to use ETS

Works pretty okay!
2 files changed, 97 insertions(+), 80 deletions(-)

M README.md
M apps/goatherd/src/gh_time.erl
M README.md +16 -1
@@ 41,10 41,25 @@ Possible alternatives:
  * Rust + Bastion crate
  * Alternative name: `brobot`
 
-# Building
+# Building & running
 
 Use `rebar3`: <https://rebar3.org/>
 
+Building:
+
+ * `rebar3 compile`
+
+Running tests:
+
+Starting and running repl:
+
+ * `rebar3 shell`
+
+Repl reference:
+
+ * https://www.erlang.org/doc/man/shell.html
+
+
 # Tools
 
  * Use `dialyzer` for linting

          
M apps/goatherd/src/gh_time.erl +81 -79
@@ 1,5 1,4 @@ 
 % Timing functions.
-% This is... hoo boy, gonna get weird.
 % See we need to have a single clock that everything uses, but it has to
 % be able to be simulated as well.  So this clock is going to stop and
 % start, potentially run at a different rate than realtime, etc.  It

          
@@ 8,24 7,15 @@ 
 % away your state and start over", which *could* involve moving
 % backwards.
 %
-% This is kinda pubsub-y, consider the Erlang `pg` module to implement
-% it.
-%
-% This is gonna heck up *everything* downstream of it unless I abstract
-% it away real good.  Guess that's *one* thing ROS does for me, and does
-% it well enough I don't notice.
-%
+% We are going to use the Erlang Term Storage to store a "global
+% mutable" value and have a little node to update it continuously.
+% Then a client can either get the latest time, or have a process
+% running that will keep track of the last time it was asked for
+% time and check for jumps, reset's, etc.
 %
-% We could do this with a gen_server that receives requests for
-% subscriptions, but those aren't really designed to send un-asked-for
-% messages.  So this might just be best as its own little thing.
-%
-% This is kinda similar to https://erlang.org/doc/man/timer.html
-% and might be best implemented using https://erlang.org/doc/man/erlang.html#send_after-4
-% but however that occurs, heck, the API will remain the same.  So
-% let's just start writing code.  This may or may not really end up
-% as a production quality end point, but I don't see a better way
-% to do it.
+% Time is Hard. See
+% https://www.erlang.org/doc/apps/erts/time_correction.html
+% for some of the details around it.
 %
 % The timer runs at a fixed rate.  For now we will call it 100 Hz, so
 % this is the maximum resolution it can manage.  It uses Erlang

          
@@ 40,20 30,9 @@ 
 % milliseconds.  The user may not assume that they know what the rate or
 % resolution of the timer is.  Just that, as far as the robot is
 % concerned, each message is "exact enough".
-%
-% There is also a "reset" message that can be sent to
-% clients to notify them that the clock has changed in some abrupt way,
-% and they need to throw out all their state and start over.
-% Following times have no relation to previous times, even if they
-% look like they do.
-%
-% This actually takes up a noticable amount of CPU time, which makes
-% me a little frumple, but the interface can stay the same I guess.
-%
-% TODO: Consider using ets (erlang term storage) instead, or atomics,
-% it has an update_counter function already
 -module(gh_time).
--export([init/0, start/0, start_link/0, subscribe/1, unsubscribe/1, reset/0]).
+-export([init/0, start/0, start_link/0, raw_time/0, start_timeserver/0,
+        timeserver/1, timeserver_time/1]).
 
 % Start the timer service
 start() ->

          
@@ 64,63 43,86 @@ start_link() ->
     spawn_link(?MODULE, init, []).
 
 init() ->
-    % We use the `pg` module to keep track of what processes
-    % are subscribed to the timer: https://erlang.org/doc/man/pg.html
-    % We don't care if it's already started or not.
-    pg:start_link(),
     register(gh_time, self()),
-    loop(erlang:monotonic_time(milli_seconds)).
+    Now = erlang:monotonic_time(milli_seconds),
+    Table = ets:new(gh_time, [set, named_table]),
+    % Add initial time just in case
+    ets:insert(Table, {now, Now}),
+    loop(Table, Now).
 
-% Subscribe to timer updates
-subscribe(Pid) ->
-    gh_time ! {subscribe, Pid}.
-
+% Call this function to get the raw time.
+% Users should do start_timeserver instead.
+raw_time() ->
+    [{now, Now}] = ets:lookup(gh_time, now),
+    Now.
 
-% Unsubscribe to timer updates
-unsubscribe(Pid) ->
-    gh_time ! {unsubscribe, Pid}.
+% Actually it should start a time tracker thingy for the user.
+% It should run one per user process and keep track of its local
+% time info, so it can report dt and whether the time has jumped,
+% reset, or something else.
+%
+% TODO: Start loop if necessary???
+% Link this to the loop so we restart these if the loop screws up?
+% Ponder more later.
+start_timeserver() ->
+    Now = raw_time(),
+    spawn(?MODULE, timeserver, [Now]).
 
-% Reset the timer, notifying all clients
-reset() ->
-    gh_time ! reset.
+timeserver_time(Timeserver) ->
+    Timeserver ! self(),
+    receive
+        Res -> Res
+    % TODO: Timeout?
+    end.
 
-loop(LastUpdate) ->
-    Clients = pg:get_local_members(gh_time),
-    case handle_messages() of
-        ok -> ok;
-        reset -> lists:foreach(fun(Client) -> Client ! gh_time_reset end, Clients)
-    end,
+% This is the main loop that uses the Erlang Term Storage to
+% store what the last known time is.  ETS is node-local, so
+% time is node-local.
+%
+% Time is monotonic.  FOR NOW we do not do anything
+% Time 0 point is arbitrary???
+% Units??? -- selectable, milliseconds for now.
+% See https://www.erlang.org/doc/apps/erts/time_correction.html
+%
+% To consider later: alternate time sources such as a specific
+% chrony server, GPS time source, etc.
+loop(Table, LastUpdate) ->
     % Milliseconds per tick
     Ms = 10,
     Now = erlang:monotonic_time(milli_seconds),
     %io:format("Time is ~p, last update ~p, dt ~p~n", [Now, LastUpdate, Now - LastUpdate]),
-    case (Now - LastUpdate) >= Ms of
-        true -> lists:foreach(
-                  fun(Client) -> Client ! {gh_time, Now} end,
-                  Clients
-                 ),
-                loop(LastUpdate + Ms);
-        % Would be kinda nice if we could sleep for less than 1 ms, but
-        % oh well.  0 is another option.
-        false -> timer:sleep(1),
-                 loop(LastUpdate)
+    Dt = (Now - LastUpdate),
+    case Dt >= Ms of
+        % Update the value stored in ets
+        true ->
+            ets:insert(Table, {now, Now}),
+            loop(Table, LastUpdate + Ms);
+        % Not time to update, sleep a bit
+        false ->
+            % We try to sleep for Dt-1, min 1 ms
+            %SleepTime = max(1, Dt-1),
+            % Or we can just KISS
+            SleepTime = 1,
+            timer:sleep(SleepTime),
+            loop(Table, LastUpdate)
     end.
 
-% Handles all incoming messages, and returns `reset` if a reset
-% was triggered, and `ok` otherwise
-handle_messages() ->
-    handle_messages(ok).
-
-% Loop through all incoming messages and deal with them.
-handle_messages(NeedsReset) ->
+timeserver(LastUpdate) ->
+    % Hmmm, what should the max error be?
+    % Basically we want to warn the client if
+    % it hasn't called this frequently enough.
+    % Maybe that's the client's problem?
+    % Hmmmm.  This is intended to keep track of
+    % it *for* the client.  Maybe have a hz param.
+    MaxError = 10,
+    Now = raw_time(),
+    Dt = Now - LastUpdate,
+    CurrentState =
+        if Dt > MaxError -> skip;
+           Dt < 0 -> reset;
+           Dt =< MaxError -> ok
+        end,
     receive
-        {subscribe, Pid} ->
-            pg:join(gh_time, Pid),
-            handle_messages(NeedsReset);
-        {unsubscribe, Pid} ->
-            pg:leave(gh_time, Pid),
-            handle_messages(NeedsReset);
-        reset -> handle_messages(reset)
-    after 0 ->
-          NeedsReset
-    end.
+        Pid -> Pid ! {CurrentState, Now, Dt}
+    end,
+    timeserver(Now).