diff --git a/.app_version b/.app_version index 1a44cad7..0f721773 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.30.1 +0.30.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 02ab8cb0..e7c5a882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index 0300deae..ef5ff993 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -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 diff --git a/app/models/stat.rb b/app/models/stat.rb index 0fa4e5e5..e291a71a 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -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) diff --git a/app/queries/stats/daily_distance_query.rb b/app/queries/stats/daily_distance_query.rb new file mode 100644 index 00000000..4057fe9e --- /dev/null +++ b/app/queries/stats/daily_distance_query.rb @@ -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 diff --git a/app/services/stats/bulk_calculator.rb b/app/services/stats/bulk_calculator.rb index aa74d60c..b803d3b1 100644 --- a/app/services/stats/bulk_calculator.rb +++ b/app/services/stats/bulk_calculator.rb @@ -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) diff --git a/app/services/stats/calculate_month.rb b/app/services/stats/calculate_month.rb index e9d6d64d..e996ac4f 100644 --- a/app/services/stats/calculate_month.rb +++ b/app/services/stats/calculate_month.rb @@ -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) diff --git a/app/views/stats/_stat.html.erb b/app/views/stats/_stat.html.erb index 470d3438..14430331 100644 --- a/app/views/stats/_stat.html.erb +++ b/app/views/stats/_stat.html.erb @@ -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}", diff --git a/app/views/stats/index.html.erb b/app/views/stats/index.html.erb index 2f14fa36..a5ab367b 100644 --- a/app/views/stats/index.html.erb +++ b/app/views/stats/index.html.erb @@ -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}", diff --git a/db/migrate/20250721204404_add_index_on_places_geodata_osm_id.rb b/db/migrate/20250721204404_add_index_on_places_geodata_osm_id.rb index 83359ec4..50f8ab97 100644 --- a/db/migrate/20250721204404_add_index_on_places_geodata_osm_id.rb +++ b/db/migrate/20250721204404_add_index_on_places_geodata_osm_id.rb @@ -2,8 +2,8 @@ class AddIndexOnPlacesGeodataOsmId < ActiveRecord::Migration[8.0] disable_ddl_transaction! def change - add_index :places, "(geodata->'properties'->>'osm_id')", - using: :btree, + add_index :places, "(geodata->'properties'->>'osm_id')", + using: :btree, name: 'index_places_on_geodata_osm_id', algorithm: :concurrently end