# HG changeset patch # User Simon Heath # Date 1655841389 14400 # Tue Jun 21 15:56:29 2022 -0400 # Node ID 0790d28d3ab94b527876ac01e7a31ff752d9380b # Parent d17927ed714126479216dd3a0359e9221ac999c5 Refactor time server to use ETS Works pretty okay! diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -41,10 +41,25 @@ * Rust + Bastion crate * Alternative name: `brobot` -# Building +# Building & running Use `rebar3`: +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 diff --git a/apps/goatherd/src/gh_time.erl b/apps/goatherd/src/gh_time.erl --- a/apps/goatherd/src/gh_time.erl +++ b/apps/goatherd/src/gh_time.erl @@ -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 @@ 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).