Optimize stats page performance

This commit is contained in:
Eugene Burmakin 2025-07-22 19:17:28 +02:00
parent 7c1c42dfc1
commit 88909b3e9f
14 changed files with 56 additions and 12 deletions

View file

@ -77,4 +77,5 @@ group :development do
gem 'database_consistency', require: false
gem 'foreman'
gem 'rubocop-rails', require: false
gem 'bullet'
end

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -4,7 +4,7 @@
</h2>
<div class='my-10'>
<%= column_chart(
Stat.year_distance(year, current_user),
@year_distances[year],
height: '200px',
suffix: " #{current_user.safe_settings.distance_unit}",
xtitle: 'Days',

View file

@ -82,7 +82,7 @@
</div>
<% 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',

View file

@ -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

View file

@ -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'

View file

@ -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.

View file

@ -6,6 +6,7 @@
- imports
- exports
- stats
- trips
- tracks
- reverse_geocoding
- visit_suggesting