Packages
mob_dev
0.3.10
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_dev/bench/summary.ex
defmodule MobDev.Bench.Summary do
@moduledoc """
Post-run analysis of a bench CSV log.
Reads a `MobDev.Bench.Logger` CSV and produces a summary map with
metrics that tell you whether the bench measurement is trustworthy:
- `total_samples` — how many polls completed
- `successful_samples` — those that produced a battery reading
- `success_rate` — fraction (0.0..1.0)
- `reconnect_count` — number of times we transitioned :unreachable / :alive_*_only → :alive_rpc
- `longest_gap_sec` — longest interval between successful battery reads
- `state_durations` — total time (sec) spent in each reachability state
- `screen_off_duration_sec` — time the screen was off
- `screen_on_duration_sec` — time the screen was on
- `start_battery`, `end_battery`, `drain_pct` — first and last successful reads
- `effective_rate_pct_per_hour` — drain extrapolated to per-hour
"""
alias MobDev.Bench.Logger
@type metrics :: %{
total_samples: non_neg_integer(),
successful_samples: non_neg_integer(),
success_rate: float(),
reconnect_count: non_neg_integer(),
longest_gap_sec: float(),
state_durations: %{atom() => float()},
screen_off_duration_sec: float(),
screen_on_duration_sec: float(),
start_battery: integer() | nil,
end_battery: integer() | nil,
drain_pct: integer() | nil,
effective_rate_pct_per_hour: float() | nil,
taint_warnings: [String.t()]
}
@doc """
Compute summary metrics for a bench CSV.
"""
@spec from_csv(Path.t()) :: metrics()
def from_csv(path) do
rows = Logger.read(path)
from_rows(rows)
end
@doc """
Compute summary metrics from already-parsed rows. Useful for tests.
"""
@spec from_rows([map()]) :: metrics()
def from_rows([]), do: empty_metrics()
def from_rows(rows) when is_list(rows) do
successful = Enum.filter(rows, &is_integer(&1.battery_pct))
%{
total_samples: length(rows),
successful_samples: length(successful),
success_rate: length(successful) / length(rows),
reconnect_count: count_reconnects(rows),
longest_gap_sec: longest_gap_sec(successful),
state_durations: state_durations(rows),
screen_off_duration_sec: screen_duration(rows, :off),
screen_on_duration_sec: screen_duration(rows, :on),
start_battery: start_battery(successful),
end_battery: end_battery(successful),
drain_pct: drain(successful),
effective_rate_pct_per_hour: effective_rate(successful),
taint_warnings: taint_warnings(rows)
}
end
defp empty_metrics do
%{
total_samples: 0,
successful_samples: 0,
success_rate: 0.0,
reconnect_count: 0,
longest_gap_sec: 0.0,
state_durations: %{},
screen_off_duration_sec: 0.0,
screen_on_duration_sec: 0.0,
start_battery: nil,
end_battery: nil,
drain_pct: nil,
effective_rate_pct_per_hour: nil,
taint_warnings: []
}
end
# ── Reconnect counting ───────────────────────────────────────────────────
defp count_reconnects(rows) do
rows
|> Enum.map(& &1.reachability)
|> Enum.chunk_every(2, 1, :discard)
|> Enum.count(fn
[prev, :alive_rpc] when prev != :alive_rpc -> true
_ -> false
end)
end
# ── Gap analysis ─────────────────────────────────────────────────────────
defp longest_gap_sec([]), do: 0.0
defp longest_gap_sec([_]), do: 0.0
defp longest_gap_sec(rows) do
rows
|> Enum.map(& &1.elapsed_sec)
|> Enum.chunk_every(2, 1, :discard)
|> Enum.map(fn [a, b] -> b - a end)
|> Enum.max(fn -> 0.0 end)
end
# ── State duration breakdown ─────────────────────────────────────────────
defp state_durations([]), do: %{}
defp state_durations(rows) do
# For each row, the duration is the gap until the next row (or 0 for the
# last row). We attribute that duration to the row's reachability state.
rows
|> Enum.chunk_every(2, 1, :discard)
|> Enum.reduce(%{}, fn [a, b], acc ->
duration = b.elapsed_sec - a.elapsed_sec
Map.update(acc, a.reachability, duration, &(&1 + duration))
end)
|> Map.new(fn {k, v} -> {k, Float.round(v, 2)} end)
end
defp screen_duration([], _state), do: 0.0
defp screen_duration(rows, target_state) do
rows
|> Enum.chunk_every(2, 1, :discard)
|> Enum.reduce(0.0, fn [a, b], acc ->
if a.screen == target_state do
acc + (b.elapsed_sec - a.elapsed_sec)
else
acc
end
end)
|> Float.round(2)
end
# ── Battery extraction ───────────────────────────────────────────────────
defp start_battery([first | _]), do: first.battery_pct
defp start_battery([]), do: nil
defp end_battery([]), do: nil
defp end_battery(rows), do: List.last(rows).battery_pct
defp drain([]), do: nil
defp drain([_]), do: 0
defp drain(rows) do
s = start_battery(rows)
e = end_battery(rows)
if s && e, do: s - e, else: nil
end
defp effective_rate([]), do: nil
defp effective_rate([_]), do: nil
defp effective_rate(rows) do
case {drain(rows), List.last(rows).elapsed_sec - List.first(rows).elapsed_sec} do
{drain, elapsed} when is_integer(drain) and is_float(elapsed) and elapsed > 0 ->
Float.round(drain * 3600.0 / elapsed, 2)
_ ->
nil
end
end
# ── Taint detection ──────────────────────────────────────────────────────
#
# Surface things that would make a measurement inconclusive: screen came
# back on mid-run, app died, reconnect-storms, etc.
defp taint_warnings(rows) do
[]
|> add_warning(screen_on_during_off_run?(rows), "screen turned ON during off-screen run")
|> add_warning(app_died?(rows), "app process reported as dead at some point")
|> add_warning(unreachable_majority?(rows), "majority of polls were :unreachable")
|> add_warning(many_reconnects?(rows), "many reconnects (>=10) — flapping connection")
end
defp add_warning(list, true, msg), do: list ++ [msg]
defp add_warning(list, false, _), do: list
defp screen_on_during_off_run?(rows) do
states = rows |> Enum.map(& &1.screen) |> MapSet.new()
MapSet.member?(states, :off) and MapSet.member?(states, :on)
end
defp app_died?(rows), do: Enum.any?(rows, &(&1.app_process == :app_dead))
defp unreachable_majority?([]), do: false
defp unreachable_majority?(rows) do
unreachable = Enum.count(rows, &(&1.reachability == :unreachable))
unreachable * 2 > length(rows)
end
defp many_reconnects?(rows), do: count_reconnects(rows) >= 10
# ── Pretty-print ─────────────────────────────────────────────────────────
@doc """
Render a summary as a human-readable multi-line string.
"""
@spec pretty(metrics()) :: String.t()
def pretty(%{} = m) do
[
"Total samples: #{m.total_samples}",
"Successful samples: #{m.successful_samples} (#{percent(m.success_rate)})",
"Reconnects: #{m.reconnect_count}",
"Longest gap: #{m.longest_gap_sec} sec",
"Time by state:",
m.state_durations
|> Enum.sort_by(fn {_k, v} -> -v end)
|> Enum.map(fn {state, dur} -> " #{state}: #{dur} sec" end)
|> Enum.join("\n"),
"Screen off: #{m.screen_off_duration_sec} sec",
"Screen on: #{m.screen_on_duration_sec} sec",
battery_line(m),
taint_lines(m.taint_warnings)
]
|> Enum.reject(&(&1 == ""))
|> Enum.join("\n")
end
defp percent(rate) when is_float(rate), do: "#{Float.round(rate * 100, 1)}%"
defp battery_line(%{start_battery: nil}), do: "Battery: no successful reads"
defp battery_line(m) do
rate =
case m.effective_rate_pct_per_hour do
nil -> ""
r -> " (≈ #{r} %/hr)"
end
"Battery: #{m.start_battery}% → #{m.end_battery}% (drain #{m.drain_pct}%)#{rate}"
end
defp taint_lines([]), do: ""
defp taint_lines(warnings) do
"WARNINGS (run may be inconclusive):\n" <>
Enum.map_join(warnings, "\n", &(" - " <> &1))
end
end