execution_timing.ex (4431B)
1 defmodule Credo.Execution.ExecutionTiming do 2 @moduledoc """ 3 The `ExecutionTiming` module can help in timing the execution of code parts and 4 storing those timing inside the `Credo.Execution` struct. 5 """ 6 7 use GenServer 8 9 alias Credo.Execution 10 11 @doc """ 12 Runs the given `fun` and prints the time it took with the given `label`. 13 14 iex> Credo.Execution.ExecutionTiming.inspect("foo", fn -> some_complicated_stuff() end) 15 foo: 51284 16 17 """ 18 def inspect(label, fun) do 19 {duration, result} = :timer.tc(fun) 20 21 # credo:disable-for-lines:3 Credo.Check.Warning.IoInspect 22 duration 23 |> format_time() 24 |> IO.inspect(label: label) 25 26 result 27 end 28 29 @doc """ 30 Returns the current timestamp in the same format (microseconds) as the returned starting times of `run/1`. 31 """ 32 def now, do: :os.system_time(:microsecond) 33 34 @doc """ 35 Runs the given `fun` and returns a tuple of `{started_at, duration, result}`. 36 37 iex> Credo.Execution.ExecutionTiming.run(fn -> some_complicated_stuff() end) 38 {1540540119448181, 51284, [:whatever, :fun, :returned]} 39 40 """ 41 def run(fun) do 42 started_at = now() 43 {duration, result} = :timer.tc(fun) 44 45 {started_at, duration, result} 46 end 47 48 @doc "Same as `run/1` but takes `fun` and `args` separately." 49 def run(fun, args) do 50 started_at = now() 51 {duration, result} = :timer.tc(fun, args) 52 53 {started_at, duration, result} 54 end 55 56 @doc """ 57 Adds a timing to the given `exec` using the given values of `tags`, `started_at` and `duration`. 58 """ 59 def append(%Execution{timing_pid: pid}, tags, started_at, duration) do 60 spawn(fn -> 61 GenServer.call(pid, {:append, tags, started_at, duration}) 62 end) 63 end 64 65 @doc """ 66 Adds a timing piped from `run/2` to the given `exec` (using the given values of `tags`, `started_at` and `duration`). 67 """ 68 def append({started_at, duration, _result}, %Execution{timing_pid: pid}, tags) do 69 spawn(fn -> 70 GenServer.call(pid, {:append, tags, started_at, duration}) 71 end) 72 end 73 74 @doc """ 75 Returns all timings for the given `exec`. 76 """ 77 def all(%Execution{timing_pid: pid}) do 78 GenServer.call(pid, :all) 79 end 80 81 @doc """ 82 Groups all timings for the given `exec` and `tag_name`. 83 """ 84 def grouped_by_tag(exec, tag_name) do 85 map = 86 exec 87 |> all() 88 |> Enum.filter(fn {tags, _started_at, _time} -> tags[tag_name] end) 89 |> Enum.group_by(fn {tags, _started_at, _time} -> tags[tag_name] end) 90 91 map 92 |> Map.keys() 93 |> Enum.map(fn map_key -> 94 sum = Enum.reduce(map[map_key], 0, fn {_tags, _, time}, acc -> time + acc end) 95 96 {[{tag_name, map_key}, {:accumulated, true}], nil, sum} 97 end) 98 end 99 100 @doc """ 101 Returns all timings for the given `exec` and `tag_name`. 102 """ 103 def by_tag(exec, tag_name) do 104 exec 105 |> all() 106 |> Enum.filter(fn {tags, _started_at, _time} -> tags[tag_name] end) 107 end 108 109 @doc """ 110 Returns all timings for the given `exec` and `tag_name` where the tag's value also matches the given `regex`. 111 """ 112 def by_tag(exec, tag_name, regex) do 113 exec 114 |> all() 115 |> Enum.filter(fn {tags, _started_at, _time} -> 116 tags[tag_name] && to_string(tags[tag_name]) =~ regex 117 end) 118 end 119 120 @doc """ 121 Returns the earliest timestamp for the given `exec`. 122 """ 123 def started_at(exec) do 124 {_, started_at, _} = 125 exec 126 |> all() 127 |> List.first() 128 129 started_at 130 end 131 132 @doc """ 133 Returns the latest timestamp plus its duration for the given `exec`. 134 """ 135 def ended_at(exec) do 136 {_, started_at, duration} = 137 exec 138 |> all() 139 |> List.last() 140 141 started_at + duration 142 end 143 144 defp format_time(time) do 145 cond do 146 time > 1_000_000 -> 147 "#{div(time, 1_000_000)}s" 148 149 time > 1_000 -> 150 "#{div(time, 1_000)}ms" 151 152 true -> 153 "#{time}μs" 154 end 155 end 156 157 # callbacks 158 159 @doc false 160 def start_server(exec) do 161 {:ok, pid} = GenServer.start_link(__MODULE__, []) 162 163 %Execution{exec | timing_pid: pid} 164 end 165 166 @doc false 167 def init(_) do 168 {:ok, []} 169 end 170 171 @doc false 172 def handle_call({:append, tags, started_at, time}, _from, current_state) do 173 new_current_state = [{tags, started_at, time} | current_state] 174 175 {:reply, :ok, new_current_state} 176 end 177 178 @doc false 179 def handle_call(:all, _from, current_state) do 180 list = Enum.sort_by(current_state, fn {_, started_at, _} -> started_at end) 181 182 {:reply, list, current_state} 183 end 184 end