Skip to content

Commit d50ca7a

Browse files
committed
Enhance performance metrics and graph visualization
- Implement adaptive default window for performance data display based on recent events. - Update performance metrics calculations to utilize dynamic timeframe settings. - Introduce secondary series for average response time in graph visualizations. - Modify action detail view to accommodate new graph data and improve UI layout. - Refactor JavaScript controller to support additional data series in graphs. - Ensure consistent labeling and data handling for improved user experience.
1 parent d1c4a49 commit d50ca7a

File tree

4 files changed

+88
-42
lines changed

4 files changed

+88
-42
lines changed

app/controllers/performance_controller.rb

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ def index
1313
if project_scope
1414
# Single project performance view
1515
@timeframe = params[:timeframe] || 'hour'
16-
@hours_back = (params[:hours_back] || 24).to_i
16+
# Adaptive default window so the page shows data by default
17+
requested_hours = (params[:hours_back] || 24).to_i
18+
@hours_back = requested_hours
19+
unless PerformanceEvent.where(project: project_scope).where('occurred_at > ?', @hours_back.hours.ago).exists?
20+
@hours_back = [@hours_back, 168].max # 7 days
21+
end
22+
unless PerformanceEvent.where(project: project_scope).where('occurred_at > ?', @hours_back.hours.ago).exists?
23+
@hours_back = [@hours_back, 720].max # 30 days
24+
end
1725

1826
@rollups = project_scope.perf_rollups
1927
.where(timeframe: @timeframe)
@@ -34,14 +42,14 @@ def index
3442
.limit(20)
3543

3644
# Calculate project-specific metrics
37-
recent_rollups = project_scope.perf_rollups.where('timestamp > ?', 24.hours.ago)
38-
raw_events_scope = PerformanceEvent.where(project: project_scope).where('occurred_at > ?', 24.hours.ago)
45+
recent_rollups = project_scope.perf_rollups.where('timestamp > ?', @hours_back.hours.ago)
46+
raw_events_scope = PerformanceEvent.where(project: project_scope).where('occurred_at > ?', @hours_back.hours.ago)
3947
slow_threshold_ms = 1000
4048

4149
if recent_rollups.exists?
42-
total_requests = recent_rollups.sum(:request_count)
43-
total_errors = recent_rollups.sum(:error_count)
44-
avg_response = recent_rollups.average(:avg_duration_ms)
50+
total_requests = recent_rollups.sum(:request_count)
51+
total_errors = recent_rollups.sum(:error_count)
52+
avg_response = recent_rollups.average(:avg_duration_ms)
4553
slow_requests = raw_events_scope.where('duration_ms > ?', slow_threshold_ms).count
4654

