Skip to content

Commit 4be9dac

Browse files
committed
Optimize performance with pre-aggregated Summary data
- Add rails_pulse_summaries table for pre-aggregated metrics - Update all controllers to use Summary data for charts/metrics - Keep raw Request/Operation data for detailed views - Add BackfillSummariesJob and SummaryJob for data management - Add rake task for backfilling historical summaries - Improve cross-database compatibility (PostgreSQL, MySQL, SQLite) - Optimize dashboard queries from ~500ms to ~50ms load times - Remove cached_components in favor of inline rendering - Update metric cards to use conditional SQL aggregation - Fix sorting, pagination, and filtering across all pages
1 parent bdf9952 commit 4be9dac

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1463
-163581
lines changed

app/controllers/concerns/chart_table_concern.rb

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ module ChartTableConcern
1616
def setup_chart_and_table_data
1717
ransack_params = params[:q] || {}
1818

19-
# Setup chart data first using original time range (no sorting from table)
2019
unless turbo_frame_request?
21-
setup_chart_formatters
20+
# Setup chart data first using original time range (no sorting from table)
2221
setup_chart_data(ransack_params)
22+
setup_chart_formatters
2323
end
2424

2525
# Setup table data using zoom parameters if present, otherwise use chart parameters
@@ -31,7 +31,10 @@ def setup_chart_data(ransack_params)
3131
chart_ransack_query = chart_model.ransack(chart_ransack_params)
3232
@chart_data = chart_class.new(
3333
ransack_query: chart_ransack_query,
34-
group_by: group_by,
34+
period_type: period_type,
35+
start_time: @start_time,
36+
end_time: @end_time,
37+
start_duration: @start_duration,
3538
**chart_options
3639
).to_rails_chart
3740
end
@@ -57,10 +60,14 @@ def setup_time_and_response_ranges
5760
end
5861

5962
def setup_chart_formatters
60-
@xaxis_formatter = RailsPulse::ChartFormatters.occurred_at_as_time_or_date(@time_diff_hours)
63+
@xaxis_formatter = RailsPulse::ChartFormatters.period_as_time_or_date(@time_diff_hours)
6164
@tooltip_formatter = RailsPulse::ChartFormatters.tooltip_as_time_or_date_with_marker(@time_diff_hours)
6265
end
6366

67+
def period_type
68+
@time_diff_hours <= 25 ? :hour : :day
69+
end
70+
6471
def group_by
6572
@time_diff_hours <= 25 ? :group_by_hour : :group_by_day
6673
end

app/controllers/concerns/response_range_concern.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ def setup_duration_range(type = :route)
55
ransack_params = params[:q] || {}
66
thresholds = RailsPulse.configuration.public_send("#{type}_thresholds")
77

8-
if ransack_params[:duration].present?
9-
selected_range = ransack_params[:duration]
8+
# Check both avg_duration (for Summary) and duration (for Request/Operation)
9+
duration_param = ransack_params[:avg_duration] || ransack_params[:duration]
10+
11+
if duration_param.present?
12+
selected_range = duration_param
1013
start_duration =
11-
case ransack_params[:duration].to_sym
14+
case duration_param.to_sym
1215
when :slow then thresholds[:slow]
1316
when :very_slow then thresholds[:very_slow]
1417
when :critical then thresholds[:critical]

app/controllers/concerns/time_range_concern.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,13 @@ def setup_time_range
1717

1818
ransack_params = params[:q] || {}
1919

20-
if ransack_params[:requests_occurred_at_gteq].present?
21-
# Custom time range from routes index chart zoom which filters requests through an association
22-
start_time = parse_time_param(ransack_params[:requests_occurred_at_gteq])
23-
end_time = parse_time_param(ransack_params[:requests_occurred_at_lt])
24-
elsif ransack_params[:occurred_at_gteq].present?
20+
if ransack_params[:occurred_at_gteq].present?
2521
# Custom time range from chart zoom where there is no association
2622
start_time = parse_time_param(ransack_params[:occurred_at_gteq])
2723
end_time = parse_time_param(ransack_params[:occurred_at_lt])
28-
elsif ransack_params[:occurred_at_range]
24+
elsif ransack_params[:period_start_range]
2925
# Predefined time range from dropdown
30-
selected_time_range = ransack_params[:occurred_at_range]
26+
selected_time_range = ransack_params[:period_start_range]
3127
start_time =
3228
case selected_time_range.to_sym
3329
when :last_day then 1.day.ago

app/controllers/rails_pulse/application_controller.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ def fallback_http_basic_auth
5454
end
5555

5656
def session_pagination_limit
57-
# Keep default small for optimal performance
58-
session[:pagination_limit] || 10
57+
# Use URL param if present, otherwise session, otherwise default
58+
limit = params[:limit].presence || session[:pagination_limit] || 10
59+
# Update session if URL param was used
60+
session[:pagination_limit] = limit.to_i if params[:limit].present?
61+
limit.to_i
5962
end
6063

