Current section

Files

Jump to
excessibility lib telemetry_capture analyzers event_pattern.ex
Raw

lib/telemetry_capture/analyzers/event_pattern.ex

defmodule Excessibility.TelemetryCapture.Analyzers.EventPattern do
@moduledoc """
Analyzes event patterns and sequences across timeline.
Detects:
- Duplicate consecutive events (unnecessary re-renders)
- Excessive event counts (>10 of same event)
- Common event sequences (repeated patterns)
- Optimization opportunities (debouncing, throttling)
## Algorithm
1. Track event frequencies
2. Detect consecutive duplicates (3+ same events in a row)
3. Detect excessive events (>10 total of same type)
4. Identify common sequences (2+ event patterns)
5. Suggest optimizations based on patterns
## Output
Returns findings and statistics:
%{
findings: [
%{
severity: :warning,
message: "3 consecutive 'handle_event:filter' events - may indicate unnecessary re-renders",
events: [2, 3, 4],
metadata: %{event_type: "handle_event:filter", count: 3}
}
],
stats: %{
event_counts: %{"mount" => 1, "handle_event:filter" => 5},
most_common_event: "handle_event:filter",
common_sequences: [["mount", "handle_event:filter", "handle_event:sort"]]
}
}
"""
@behaviour Excessibility.TelemetryCapture.Analyzer
def name, do: :event_pattern
def default_enabled?, do: true
def requires_enrichers, do: []
def analyze(%{timeline: []}, _opts) do
%{findings: [], stats: %{}}
end
def analyze(%{timeline: timeline}, _opts) do
findings = detect_patterns(timeline)
stats = calculate_stats(timeline)
%{
findings: findings,
stats: stats
}
end
defp detect_patterns(timeline) do
consecutive_findings = detect_consecutive_duplicates(timeline)
excessive_findings = detect_excessive_events(timeline)
optimization_findings = suggest_optimizations(timeline)
consecutive_findings ++ excessive_findings ++ optimization_findings
end
defp detect_consecutive_duplicates(timeline) do
timeline
|> Enum.chunk_by(& &1.event)
|> Enum.flat_map(fn chunk ->
if length(chunk) >= 3 do
event_type = List.first(chunk).event
sequences = Enum.map(chunk, & &1.sequence)
[
%{
severity: :warning,
message: "#{length(chunk)} consecutive '#{event_type}' events - may indicate unnecessary re-renders",
events: sequences,
metadata: %{event_type: event_type, count: length(chunk)}
}
]
else
[]
end
end)
end
defp detect_excessive_events(timeline) do
event_counts = count_events(timeline)
Enum.flat_map(event_counts, fn {event_type, count} ->
if count > 10 do
sequences = timeline |> Enum.filter(&(&1.event == event_type)) |> Enum.map(& &1.sequence)
[
%{
severity: :info,
message: "Many '#{event_type}' events (#{count} total) - consider if all are necessary",
events: sequences,
metadata: %{event_type: event_type, count: count}
}
]
else
[]
end
end)
end
defp suggest_optimizations(timeline) do
# Detect rapid-fire events (likely candidates for debouncing)
rapid_events = detect_rapid_events(timeline)
Enum.flat_map(rapid_events, fn {event_type, sequences, count} ->
suggestion =
cond do
event_type =~ ~r/(keyup|keydown|input|change)/ -> "consider debouncing"
event_type =~ ~r/(scroll|resize|mousemove)/ -> "consider throttling"
true -> "consider batching or debouncing"
end
[
%{
severity: :info,
message: "Rapid '#{event_type}' events (#{count} in sequence) - #{suggestion}",
events: sequences,
metadata: %{event_type: event_type, count: count, suggestion: suggestion}
}
]
end)
end
defp detect_rapid_events(timeline) do
timeline
|> Enum.chunk_by(& &1.event)
|> Enum.filter(fn chunk -> length(chunk) >= 4 end)
|> Enum.map(fn chunk ->
event_type = List.first(chunk).event
sequences = Enum.map(chunk, & &1.sequence)
{event_type, sequences, length(chunk)}
end)
end
defp calculate_stats([]), do: %{}
defp calculate_stats(timeline) do
event_counts = count_events(timeline)
sequences = extract_sequences(timeline)
most_common =
if map_size(event_counts) > 0 do
event_counts
|> Enum.max_by(fn {_event, count} -> count end)
|> elem(0)
end
common_seqs = find_common_sequences(sequences)
%{
event_counts: event_counts,
most_common_event: most_common,
common_sequences: common_seqs
}
end
defp count_events(timeline) do
timeline
|> Enum.map(& &1.event)
|> Enum.frequencies()
end
defp extract_sequences(timeline) do
timeline
|> Enum.map(& &1.event)
|> Enum.chunk_every(3, 1, :discard)
end
defp find_common_sequences(sequences) do
sequences
|> Enum.frequencies()
|> Enum.filter(fn {_seq, count} -> count >= 2 end)
|> Enum.map(fn {seq, _count} -> seq end)
end
end