Fix stats calculation performance

This commit is contained in:
Eugene Burmakin 2025-07-22 22:41:12 +02:00
parent bd2558ed29
commit 0c904a6b84
10 changed files with 87 additions and 14 deletions

View file

@ -1 +1 @@
0.30.1
0.30.2

View file

@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
# [0.30.2] - 2025-07-22
## Fixed
- Stats calculation is now significantly faster.
# [0.30.1] - 2025-07-22
## Fixed

View file

@ -13,6 +13,7 @@ class StatsController < ApplicationController
def show
@year = params[:year].to_i
@stats = current_user.stats.where(year: @year).order(:month)
@year_distances = { @year => Stat.year_distance(@year, current_user) }
end
def update

View file

@ -37,12 +37,7 @@ class Stat < ApplicationRecord
end
def calculate_daily_distances(monthly_points)
timespan.to_a.map.with_index(1) do |day, index|
daily_points = filter_points_for_day(monthly_points, day)
# Calculate distance in meters for consistent storage
distance_meters = Point.total_distance(daily_points, :m)
[index, distance_meters.round]
end
Stats::DailyDistanceQuery.new(monthly_points, timespan).call
end
def filter_points_for_day(points, day)

View file

@ -0,0 +1,64 @@
# frozen_string_literal: true
class Stats::DailyDistanceQuery
def initialize(monthly_points, timespan)
@monthly_points = monthly_points
@timespan = timespan
end
def call
daily_distances = daily_distances(monthly_points)
distance_by_day_map = distance_by_day_map(daily_distances)
convert_to_daily_distances(distance_by_day_map)
end
private
attr_reader :monthly_points, :timespan
def daily_distances(monthly_points)
Stat.connection.select_all(<<-SQL.squish)
WITH points_with_distances AS (
SELECT
EXTRACT(day FROM to_timestamp(timestamp)) as day_of_month,
CASE
WHEN LAG(lonlat) OVER (
PARTITION BY EXTRACT(day FROM to_timestamp(timestamp))
ORDER BY timestamp
) IS NOT NULL THEN
ST_Distance(
lonlat::geography,
LAG(lonlat) OVER (
PARTITION BY EXTRACT(day FROM to_timestamp(timestamp))
ORDER BY timestamp
)::geography
)
ELSE 0
END as segment_distance
FROM (#{monthly_points.to_sql}) as points
)
SELECT
day_of_month,
ROUND(COALESCE(SUM(segment_distance), 0)) as distance_meters
FROM points_with_distances
GROUP BY day_of_month
ORDER BY day_of_month
SQL
end
def distance_by_day_map(daily_distances)
daily_distances.index_by do |row|
row['day_of_month'].to_i
end
end
def convert_to_daily_distances(distance_by_day_map)
timespan.to_a.map.with_index(1) do |day, index|
distance_meters =
distance_by_day_map[day.day]&.fetch('distance_meters', 0) || 0
[index, distance_meters.to_i]
end
end
end

View file

@ -21,7 +21,7 @@ module Stats
last_calculated_at ||= DateTime.new(1970, 1, 1)
time_diff = last_calculated_at.to_i..Time.current.to_i
Point.where(user_id:, timestamp: time_diff).pluck(:timestamp)
Point.where(user_id:, timestamp: time_diff).pluck(:timestamp).uniq
end
def extract_months(timestamps)

View file

@ -50,7 +50,7 @@ class Stats::CalculateMonth
.tracked_points
.without_raw_data
.where(timestamp: start_timestamp..end_timestamp)
.select(:lonlat, :timestamp, :city, :country)
.select(:lonlat, :timestamp)
.order(timestamp: :asc)
end
@ -59,7 +59,13 @@ class Stats::CalculateMonth
end
def toponyms
CountriesAndCities.new(points).call
toponym_points = user
.tracked_points
.without_raw_data
.where(timestamp: start_timestamp..end_timestamp)
.select(:city, :country)
.distinct
CountriesAndCities.new(toponym_points).call
end
def create_stats_update_failed_notification(user, error)

View file

@ -20,7 +20,7 @@
<%= area_chart(
stat.daily_distance.map { |day, distance_meters|
[day, Stat.convert_distance(distance_meters, current_user.safe_settings.distance_unit).round(2)]
[day, Stat.convert_distance(distance_meters, current_user.safe_settings.distance_unit).round]
},
height: '200px',
suffix: " #{current_user.safe_settings.distance_unit}",

View file

@ -83,7 +83,7 @@
<% end %>
<%= column_chart(
@year_distances[year].map { |month_name, distance_meters|
[month_name, Stat.convert_distance(distance_meters, current_user.safe_settings.distance_unit).round(2)]
[month_name, Stat.convert_distance(distance_meters, current_user.safe_settings.distance_unit).round]
},
height: '200px',
suffix: " #{current_user.safe_settings.distance_unit}",