6164
def store_pagination_limit(limit)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
module RailsPulse
22
class DashboardController < ApplicationController
33
def index
4+
@average_query_times_metric_card = RailsPulse::Routes::Cards::AverageResponseTimes.new(route: nil).to_metric_card
5+
@percentile_response_times_metric_card = RailsPulse::Routes::Cards::PercentileResponseTimes.new(route: nil).to_metric_card
6+
@request_count_totals_metric_card = RailsPulse::Routes::Cards::RequestCountTotals.new(route: nil).to_metric_card
7+
@error_rate_per_route_metric_card = RailsPulse::Routes::Cards::ErrorRatePerRoute.new(route: nil).to_metric_card
8+
9+
# Generate chart data for inline rendering
10+
@average_response_time_chart_data = RailsPulse::Dashboard::Charts::AverageResponseTime.new.to_chart_data
11+
@p95_response_time_chart_data = RailsPulse::Dashboard::Charts::P95ResponseTime.new.to_chart_data
12+
13+
# Generate table data for inline rendering
14+
@slow_routes_table_data = RailsPulse::Dashboard::Tables::SlowRoutes.new.to_table_data
15+
@slow_queries_table_data = RailsPulse::Dashboard::Tables::SlowQueries.new.to_table_data
416
end
517
end
618
end

app/controllers/rails_pulse/queries_controller.rb

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,29 @@ class QueriesController < ApplicationController
55
before_action :set_query, only: :show
66

77
def index
8+
@average_query_times_metric_card = RailsPulse::Queries::Cards::AverageQueryTimes.new(query: @query).to_metric_card
9+
@percentile_query_times_metric_card = RailsPulse::Queries::Cards::PercentileQueryTimes.new(query: @query).to_metric_card
10+
@execution_rate_metric_card = RailsPulse::Queries::Cards::ExecutionRate.new(query: @query).to_metric_card
11+
812
setup_chart_and_table_data
913
end
1014

1115
def show
16+
@average_query_times_metric_card = RailsPulse::Queries::Cards::AverageQueryTimes.new(query: @query).to_metric_card
17+
@percentile_query_times_metric_card = RailsPulse::Queries::Cards::PercentileQueryTimes.new(query: @query).to_metric_card
18+
@execution_rate_metric_card = RailsPulse::Queries::Cards::ExecutionRate.new(query: @query).to_metric_card
19+
1220
setup_chart_and_table_data
1321
end
1422

1523
private
1624

1725
def chart_model
18-
show_action? ? Operation : Query
26+
Summary
1927
end
2028

2129
def table_model
22-
show_action? ? Operation : Query
30+
show_action? ? Operation : Summary
2331
end
2432

2533
def chart_class
@@ -31,75 +39,56 @@ def chart_options
3139
end
3240

3341
def build_chart_ransack_params(ransack_params)
34-
base_params = ransack_params.except(:s)
42+
base_params = ransack_params.except(:s).merge(
43+
avg_duration: @start_duration,
44+
period_start_gteq: Time.at(@start_time),
45+
period_start_lt: Time.at(@end_time)
46+
)
3547

3648
if show_action?
37-
base_params.merge(
38-
query_id_eq: @query.id,
39-
occurred_at_gteq: Time.at(@start_time),
40-
occurred_at_lt: Time.at(@end_time),
41-
duration_gteq: @start_duration
42-
)
49+
base_params.merge(summarizable_id_eq: @query.id)
4350
else
44-
base_params.merge(
45-
operations_occurred_at_gteq: Time.at(@start_time),
46-
operations_occurred_at_lt: Time.at(@end_time),
47-
operations_duration_gteq: @start_duration
48-
)
51+
base_params
4952
end
5053
end
5154

5255
def build_table_ransack_params(ransack_params)
5356
if show_action?
57+
# For Operation model on show page
5458
ransack_params.merge(
55-
query_id_eq: @query.id,
5659
occurred_at_gteq: Time.at(@table_start_time),
5760
occurred_at_lt: Time.at(@table_end_time),
58-
duration_gteq: @start_duration
59-
)
61+
query_id_eq: @query.id
62+
).tap { |params| params[:duration_gteq] = @start_duration if @start_duration }
6063
else
64+
# For Summary model on index page
6165
ransack_params.merge(
62-
operations_occurred_at_gteq: Time.at(@table_start_time),
63-
operations_occurred_at_lt: Time.at(@table_end_time),
64-
operations_duration_gteq: @start_duration
66+
avg_duration: @start_duration,
67+
period_start_gteq: Time.at(@table_start_time),
68+
period_start_lt: Time.at(@table_end_time)
6569
)
6670
end
6771
end
6872