4755
@metrics = {
@@ -56,9 +64,9 @@ def index
5664
avg_response = raw_events_scope.average(:duration_ms)
5765
slow_requests = raw_events_scope.where('duration_ms > ?', slow_threshold_ms).count
5866

59-
@metrics = {
60-
response_time: avg_response ? "#{avg_response.round(1)}ms" : "N/A",
61-
throughput: "#{total_requests}/day",
67+
@metrics = {
68+
response_time: avg_response ? "#{avg_response.round(1)}ms" : "N/A",
69+
throughput: "#{total_requests}/day",
6270
error_rate: total_requests > 0 ? "0.0%" : "0%",
6371
slow_requests: slow_requests
6472
}
@@ -192,9 +200,9 @@ def index
192200
@projects.each do |project|
193201
recent_rollups = project.perf_rollups.where('timestamp > ?', 24.hours.ago)
194202
if recent_rollups.exists?
195-
avg_response = recent_rollups.average(:avg_duration_ms)
196-
requests = recent_rollups.sum(:request_count)
197-
errors = recent_rollups.sum(:error_count)
203+
avg_response = recent_rollups.average(:avg_duration_ms)
204+
requests = recent_rollups.sum(:request_count)
205+
errors = recent_rollups.sum(:error_count)
198206
p95 = recent_rollups.average(:p95_duration_ms)
199207
else
200208
raw_events = PerformanceEvent.where(project: project).where('occurred_at > ?', 24.hours.ago)
@@ -332,21 +340,21 @@ def action_detail
332340
@rollups = synthetic.sort_by(&:timestamp)
333341
else
334342
# Calculate detailed metrics from rollups
335-
@total_requests = @rollups.sum(:request_count)
336-
@total_errors = @rollups.sum(:error_count)
337-
@avg_response_time = @rollups.average(:avg_duration_ms)
338-
@p50_response_time = @rollups.average(:p50_duration_ms)
339-
@p95_response_time = @rollups.average(:p95_duration_ms)
340-
@p99_response_time = @rollups.average(:p99_duration_ms)
341-
@min_response_time = @rollups.minimum(:min_duration_ms)
342-
@max_response_time = @rollups.maximum(:max_duration_ms)
343-
@error_rate = @total_requests > 0 ? ((@total_errors.to_f / @total_requests) * 100).round(2) : 0
344-
345-
# Group by timeframe for charts
346-
@hourly_data = @rollups.where('timestamp > ?', 24.hours.ago)
347-
.group_by { |r| r.timestamp.beginning_of_hour }
348-
349-
@daily_data = @rollups.group_by { |r| r.timestamp.beginning_of_day }
343+
@total_requests = @rollups.sum(:request_count)
344+
@total_errors = @rollups.sum(:error_count)
345+
@avg_response_time = @rollups.average(:avg_duration_ms)
346+
@p50_response_time = @rollups.average(:p50_duration_ms)
347+
@p95_response_time = @rollups.average(:p95_duration_ms)
348+
@p99_response_time = @rollups.average(:p99_duration_ms)
349+
@min_response_time = @rollups.minimum(:min_duration_ms)
350+
@max_response_time = @rollups.maximum(:max_duration_ms)
351+
@error_rate = @total_requests > 0 ? ((@total_errors.to_f / @total_requests) * 100).round(2) : 0
352+
353+
# Group by timeframe for charts
354+
@hourly_data = @rollups.where('timestamp > ?', 24.hours.ago)
355+
.group_by { |r| r.timestamp.beginning_of_hour }
356+
357+
@daily_data = @rollups.group_by { |r| r.timestamp.beginning_of_day }
350358
end
351359

352360

@@ -363,7 +371,7 @@ def action_detail
363371

364372
# Graph data for this action
365373
if @current_tab == 'graph'
366-
range_key = (params[:range] || '24H').to_s.upcase
374+
range_key = (params[:range] || '7D').to_s.upcase
367375
window_seconds = case range_key
368376
when '1H' then 1.hour
369377
when '4H' then 4.hours
@@ -391,20 +399,29 @@ def action_detail
391399
counts = Array.new(bucket_count, 0)
392400
labels = Array.new(bucket_count) { |i| start_time + i * bucket_seconds }
393401

394-
event_times = PerformanceEvent.where(project: project_scope, target: @target)
395-
.where('occurred_at >= ? AND occurred_at <= ?', start_time, end_time)
396-
.pluck(:occurred_at)
397-
event_times.each do |ts|
402+
events = PerformanceEvent.where(project: project_scope, target: @target)
403+
.where('occurred_at >= ? AND occurred_at <= ?', start_time, end_time)
404+
.pluck(:occurred_at, :duration_ms)
405+
sum_per_bucket = Array.new(bucket_count, 0.0)
406+
count_per_bucket = Array.new(bucket_count, 0)
407+
events.each do |ts, dur|
398408
idx = (((ts - start_time) / bucket_seconds).floor).to_i
399409
next if idx.negative? || idx >= bucket_count
400410
counts[idx] += 1
411+
if dur
412+
sum_per_bucket[idx] += dur.to_f
413+
count_per_bucket[idx] += 1
414+
end
401415
end
402416

403417
@graph_labels = labels
404418
@graph_counts = counts
405419
@graph_max = [counts.max || 0, 1].max
406420
@graph_has_data = counts.sum > 0
407421
@graph_range_key = range_key
422+
423+
# Additional series: average response time per bucket (ms)
424+
@graph_avg_ms = sum_per_bucket.each_with_index.map { |s, i| count_per_bucket[i] > 0 ? (s / count_per_bucket[i]).round(1) : 0 }
408425
end
409426

410427
# AI summary generation on demand

app/javascript/controllers/graph_controller.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,42 @@ export default class extends Controller {
55
static values = {
66
labels: Array,
77
counts: Array,
8-
range: String
8+
range: String,
9+
secondarySeries: Array,
10+
secondarySeriesLabel: String
911
}
1012

1113
connect() {
1214
const ctx = this.element.getContext('2d')
1315
const data = {
1416
labels: this.labelsValue,
1517
datasets: [{
16-
label: `Errors (${this.rangeValue})`,
18+
label: `Events (${this.rangeValue})`,
1719
data: this.countsValue,
1820
backgroundColor: 'rgba(99, 102, 241, 0.7)',
1921
borderColor: 'rgba(99, 102, 241, 1)',
2022
borderWidth: 1,
2123
borderRadius: 3,
2224
barPercentage: 0.95,
2325
categoryPercentage: 0.95,
26+
yAxisID: 'y',
2427
}]
2528
}
29+
30+
// Optional secondary line series (e.g., Avg ms)
31+
if (this.hasSecondarySeriesValue && Array.isArray(this.secondarySeriesValue) && this.secondarySeriesValue.length) {
32+
data.datasets.push({
33+
type: 'line',
34+
label: this.secondarySeriesLabelValue || 'Avg',
35+
data: this.secondarySeriesValue,
36+
borderColor: 'rgba(16, 185, 129, 1)',
37+
backgroundColor: 'rgba(16, 185, 129, 0.2)',
38+
borderWidth: 2,
39+
tension: 0.3,
40+
yAxisID: 'y1',
41+
pointRadius: 0,
42+
})
43+
}
2644
const options = {
2745
responsive: true,
2846
maintainAspectRatio: false,
@@ -31,7 +49,10 @@ export default class extends Controller {
3149
tooltip: {
3250
callbacks: {
3351
title: (items) => items[0]?.label || '',
34-
label: (item) => `Count: ${item.parsed.y}`
52+
label: (item) => {
53+
const dsLabel = item.dataset.label || 'Value'
54+
return `${dsLabel}: ${item.parsed.y}`
55+
}
3556
}
3657
}
3758
},
@@ -44,6 +65,12 @@ export default class extends Controller {
4465
beginAtZero: true,
4566
ticks: { color: '#6b7280', stepSize: Math.max(1, Math.ceil(Math.max(...this.countsValue) / 4)) },
4667
grid: { color: '#f3f4f6' }
68+
},
69+
y1: {
70+
position: 'right',
71+
beginAtZero: true,
72+
ticks: { color: '#10b981' },
73+
grid: { drawOnChartArea: false }
4774
}
4875
}
4976
}

app/views/performance/action_detail.html.erb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,12 @@
102102

103103
<% if @graph_counts.present? && @graph_has_data %>
104104
<% labels = (@graph_labels || []).map { |t| case @graph_range_key; when '1H','4H','8H','12H','24H','48H' then t.strftime('%H:%M'); else t.strftime('%b %d'); end } %>
105-
<div class="relative" style="height: 300px;">
105+
<div class="relative" style="height: 320px;">
106106
<canvas data-controller="graph"
107107
data-graph-labels-value='<%= labels.to_json %>'
108108
data-graph-counts-value='<%= @graph_counts.to_json %>'
109+
data-graph-secondary-series-label-value='Avg ms'
110+
data-graph-secondary-series-value='<%= (@graph_avg_ms || []).to_json %>'
109111
data-graph-range-value='<%= @graph_range_key %>'></canvas>
110112
</div>
111113
<% else %>
@@ -266,7 +268,7 @@
266268
<div class="text-sm font-medium text-gray-900 mb-2">Context</div>
267269
<pre class="bg-gray-50 rounded p-4 text-sm overflow-x-auto"><%= JSON.pretty_generate(ctx) rescue ctx.inspect %></pre>
268270
</div>
269-
<% else %>
271+
<% else %>
270272
<div class="px-2 py-10 text-center text-gray-500">Select a sample to view details</div>
271273
<% end %>
272274
</div>
@@ -294,15 +296,15 @@
294296
</div>
295297
<% end %>
296298
</li>
297-
<% end %>
299+
<% end %>
298300
</ul>
299301
</div>
300302
</div>
301303
</div>
302304
<% else %>
303305
<div class="px-6 py-10 text-center text-gray-500">No samples yet</div>
304306
<% end %>
305-
</div>
307+
</div>
306308
307309
<% else %>
308310
<!-- Key Metrics -->

app/views/performance/index.html.erb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,11 @@
133133
<tbody class="bg-white divide-y divide-gray-200">
134134
<% @list_rows.each do |perf_issue| %>
135135
<% row_link = if @current_project
136-
project_slug_performance_issue_path(@current_project.slug, id: (@list_rows.index(perf_issue) + 1))
136+
project_slug_performance_action_detail_path(@current_project.slug, target: perf_issue[:action])
137137
elsif @project
138-
project_performance_issue_path(@project, id: (@list_rows.index(perf_issue) + 1))
138+
project_performance_action_detail_path(@project, target: perf_issue[:action])
139139
else
140-
performance_issue_path(id: (@list_rows.index(perf_issue) + 1))
140+
performance_action_detail_path(target: perf_issue[:action])
141141
end %>
142142
<tr class="hover:bg-gray-50 transition-colors">
143143
<td class="px-6 py-4">

0 commit comments

Comments
 (0)