├── .formatter.exs ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── bench ├── bench.sh ├── ex0.ex ├── ex1.ex └── sleep.c ├── config └── config.exs ├── examples ├── apply_after.exs ├── apply_every.exs └── send_every.exs ├── lib └── micro_timer.ex ├── mix.exs ├── mix.lock └── test ├── micro_timer_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | micro_timer-*.tar 24 | 25 | # Ignore Elixir Language Server cache 26 | .elixir_ls/ 27 | 28 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [{ 7 | "type": "mix_task", 8 | "name": "mix (Default task)", 9 | "request": "launch", 10 | "projectDir": "${workspaceRoot}" 11 | }, 12 | { 13 | "type": "mix_task", 14 | "name": "mix test", 15 | "request": "launch", 16 | "task": "test", 17 | "taskArgs": [ 18 | "--trace", 19 | "--color" 20 | ], 21 | "startApps": true, 22 | "projectDir": "${workspaceRoot}", 23 | "requireFiles": [ 24 | "test/**/test_helper.exs", 25 | "test/**/*_test.exs" 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [{ 4 | "label": "mix test", 5 | "type": "shell", 6 | "command": "mix", 7 | "args": [ 8 | "test", 9 | "--color", 10 | "--trace" 11 | ], 12 | "options": { 13 | "cwd": "${workspaceRoot}", 14 | "requireFiles": [ 15 | "test/**/test_helper.exs", 16 | "test/**/*_test.exs" 17 | ] 18 | }, 19 | "problemMatcher": "$mixTestFailure", 20 | "group": { 21 | "kind": "build", 22 | "isDefault": true 23 | } 24 | }] 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Massimo Ronca 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroTimer 2 | 3 | [![Hex pm](https://img.shields.io/hexpm/v/micro_timer.svg?style=flat)](https://hex.pm/packages/micro_timer) [![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](http://hexdocs.pm/micro_timer/) 4 | 5 | A timer module with microsend resolution. 6 | 7 | `MicroTimer` can sleep for as low as it takes to the `BEAM` to send a message 8 | (usually 3-5µ). 9 | It can also send messages and invoke functions after a `timeout` or repeatedly 10 | every `timeout` µs. 11 | 12 | Keep in mind that the system `sleep` primitive literally waits doing nothing, consuming no CPU whatsoever, while `µsleep` is implemented with message passing 13 | and wastes CPU cycles for a maximum of 2ms per call. 14 | 15 | The CPU load shouldn't be a problem anyway, but it's definitely non-zero. 16 | 17 | ## Installation 18 | 19 | The package can be installed by adding `micro_timer` to your list of dependencies in `mix.exs`: 20 | 21 | ```elixir 22 | def deps do 23 | [ 24 | {:micro_timer, "~> 0.1.0"} 25 | ] 26 | end 27 | ``` 28 | 29 | ## API 30 | 31 | **MicroTimer** has a very simple API 32 | 33 | - `usleep(timeout)` and the alias `µsleep(timeout)` 34 | sleep for `timeout` µs. 35 | 36 | - `apply_after(timeout, executable, args \\ [])` 37 | invoke the `executable` after `timeout` µs with the args `args` 38 | `executable` can be the tuple `{Module, :function}` or a function 39 | 40 | - `apply_every(timeout, executable, args \\ [])` 41 | invoke the `executable` every `timeout` µs with the args `args` 42 | 43 | - `send_after(timeout, message, pid \\ self())` 44 | send `message` after `timeout` µs to `pid` 45 | if `pid` is empty, the message is sento to `self()` 46 | 47 | - `send_every(timeout, message, pid \\ self())` 48 | send `message` every `timeout` µs to `pid` 49 | 50 | - `cancel_timer(timer)` 51 | cancel the `timer` created by one of `apply_after`, `apply_every`, `send_after` and `send_every` 52 | 53 | `*_after` and `*_avery` return a timer reference that is just a regular `pid`. 54 | You can check if the timer is still active or not with a simple call to `Process.alive?(pid)` 55 | 56 | ## Basic Usage 57 | 58 | ```elixir 59 | iex(1)> :timer.tc(fn -> MicroTimer.usleep(250) end) 60 | {257, :ok} 61 | 62 | iex(2)> MicroTimer.send_after(666, :msg) 63 | #PID<0.222.0> 64 | # approximately 666µs later 65 | iex(3)> flush 66 | :msg 67 | :ok 68 | ``` 69 | 70 | Check out the [`examples`](examples/) folder in this repository for more realistic examples. 71 | 72 | Full documentation can be found at [https://hexdocs.pm/micro_timer](https://hexdocs.pm/micro_timer). 73 | 74 | ## Benchmarks 75 | 76 | [Google Drive spreadsheet](https://docs.google.com/spreadsheets/u/2/d/e/2PACX-1vTmvR8JOpVriDxXGv3UJpsxEMN4hIa56NYrCgRCc_V3OPxkixaNat6lgzUOr1lr2ftTih972TlsWdM9/pubhtml) 77 | 78 | ## License 79 | 80 | **MicroTimer** is released under the MIT License - see the [LICENSE](LICENSE) file. 81 | 82 | -------------------------------------------------------------------------------- /bench/bench.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | BASEDIR="$(cd "$(dirname "$0")" && pwd)"; 4 | 5 | OUT_FILE="$BASEDIR/bench_results.csv"; 6 | BIN_DIR="$BASEDIR/bin" 7 | TMP_DIR="$BASEDIR/tmp" 8 | 9 | rm -rf "$BIN_DIR"; 10 | mkdir -p "$BIN_DIR"; 11 | 12 | rm -rf "$TMP_DIR"; 13 | mkdir -p "$TMP_DIR"; 14 | 15 | gcc -o "$BIN_DIR/sleep" "$BASEDIR/sleep.c" 16 | elixirc --ignore-module-conflict -o "$BIN_DIR" "$BASEDIR/ex0.ex" 17 | elixirc --ignore-module-conflict -o "$BIN_DIR" "$BASEDIR/ex1.ex" 18 | 19 | for i in {1..5000}; do 20 | od -vAn -N2 -tu2 < /dev/urandom | tr -d ' ' | grep -v "^$" >> "$TMP_DIR/timeouts"; 21 | done; 22 | 23 | "$BIN_DIR/sleep" "$TMP_DIR/timeouts" > "$TMP_DIR/c_bench.csv"; 24 | elixir -pa "$BIN_DIR" -e "MicroTimerBenchNoAdjust.run(\"$TMP_DIR/timeouts\")" > "$TMP_DIR/ex0_bench.csv"; 25 | elixir -pa "$BIN_DIR" -e "MicroTimerBenchAdjust.run(\"$TMP_DIR/timeouts\")" > "$TMP_DIR/ex1_bench.csv"; 26 | 27 | echo "timeout;C timeout;C jitter;C %;Ex0 timeout;Ex0 jitter;Ex0 %;Ex1 timeout;Ex1 jitter;Ex1 %;" > "$TMP_DIR/header.csv"; 28 | paste -d ';' "$TMP_DIR/timeouts" "$TMP_DIR/c_bench.csv" "$TMP_DIR/ex0_bench.csv" "$TMP_DIR/ex1_bench.csv" > "$TMP_DIR/results.csv"; 29 | 30 | cat "$TMP_DIR/header.csv" "$TMP_DIR/results.csv" > "$BASEDIR/bench_results.csv" 31 | -------------------------------------------------------------------------------- /bench/ex0.ex: -------------------------------------------------------------------------------- 1 | defmodule MicroTimerBenchNoAdjust do 2 | def usleep(timeout) when is_integer(timeout) and timeout > 0 do 3 | do_usleep(timeout) 4 | 5 | receive do 6 | :done -> :ok 7 | end 8 | end 9 | 10 | defp do_usleep(timeout) when timeout > 2_000 do 11 | ms_timeout = div(timeout, 1_000) - 1 12 | us_timeout = timeout - ms_timeout * 1_000 13 | 14 | Process.sleep(ms_timeout) 15 | 16 | do_usleep(System.monotonic_time(:microsecond), us_timeout) 17 | end 18 | 19 | defp do_usleep(timeout) do 20 | do_usleep(System.monotonic_time(:microsecond), timeout) 21 | end 22 | 23 | defp do_usleep(start, timeout) do 24 | if System.monotonic_time(:microsecond) - start >= timeout do 25 | send(self(), :done) 26 | else 27 | do_usleep(start, timeout) 28 | end 29 | end 30 | 31 | def run(file) do 32 | IO.write(:stderr, "Ex0 started processing data...\n") 33 | 34 | File.stream!(Path.expand(file)) 35 | |> Stream.map(&String.trim/1) 36 | |> Stream.map(&String.to_integer/1) 37 | |> Stream.with_index() 38 | |> Stream.each(fn {timeout, index} -> 39 | if index > 0 and rem(index, 100) == 0 do 40 | IO.write(:stderr, "\rEx0 processed #{index} lines...") 41 | end 42 | 43 | {time, _} = 44 | :timer.tc(fn -> 45 | usleep(timeout) 46 | end) 47 | 48 | r = :io_lib.format("~f", [(time - timeout) / timeout]) 49 | IO.puts("#{time};#{time - timeout};#{r}") 50 | end) 51 | |> Stream.run() 52 | 53 | IO.write(:stderr, "\nEx0 done processing!\n") 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /bench/ex1.ex: -------------------------------------------------------------------------------- 1 | defmodule MicroTimerBenchAdjust do 2 | def usleep(timeout) when is_integer(timeout) and timeout > 0 do 3 | do_usleep(timeout) 4 | 5 | receive do 6 | :done -> :ok 7 | end 8 | end 9 | 10 | defp do_usleep(timeout) when timeout > 2_000 do 11 | ms_timeout = div(timeout, 1_000) - 1 12 | 13 | {real_sleep_time, _} = 14 | :timer.tc(fn -> 15 | Process.sleep(ms_timeout) 16 | end) 17 | 18 | do_usleep(System.monotonic_time(:microsecond), timeout - real_sleep_time) 19 | end 20 | 21 | defp do_usleep(timeout) do 22 | do_usleep(System.monotonic_time(:microsecond), timeout) 23 | end 24 | 25 | defp do_usleep(start, timeout) do 26 | if System.monotonic_time(:microsecond) - start >= timeout do 27 | send(self(), :done) 28 | else 29 | do_usleep(start, timeout) 30 | end 31 | end 32 | 33 | def run(file) do 34 | IO.write(:stderr, "Ex1 started processing data...\n") 35 | 36 | File.stream!(Path.expand(file)) 37 | |> Stream.map(&String.trim/1) 38 | |> Stream.map(&String.to_integer/1) 39 | |> Stream.with_index() 40 | |> Stream.each(fn {timeout, index} -> 41 | if index > 0 and rem(index, 100) == 0 do 42 | IO.write(:stderr, "\rEx1 processed #{index} lines...") 43 | end 44 | 45 | {time, _} = 46 | :timer.tc(fn -> 47 | usleep(timeout) 48 | end) 49 | 50 | r = :io_lib.format("~f", [(time - timeout) / timeout]) 51 | IO.puts("#{time};#{time - timeout};#{r}") 52 | end) 53 | |> Stream.run() 54 | 55 | IO.write(:stderr, "\nEx1 done processing!\n") 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /bench/sleep.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main(int argc, const char *argv[]) 7 | { 8 | 9 | int timeout; 10 | FILE *file; 11 | file = fopen(argv[1], "r"); 12 | if (file) 13 | { 14 | fprintf(stderr, "C started processing data...\n"); 15 | int line = 0; 16 | while (fscanf(file, "%d\n", &timeout) != EOF) 17 | { 18 | if (++line % 100 == 0) 19 | { 20 | fprintf(stderr, "\rC processed %d lines...", line); 21 | } 22 | 23 | struct timeval st, et; 24 | 25 | gettimeofday(&st, NULL); 26 | select(0, NULL, NULL, NULL, &((struct timeval){0, timeout})); 27 | // nanosleep((const struct timespec[]){{0, timeout * 1000}}, NULL); 28 | gettimeofday(&et, NULL); 29 | 30 | int elapsed = (((et.tv_sec - st.tv_sec) * 1000000) + (et.tv_usec - st.tv_usec)); 31 | 32 | printf("%d;%d;%.4f\n", 33 | elapsed, 34 | elapsed - timeout, 35 | (double)(elapsed - timeout) / (double)timeout); 36 | } 37 | fclose(file); 38 | fprintf(stderr, "\nC done processing %d lines!\n", line); 39 | } 40 | 41 | return 0; 42 | } 43 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :micro_timer, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:micro_timer, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /examples/apply_after.exs: -------------------------------------------------------------------------------- 1 | # run from the project root with 2 | # elixir --no-halt -r lib/* examples/apply_after.exs 3 | 4 | defmodule FpsCounter do 5 | @timeout Float.round(1_000_000 / 60) |> trunc 6 | 7 | def run do 8 | MicroTimer.apply_after(@timeout, &show_fps/1, [System.monotonic_time(:microsecond)]) 9 | end 10 | 11 | def show_fps(start) do 12 | fps = 1_000_000 / (System.monotonic_time(:microsecond) - start) 13 | IO.write("\rFPS: #{fps}") 14 | MicroTimer.apply_after(@timeout, &show_fps/1, [System.monotonic_time(:microsecond)]) 15 | end 16 | end 17 | 18 | FpsCounter.run() 19 | -------------------------------------------------------------------------------- /examples/apply_every.exs: -------------------------------------------------------------------------------- 1 | # run from the project root with 2 | # elixir --no-halt -r lib/* examples/apply_every.exs 3 | 4 | defmodule FpsCounter do 5 | @timeout Float.round(1_000_000 / 60) |> trunc 6 | 7 | def run do 8 | {:ok, pid} = Agent.start_link(fn -> System.monotonic_time(:microsecond) end) 9 | MicroTimer.apply_every(@timeout, &show_fps/1, [pid]) 10 | end 11 | 12 | def show_fps(pid) do 13 | start = Agent.get(pid, & &1) 14 | fps = 1_000_000 / (System.monotonic_time(:microsecond) - start) 15 | IO.write("\rFPS: #{fps}") 16 | Agent.update(pid, fn _ -> System.monotonic_time(:microsecond) end) 17 | end 18 | end 19 | 20 | FpsCounter.run() 21 | -------------------------------------------------------------------------------- /examples/send_every.exs: -------------------------------------------------------------------------------- 1 | # run from the project root with 2 | # elixir --no-halt -r lib/* examples/send_every.exs 3 | 4 | defmodule FpsCounter do 5 | use GenServer 6 | 7 | @timeout Float.round(1_000_000 / 60) |> trunc 8 | 9 | def run do 10 | GenServer.start_link(__MODULE__, []) 11 | end 12 | 13 | def init(_args) do 14 | MicroTimer.send_every(@timeout, :tick) 15 | {:ok, System.monotonic_time(:microsecond)} 16 | end 17 | 18 | def handle_info(:tick, state) do 19 | fps = 1_000_000 / (System.monotonic_time(:microsecond) - state) 20 | IO.write("\rFPS: #{fps}") 21 | {:noreply, System.monotonic_time(:microsecond)} 22 | end 23 | end 24 | 25 | FpsCounter.run() 26 | -------------------------------------------------------------------------------- /lib/micro_timer.ex: -------------------------------------------------------------------------------- 1 | defmodule MicroTimer do 2 | @moduledoc """ 3 | A timer module with microsecond resolution. 4 | """ 5 | 6 | @sleep_done :___usleep_done 7 | @type executable :: {module(), atom()} | function() 8 | 9 | @doc """ 10 | Suspend the current process for the given `timeout` and then returns `:ok`. 11 | 12 | `timeout` is the number of microsends to sleep as an integer. 13 | 14 | ## Examples 15 | 16 | iex> MicroTimer.usleep(250) 17 | :ok 18 | 19 | """ 20 | @spec µsleep(non_neg_integer()) :: :ok 21 | defdelegate µsleep(timeout), to: __MODULE__, as: :usleep 22 | 23 | @spec usleep(non_neg_integer()) :: :ok 24 | def usleep(timeout) when is_integer(timeout) and timeout > 0 do 25 | do_usleep(timeout) 26 | 27 | receive do 28 | @sleep_done -> :ok 29 | end 30 | end 31 | 32 | def usleep(timeout) when is_integer(timeout) do 33 | :ok 34 | end 35 | 36 | @doc """ 37 | Invokes the given `executable` after `timeout` microseconds with the list of 38 | arguments `args`. 39 | 40 | `executable` can either be the tuple `{Module, :function}`, an anonymous function 41 | or a function capture. 42 | 43 | Returns the `pid` of the timer. 44 | 45 | See also `cancel_timer/1`. 46 | 47 | ## Examples 48 | 49 | MicroTimer.apply_after(250, {Module. :function}, []) 50 | 51 | MicroTimer.apply_after(250, fn a -> a + 1 end, [1]) 52 | 53 | iex> pid = MicroTimer.apply_after(250, fn arg -> arg end, [1]) 54 | iex> is_pid(pid) 55 | true 56 | 57 | """ 58 | 59 | @spec apply_after(non_neg_integer(), executable, [any]) :: pid() 60 | def apply_after(timeout, executable, args \\ []) 61 | 62 | def apply_after(timeout, {module, function}, args) 63 | when is_atom(module) and is_atom(function) do 64 | spawn(fn -> 65 | do_apply_after(timeout, {module, function}, args) 66 | end) 67 | end 68 | 69 | def apply_after(time, function, args) when is_function(function) do 70 | spawn(fn -> 71 | do_apply_after(time, function, args) 72 | end) 73 | end 74 | 75 | @doc """ 76 | Invokes the given `executable` repeatedly every `timeout` microseconds with the list of 77 | arguments `args`. 78 | 79 | `executable` can either be the tuple `{Module, :function}`, an anonymous function 80 | or a function capture. 81 | 82 | Returns the `pid` of the timer. 83 | 84 | See also `cancel_timer/1`. 85 | 86 | ## Examples 87 | 88 | MicroTimer.apply_every(250, {Module. :function}, []) 89 | 90 | MicroTimer.apply_every(250, fn a -> a + 1 end, [1]) 91 | 92 | iex> pid = MicroTimer.apply_every(250, fn arg -> arg end, [1]) 93 | iex> is_pid(pid) 94 | true 95 | 96 | """ 97 | @spec apply_every(non_neg_integer(), executable, [any]) :: pid() 98 | def apply_every(timeout, executable, args \\ []) 99 | 100 | def apply_every(timeout, {module, function}, args) 101 | when is_atom(module) and is_atom(function) do 102 | spawn(fn -> 103 | do_apply_every(timeout, {module, function}, args) 104 | end) 105 | end 106 | 107 | def apply_every(timeout, function, args) when is_function(function) do 108 | spawn(fn -> 109 | do_apply_every(timeout, function, args) 110 | end) 111 | end 112 | 113 | @doc """ 114 | Send `message` to `pid` after `timeout` microseconds. 115 | 116 | If `pid` is left empty, the `message` is sent to `self()`. 117 | 118 | Returns the `pid` of the timer. 119 | 120 | See also `cancel_timer/1`. 121 | 122 | ## Examples 123 | 124 | MicroTimer.send_after(250, :msg) 125 | 126 | MicroTimer.send_after(250, "msg", self()) 127 | 128 | iex> pid = MicroTimer.send_after(250, :msg) 129 | iex> is_pid(pid) 130 | true 131 | 132 | """ 133 | @spec send_after(non_neg_integer(), any, pid()) :: pid() 134 | def send_after(timeout, message, pid \\ self()) do 135 | spawn(fn -> 136 | do_apply_after(timeout, fn -> send(pid, message) end, []) 137 | end) 138 | end 139 | 140 | @doc """ 141 | Send `message` to `pid` every `timeout` microseconds. 142 | 143 | If `pid` is left empty, the `message` is sent to `self()`. 144 | 145 | Returns the `pid` of the timer. 146 | 147 | See also `cancel_timer/1`. 148 | 149 | ## Examples 150 | 151 | MicroTimer.send_every(250, :msg) 152 | 153 | MicroTimer.send_every(250, "msg", self()) 154 | 155 | iex> pid = MicroTimer.send_every(250, :msg) 156 | iex> is_pid(pid) 157 | true 158 | 159 | """ 160 | @spec send_every(non_neg_integer(), any, pid()) :: pid() 161 | def send_every(timeout, message, pid \\ self()) do 162 | spawn(fn -> 163 | do_apply_every(timeout, fn -> send(pid, message) end, []) 164 | end) 165 | end 166 | 167 | @doc """ 168 | Cancel a timer `pid` created by `apply_after/2`, `apply_after/3`, `apply_every/2` 169 | or `apply_every/3` 170 | 171 | Always returns `true` 172 | 173 | ## Examples 174 | 175 | timer = MicroTimer.apply_every(250, {Module. :function}, []) 176 | MicroTimer.cancel_timer(timer) 177 | 178 | iex> pid = MicroTimer.apply_every(250, fn arg -> arg end, [1]) 179 | iex> MicroTimer.cancel_timer(pid) 180 | iex> Process.alive?(pid) 181 | false 182 | 183 | """ 184 | @spec cancel_timer(pid()) :: true 185 | def cancel_timer(pid) when is_pid(pid) do 186 | Process.exit(pid, :kill) 187 | end 188 | 189 | defp do_usleep(timeout) when timeout > 2_000 do 190 | ms_timeout = div(timeout, 1_000) - 1 191 | 192 | {real_sleep_time, _} = 193 | :timer.tc(fn -> 194 | Process.sleep(ms_timeout) 195 | end) 196 | 197 | do_usleep(System.monotonic_time(:microsecond), timeout - real_sleep_time) 198 | end 199 | 200 | defp do_usleep(timeout) do 201 | do_usleep(System.monotonic_time(:microsecond), timeout) 202 | end 203 | 204 | defp do_usleep(start, timeout) do 205 | if System.monotonic_time(:microsecond) - start >= timeout do 206 | send(self(), @sleep_done) 207 | else 208 | do_usleep(start, timeout) 209 | end 210 | end 211 | 212 | defp do_apply_after(timeout, {module, function}, args) do 213 | usleep(timeout) 214 | apply(module, function, args) 215 | end 216 | 217 | defp do_apply_after(timeout, function, args) do 218 | usleep(timeout) 219 | apply(function, args) 220 | end 221 | 222 | defp do_apply_every(timeout, executable, args) do 223 | do_apply_after(timeout, executable, args) 224 | do_apply_every(timeout, executable, args) 225 | end 226 | end 227 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MicroTimer.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :micro_timer, 7 | version: "0.1.1", 8 | elixir: "~> 1.7", 9 | description: "A timer module with microsecond resolution", 10 | start_permanent: Mix.env() == :prod, 11 | package: package(), 12 | deps: deps(), 13 | docs: docs() 14 | ] 15 | end 16 | 17 | # Run "mix help compile.app" to learn about applications. 18 | def application do 19 | [ 20 | extra_applications: [:logger] 21 | ] 22 | end 23 | 24 | # Run "mix help deps" to learn about dependencies. 25 | defp deps do 26 | [ 27 | {:ex_doc, "~> 0.19", only: :dev, runtime: false}, 28 | {:stream_data, "~> 0.4.2", only: :test, runtime: false} 29 | ] 30 | end 31 | 32 | defp package do 33 | [ 34 | files: ~w(lib mix.exs README.md LICENSE), 35 | maintainers: ["Massimo Ronca"], 36 | licenses: ["MIT"], 37 | links: %{ 38 | "GitLab" => "https://gitlab.com/wstucco/micro_timer", 39 | "GitHub" => "https://github.com/wstucco/micro_timer" 40 | } 41 | ] 42 | end 43 | 44 | defp docs() do 45 | [ 46 | main: "readme", 47 | name: "MicroTimer", 48 | canonical: "http://hexdocs.pm/micro_timer", 49 | source_url: "https://gotlab.com/wstucco/micro_timer", 50 | extras: [ 51 | "README.md" 52 | ] 53 | ] 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.3.0", "17f0c38eaafb4800f746b457313af4b2442a8c2405b49c645768680f900be603", [:mix], [], "hexpm"}, 3 | "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, 7 | "stream_data": {:hex, :stream_data, "0.4.2", "fa86b78c88ec4eaa482c0891350fcc23f19a79059a687760ddcf8680aac2799b", [:mix], [], "hexpm"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/micro_timer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MicroTimerTest do 2 | use ExUnit.Case 3 | use ExUnitProperties 4 | use TestMacros 5 | doctest MicroTimer 6 | 7 | test "usleep/1" do 8 | assert MicroTimer.usleep(1) == :ok 9 | end 10 | 11 | test "usleep/1 accpets only integers" do 12 | assert_raise FunctionClauseError, fn -> 13 | MicroTimer.usleep(1.1) == :ok 14 | end 15 | end 16 | 17 | test "usleep/1 sleeps for at leat `timeout` µs" do 18 | check all timeout <- positive_integer(), timeout < 1_000 do 19 | {elapsed_time, :ok} = 20 | :timer.tc(fn -> 21 | MicroTimer.usleep(timeout) 22 | end) 23 | 24 | assert elapsed_time >= timeout 25 | end 26 | end 27 | 28 | test "apply_after/2" do 29 | assert MicroTimer.apply_after(1, fn -> nil end) |> is_pid() 30 | end 31 | 32 | test "apply_after/3" do 33 | assert MicroTimer.apply_after(1, fn -> nil end, []) |> is_pid() 34 | end 35 | 36 | test "apply_after/2 invoke a function after `timeout` microseconds" do 37 | parent = self() 38 | pid = MicroTimer.apply_after(250, fn -> send(parent, :msg) end) 39 | assert_received_after(250, :msg) 40 | refute Process.alive?(pid) 41 | end 42 | 43 | test "apply_after/3 invoke a function with args after `timeout` microseconds" do 44 | pid = MicroTimer.apply_after(250, fn msg, parent -> send(parent, msg) end, [:msg, self()]) 45 | assert_received_after(250, :msg) 46 | refute Process.alive?(pid) 47 | end 48 | 49 | test "apply_after/3 invoke Module.function with args after `timeout` microseconds" do 50 | defmodule B do 51 | def f(msg, parent), do: send(parent, msg) 52 | end 53 | 54 | pid = MicroTimer.apply_after(250, {B, :f}, [:msg, self()]) 55 | assert_received_after(250, :msg) 56 | refute Process.alive?(pid) 57 | end 58 | 59 | test "apply_every/2" do 60 | assert MicroTimer.apply_every(1, fn -> nil end) |> is_pid() 61 | end 62 | 63 | test "apply_every/3" do 64 | assert MicroTimer.apply_every(1, fn -> nil end, []) |> is_pid() 65 | end 66 | 67 | test "apply_every/2 invoke a function every `timeout` microseconds" do 68 | parent = self() 69 | pid = MicroTimer.apply_every(250, fn -> send(parent, :msg) end) 70 | 71 | assert_received_every(250, :msg) 72 | MicroTimer.cancel_timer(pid) 73 | refute Process.alive?(pid) 74 | end 75 | 76 | test "apply_every/3 invoke a function with args after `timeout` microseconds" do 77 | pid = MicroTimer.apply_every(250, fn msg, parent -> send(parent, msg) end, [:msg, self()]) 78 | 79 | assert_received_every(250, :msg) 80 | 81 | MicroTimer.cancel_timer(pid) 82 | refute Process.alive?(pid) 83 | end 84 | 85 | test "apply_every/3 invoke Module.function with args after `timeout` microseconds" do 86 | defmodule C do 87 | def f(msg, parent), do: send(parent, msg) 88 | end 89 | 90 | pid = MicroTimer.apply_every(250, {C, :f}, [:msg, self()]) 91 | assert_received_every(250, :msg) 92 | 93 | MicroTimer.cancel_timer(pid) 94 | refute Process.alive?(pid) 95 | end 96 | 97 | test "send_after/2" do 98 | assert MicroTimer.send_after(1, :msg) |> is_pid() 99 | end 100 | 101 | test "send_after/3" do 102 | assert MicroTimer.send_after(1, :msg, self()) |> is_pid() 103 | end 104 | 105 | test "send_after/3 sends a message to `pid` after `timeout` microseconds" do 106 | MicroTimer.send_after(250, :msg, self()) 107 | assert_received_after(250, :msg) 108 | end 109 | 110 | test "send_after/3 returns a `pid` that can be cancelled" do 111 | pid = MicroTimer.send_after(2_000, :msg, self()) 112 | MicroTimer.cancel_timer(pid) 113 | refute_receive :msg, 2 114 | refute Process.alive?(pid) 115 | end 116 | 117 | test "send_every/2" do 118 | assert MicroTimer.send_every(1, :msg) |> is_pid() 119 | end 120 | 121 | test "send_every/3" do 122 | assert MicroTimer.send_every(1, :msg, self()) |> is_pid() 123 | end 124 | 125 | test "send_every/3 sends a message to `pid` every `timeout` microseconds" do 126 | pid = MicroTimer.send_every(2_000, :msg, self()) 127 | 128 | assert_received_every(250, :msg) 129 | 130 | MicroTimer.cancel_timer(pid) 131 | refute Process.alive?(pid) 132 | end 133 | 134 | test "send_every/3 returns a `pid` that can be cancelled" do 135 | pid = MicroTimer.send_every(2_000, :msg, self()) 136 | MicroTimer.cancel_timer(pid) 137 | refute_receive :msg, 2 138 | refute Process.alive?(pid) 139 | end 140 | 141 | test "cancel_timer/1" do 142 | pid = MicroTimer.apply_every(250, fn -> nil end) 143 | assert MicroTimer.cancel_timer(pid) == true 144 | end 145 | 146 | test "cancel_timer/1 kills the timer identified by `pid`" do 147 | pid = MicroTimer.apply_every(250, fn parent -> send(parent, :msg) end, [self()]) 148 | assert MicroTimer.cancel_timer(pid) == true 149 | refute Process.alive?(pid) 150 | refute_receive :msg, 1 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Application.ensure_started(:stream_data) 2 | 3 | defmodule TestMacros do 4 | defmacro __using__(_opts) do 5 | quote do 6 | import TestMacros 7 | end 8 | end 9 | 10 | defmacro assert_received_after(timeout, msg) do 11 | quote do 12 | start = System.monotonic_time(:microsecond) 13 | 14 | receive do 15 | message -> 16 | assert message == unquote(msg) 17 | assert System.monotonic_time(:microsecond) - start >= unquote(timeout) 18 | after 19 | 1_000 -> 20 | throw(:timeout) 21 | end 22 | end 23 | end 24 | 25 | defmacro assert_received_every(timeout, msg, runs \\ 10) do 26 | quote do 27 | start = System.monotonic_time(:microsecond) 28 | limit = unquote(runs) * 10 29 | 30 | n = 31 | Enum.reduce_while(1..100, 0, fn 32 | _, unquote(runs) -> 33 | {:halt, unquote(runs)} 34 | 35 | _, acc -> 36 | receive do 37 | unquote(msg) -> 38 | {:cont, acc + 1} 39 | after 40 | 1_000 -> {:halt, :err} 41 | end 42 | end) 43 | 44 | assert n == unquote(runs) 45 | assert System.monotonic_time(:microsecond) - start >= unquote(timeout) * unquote(runs) 46 | end 47 | end 48 | end 49 | 50 | ExUnit.start() 51 | --------------------------------------------------------------------------------