6973
def default_table_sort
70-
"occurred_at desc"
74+
show_action? ? "occurred_at desc" : "period_start desc"
7175
end
7276

7377
def build_table_results
7478
if show_action?
75-
@ransack_query.result.select("id", "occurred_at", "duration")
79+
@ransack_query.result
7680
else
77-
# Optimized query: Use INNER JOIN since we only want queries with operations in time range
78-
# This dramatically reduces the dataset before aggregation
79-
@ransack_query.result(distinct: false)
80-
.joins("INNER JOIN rails_pulse_operations ON rails_pulse_operations.query_id = rails_pulse_queries.id")
81-
.where("rails_pulse_operations.occurred_at >= ? AND rails_pulse_operations.occurred_at < ?",
82-
Time.at(@table_start_time), Time.at(@table_end_time))
83-
.group("rails_pulse_queries.id, rails_pulse_queries.normalized_sql, rails_pulse_queries.created_at, rails_pulse_queries.updated_at")
84-
.select(
85-
"rails_pulse_queries.*",
86-
optimized_aggregations_sql
87-
)
81+
Queries::Tables::Index.new(
82+
ransack_query: @ransack_query,
83+
period_type: period_type,
84+
start_time: @start_time,
85+
params: params
86+
).to_table
8887
end
8988
end
9089

9190
private
9291

93-
def optimized_aggregations_sql
94-
# Efficient aggregations that work with our composite indexes
95-
[
96-
"COALESCE(AVG(rails_pulse_operations.duration), 0) AS average_query_time_ms",
97-
"COUNT(rails_pulse_operations.id) AS execution_count",
98-
"COALESCE(SUM(rails_pulse_operations.duration), 0) AS total_time_consumed",
99-
"MAX(rails_pulse_operations.occurred_at) AS occurred_at"
100-
].join(", ")
101-
end
102-
10392
def show_action?
10493
action_name == "show"
10594
end
@@ -108,14 +97,13 @@ def pagination_method
10897
show_action? ? :set_pagination_limit : :store_pagination_limit
10998
end
11099

111-
def set_query
112-
@query = Query.find(params[:id])
100+
def setup_time_and_response_ranges
101+
@start_time, @end_time, @selected_time_range, @time_diff_hours = setup_time_range
102+
@start_duration, @selected_response_range = setup_duration_range(:query)
113103
end
114104

115-
def setup_metic_cards
116-
@average_query_times_card = Queries::Cards::AverageQueryTimes.new(query: @query).to_metric_card
117-
@percentile_response_times_card = Queries::Cards::PercentileQueryTimes.new(query: @query).to_metric_card
118-
@execution_rate_card = Queries::Cards::ExecutionRate.new(query: @query).to_metric_card
105+
def set_query
106+
@query = Query.find(params[:id])
119107
end
120108
end
121109
end

app/controllers/rails_pulse/requests_controller.rb

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ class RequestsController < ApplicationController
55
before_action :set_request, only: :show
66

77
def index
8+
@average_response_times_metric_card = RailsPulse::Routes::Cards::AverageResponseTimes.new(route: nil).to_metric_card
9+
@percentile_response_times_metric_card = RailsPulse::Routes::Cards::PercentileResponseTimes.new(route: nil).to_metric_card
10+
@request_count_totals_metric_card = RailsPulse::Routes::Cards::RequestCountTotals.new(route: nil).to_metric_card
11+
@error_rate_per_route_metric_card = RailsPulse::Routes::Cards::ErrorRatePerRoute.new(route: nil).to_metric_card
12+
813
setup_chart_and_table_data
914
end
1015

@@ -15,7 +20,7 @@ def show
1520
private
1621

1722
def chart_model
18-
Request
23+
Summary
1924
end
2025

2126
def table_model
@@ -27,14 +32,13 @@ def chart_class
2732
end
2833

2934
def chart_options
30-
{ route: true }
35+
{}
3136
end
3237

3338
def build_chart_ransack_params(ransack_params)
3439
ransack_params.except(:s).merge(
35-
occurred_at_gteq: Time.at(@start_time),
36-
occurred_at_lt: Time.at(@end_time),
37-
duration_gteq: @start_duration
40+
period_start_gteq: Time.at(@start_time),
41+
period_start_lt: Time.at(@end_time)
3842
)
3943
end
4044

@@ -52,13 +56,14 @@ def default_table_sort
5256

5357
def build_table_results
5458
@ransack_query.result
55-
.includes(:route)
59+
.joins(:route)
5660
.select(
5761
"rails_pulse_requests.id",
5862
"rails_pulse_requests.occurred_at",
5963
"rails_pulse_requests.duration",
6064
"rails_pulse_requests.status",
61-
"rails_pulse_requests.route_id"
65+
"rails_pulse_requests.route_id",
66+
"rails_pulse_routes.path"
6267
)
6368
end
6469

0 commit comments

Comments
 (0)