From bdcfb5eb62a18913206d0f754f5fa563ea5d5dc0 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 23:57:25 +0200 Subject: [PATCH] Stats calculation is now timezone-aware. --- app/models/stat.rb | 12 +++++++- app/queries/stats/daily_distance_query.rb | 11 +++---- benchmark_stats.rb | 30 +++++++++++++++++++ .../future_add_timezone_to_users.rb.example | 16 ++++++++++ 4 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 benchmark_stats.rb create mode 100644 db/migrate/future_add_timezone_to_users.rb.example diff --git a/app/models/stat.rb b/app/models/stat.rb index f11e29d2..2cf26d04 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -37,6 +37,16 @@ class Stat < ApplicationRecord end def calculate_daily_distances(monthly_points) - Stats::DailyDistanceQuery.new(monthly_points, timespan).call + Stats::DailyDistanceQuery.new(monthly_points, timespan, user_timezone).call + end + + private + + def user_timezone + # Future: Once user.timezone column exists, uncomment the line below + # user.timezone.presence || Time.zone.name + + # For now, use application timezone + Time.zone.name end end diff --git a/app/queries/stats/daily_distance_query.rb b/app/queries/stats/daily_distance_query.rb index 4057fe9e..1673f1fa 100644 --- a/app/queries/stats/daily_distance_query.rb +++ b/app/queries/stats/daily_distance_query.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true class Stats::DailyDistanceQuery - def initialize(monthly_points, timespan) + def initialize(monthly_points, timespan, user_timezone = nil) @monthly_points = monthly_points @timespan = timespan + @user_timezone = user_timezone || 'UTC' end def call @@ -15,22 +16,22 @@ class Stats::DailyDistanceQuery private - attr_reader :monthly_points, :timespan + attr_reader :monthly_points, :timespan, :user_timezone 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, + EXTRACT(day FROM (to_timestamp(timestamp) AT TIME ZONE 'UTC' AT TIME ZONE '#{user_timezone}')) as day_of_month, CASE WHEN LAG(lonlat) OVER ( - PARTITION BY EXTRACT(day FROM to_timestamp(timestamp)) + PARTITION BY EXTRACT(day FROM (to_timestamp(timestamp) AT TIME ZONE 'UTC' AT TIME ZONE '#{user_timezone}')) ORDER BY timestamp ) IS NOT NULL THEN ST_Distance( lonlat::geography, LAG(lonlat) OVER ( - PARTITION BY EXTRACT(day FROM to_timestamp(timestamp)) + PARTITION BY EXTRACT(day FROM (to_timestamp(timestamp) AT TIME ZONE 'UTC' AT TIME ZONE '#{user_timezone}')) ORDER BY timestamp )::geography ) diff --git a/benchmark_stats.rb b/benchmark_stats.rb new file mode 100644 index 00000000..94049f14 --- /dev/null +++ b/benchmark_stats.rb @@ -0,0 +1,30 @@ +require 'benchmark' + +# Test the optimized stats calculation +data = Benchmark.measure do + user_id = 7 + + last_calculated_at = DateTime.new(1970, 1, 1) + + time_diff = last_calculated_at.to_i..Time.current.to_i + timestamps = Point.where(user_id:, timestamp: time_diff).pluck(:timestamp).uniq + + months = timestamps.group_by do |timestamp| + time = Time.zone.at(timestamp) + [time.year, time.month] + end.keys + + months.each do |year, month| + Stats::CalculateMonth.new(user_id, year, month).call + end +end + +puts "Stats calculation benchmark:" +puts "User Time: #{data.utime}s" +puts "System Time: #{data.stime}s" +puts "Total Time: #{data.real}s" + +# @real=28.869485000148416, +# @stime=2.4980050000000027, +# @total=20.303141999999976, +# @utime=17.805136999999974> diff --git a/db/migrate/future_add_timezone_to_users.rb.example b/db/migrate/future_add_timezone_to_users.rb.example new file mode 100644 index 00000000..caf60a88 --- /dev/null +++ b/db/migrate/future_add_timezone_to_users.rb.example @@ -0,0 +1,16 @@ +# Example migration for adding per-user timezone support +# To use: rename this file to remove .example and run rails db:migrate + +class AddTimezoneToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :timezone, :string, default: 'UTC' + add_index :users, :timezone + + # Populate existing users with application timezone + reversible do |dir| + dir.up do + User.update_all(timezone: Time.zone.name) + end + end + end +end \ No newline at end of file