From 88909b3e9fdefbad417daf0d96ecf17f00995c2c Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 19:17:28 +0200 Subject: [PATCH] Optimize stats page performance --- Gemfile | 1 + Gemfile.lock | 5 ++++ app/controllers/stats_controller.rb | 28 +++++++++++++++++++---- app/jobs/trips/calculate_all_job.rb | 2 +- app/jobs/trips/calculate_countries_job.rb | 2 +- app/jobs/trips/calculate_distance_job.rb | 2 +- app/jobs/trips/calculate_path_job.rb | 2 +- app/models/country.rb | 4 +++- app/views/stats/_year.html.erb | 2 +- app/views/stats/index.html.erb | 2 +- config/environments/development.rb | 9 ++++++++ config/environments/production.rb | 2 +- config/environments/test.rb | 6 +++++ config/sidekiq.yml | 1 + 14 files changed, 56 insertions(+), 12 deletions(-) diff --git a/Gemfile b/Gemfile index 614a2e95..0e11a1fa 100644 --- a/Gemfile +++ b/Gemfile @@ -77,4 +77,5 @@ group :development do gem 'database_consistency', require: false gem 'foreman' gem 'rubocop-rails', require: false + gem 'bullet' end diff --git a/Gemfile.lock b/Gemfile.lock index 640e815e..7d65bab2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,6 +113,9 @@ GEM brakeman (7.0.2) racc builder (3.3.0) + bullet (8.0.8) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11) bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) thor (~> 1.0) @@ -486,6 +489,7 @@ GEM unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) + uniform_notifier (1.17.0) uri (1.0.3) useragent (0.16.11) warden (1.2.9) @@ -519,6 +523,7 @@ DEPENDENCIES aws-sdk-s3 (~> 1.177.0) bootsnap brakeman + bullet bundler-audit capybara chartkick diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index a456452f..08074462 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -5,10 +5,30 @@ class StatsController < ApplicationController before_action :authenticate_active_user!, only: %i[update update_all] def index - @stats = current_user.stats.group_by(&:year).transform_values { |stats| stats.sort_by(&:updated_at).reverse }.sort.reverse - @points_total = current_user.tracked_points.count - @points_reverse_geocoded = current_user.total_reverse_geocoded_points - @points_reverse_geocoded_without_data = current_user.total_reverse_geocoded_points_without_data + @stats = current_user.stats.group_by(&:year).transform_values do |stats| + stats.sort_by(&:updated_at).reverse + end.sort.reverse + + # Single aggregated query to replace 3 separate COUNT queries + result = current_user.tracked_points.connection.execute(<<~SQL.squish) + SELECT#{' '} + COUNT(*) as total, + COUNT(reverse_geocoded_at) as geocoded, + COUNT(CASE WHEN geodata = '{}' THEN 1 END) as without_data + FROM points#{' '} + WHERE user_id = #{current_user.id} + SQL + + row = result.first + @points_total = row['total'].to_i + @points_reverse_geocoded = row['geocoded'].to_i + @points_reverse_geocoded_without_data = row['without_data'].to_i + + # Precompute year distance data to avoid N+1 queries in view + @year_distances = {} + @stats.each do |year, _stats| + @year_distances[year] = Stat.year_distance(year, current_user) + end end def show diff --git a/app/jobs/trips/calculate_all_job.rb b/app/jobs/trips/calculate_all_job.rb index 0500881c..3710df3e 100644 --- a/app/jobs/trips/calculate_all_job.rb +++ b/app/jobs/trips/calculate_all_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Trips::CalculateAllJob < ApplicationJob - queue_as :default + queue_as :trips def perform(trip_id, distance_unit = 'km') Trips::CalculatePathJob.perform_later(trip_id) diff --git a/app/jobs/trips/calculate_countries_job.rb b/app/jobs/trips/calculate_countries_job.rb index e63365d3..ed5ee884 100644 --- a/app/jobs/trips/calculate_countries_job.rb +++ b/app/jobs/trips/calculate_countries_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Trips::CalculateCountriesJob < ApplicationJob - queue_as :default + queue_as :trips def perform(trip_id, distance_unit) trip = Trip.find(trip_id) diff --git a/app/jobs/trips/calculate_distance_job.rb b/app/jobs/trips/calculate_distance_job.rb index 8a28e06f..15ff83c4 100644 --- a/app/jobs/trips/calculate_distance_job.rb +++ b/app/jobs/trips/calculate_distance_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Trips::CalculateDistanceJob < ApplicationJob - queue_as :default + queue_as :trips def perform(trip_id, distance_unit) trip = Trip.find(trip_id) diff --git a/app/jobs/trips/calculate_path_job.rb b/app/jobs/trips/calculate_path_job.rb index 711cfef8..f1323c5f 100644 --- a/app/jobs/trips/calculate_path_job.rb +++ b/app/jobs/trips/calculate_path_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Trips::CalculatePathJob < ApplicationJob - queue_as :default + queue_as :trips def perform(trip_id) trip = Trip.find(trip_id) diff --git a/app/models/country.rb b/app/models/country.rb index 9ef64687..e40a0c6d 100644 --- a/app/models/country.rb +++ b/app/models/country.rb @@ -12,6 +12,8 @@ class Country < ApplicationRecord end def self.names_to_iso_a2 - pluck(:name, :iso_a2).to_h + Rails.cache.fetch('countries_names_to_iso_a2', expires_in: 1.day) do + pluck(:name, :iso_a2).to_h + end end end diff --git a/app/views/stats/_year.html.erb b/app/views/stats/_year.html.erb index 886e2c96..3d8989b8 100644 --- a/app/views/stats/_year.html.erb +++ b/app/views/stats/_year.html.erb @@ -4,7 +4,7 @@
<%= column_chart( - Stat.year_distance(year, current_user), + @year_distances[year], height: '200px', suffix: " #{current_user.safe_settings.distance_unit}", xtitle: 'Days', diff --git a/app/views/stats/index.html.erb b/app/views/stats/index.html.erb index 96050095..2f14fa36 100644 --- a/app/views/stats/index.html.erb +++ b/app/views/stats/index.html.erb @@ -82,7 +82,7 @@
<% end %> <%= column_chart( - Stat.year_distance(year, current_user).map { |month_name, distance_meters| + @year_distances[year].map { |month_name, distance_meters| [month_name, Stat.convert_distance(distance_meters, current_user.safe_settings.distance_unit).round(2)] }, height: '200px', diff --git a/config/environments/development.rb b/config/environments/development.rb index c940de0e..36cb4be1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -3,6 +3,15 @@ require 'active_support/core_ext/integer/time' Rails.application.configure do + config.after_initialize do + Bullet.enable = true + Bullet.alert = true + Bullet.bullet_logger = true + Bullet.console = true + Bullet.rails_logger = true + Bullet.add_footer = true + end + # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded any time diff --git a/config/environments/production.rb b/config/environments/production.rb index 1e4b392a..53eedb18 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -29,7 +29,7 @@ Rails.application.configure do # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = true + config.assets.compile = false config.assets.content_type = { geojson: 'application/geo+json' diff --git a/config/environments/test.rb b/config/environments/test.rb index e138d076..b4884952 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -8,6 +8,12 @@ require 'active_support/core_ext/integer/time' # and recreated between test runs. Don't rely on the data there! Rails.application.configure do + config.after_initialize do + Bullet.enable = true + Bullet.bullet_logger = true + Bullet.raise = true # raise an error if n+1 query occurs + end + # Settings specified here will take precedence over those in config/application.rb. # While tests run files are not watched, reloading is not necessary. diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 87109364..c1966a7f 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -6,6 +6,7 @@ - imports - exports - stats + - trips - tracks - reverse_geocoding - visit_suggesting