Skip to content

Optimize rake task + optimize index page loading #122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@ gem 'pg'
gem 'puma'
gem 'listen'
gem 'bootsnap'
gem 'rack-mini-profiler'

group :development, :test do
gem 'pghero'
gem 'bullet'
gem 'pg_query', '>= 2'
gem 'rspec-rails'
gem 'rspec-benchmark'
gem 'rack-mini-profiler'
gem 'ruby-prof'
gem 'memory_profiler'
gem 'capybara'
gem 'strong_migrations'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
71 changes: 71 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,42 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
base64 (0.2.0)
benchmark (0.4.0)
benchmark-malloc (0.2.0)
benchmark-perf (0.6.0)
benchmark-trend (0.4.0)
bigdecimal (3.1.9)
bootsnap (1.18.4)
msgpack (~> 1.2)
builder (3.3.0)
bullet (8.0.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
crass (1.0.6)
date (3.4.1)
diff-lcs (1.6.0)
drb (2.2.1)
erubi (1.13.1)
ffi (1.17.1-arm64-darwin)
globalid (1.2.1)
activesupport (>= 6.1)
google-protobuf (4.30.0-arm64-darwin)
bigdecimal
rake (>= 13)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
io-console (0.8.0)
Expand All @@ -107,6 +128,8 @@ GEM
net-pop
net-smtp
marcel (1.0.4)
matrix (0.4.2)
memory_profiler (1.1.0)
mini_mime (1.1.5)
minitest (5.25.4)
msgpack (1.8.0)
Expand All @@ -123,12 +146,17 @@ GEM
nokogiri (1.18.2-arm64-darwin)
racc (~> 1.4)
pg (1.5.9)
pg_query (6.0.0)
google-protobuf (>= 3.25.3)
pghero (3.6.1)
activerecord (>= 6.1)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
psych (5.2.3)
date
stringio
public_suffix (6.0.1)
puma (6.6.0)
nio4r (~> 2.0)
racc (1.8.1)
Expand Down Expand Up @@ -177,32 +205,75 @@ GEM
ffi (~> 1.0)
rdoc (6.12.0)
psych (>= 4.0.0)
regexp_parser (2.10.0)
reline (0.6.0)
io-console (~> 0.5)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-benchmark (0.6.0)
benchmark-malloc (~> 0.2)
benchmark-perf (~> 0.6)
benchmark-trend (~> 0.4)
rspec (>= 3.0)
rspec-core (3.13.3)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.1.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.2)
ruby-prof (1.7.1)
securerandom (0.4.1)
stringio (3.1.2)
strong_migrations (2.2.0)
activerecord (>= 7)
thor (1.3.2)
timeout (0.4.3)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uniform_notifier (1.16.0)
uri (1.0.2)
useragent (0.16.11)
websocket-driver (0.7.7)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.1)

PLATFORMS
arm64-darwin-21
arm64-darwin-24

DEPENDENCIES
bootsnap
bullet
capybara
listen
memory_profiler
pg
pg_query (>= 2)
pghero
puma
rack-mini-profiler
rails (~> 8.0.1)
rspec-benchmark
rspec-rails
ruby-prof
strong_migrations
tzinfo-data

RUBY VERSION
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/trips_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ class TripsController < ApplicationController
def index
@from = City.find_by_name!(params[:from])
@to = City.find_by_name!(params[:to])
@trips = Trip.where(from: @from, to: @to).order(:start_time)
@trips = Trip.includes(bus: :services).where(from: @from, to: @to).order(:start_time)
end
end
1 change: 0 additions & 1 deletion app/views/trips/_delimiter.html.erb

This file was deleted.

1 change: 0 additions & 1 deletion app/views/trips/_service.html.erb

This file was deleted.

6 changes: 0 additions & 6 deletions app/views/trips/_services.html.erb

This file was deleted.

5 changes: 0 additions & 5 deletions app/views/trips/_trip.html.erb

This file was deleted.

19 changes: 15 additions & 4 deletions app/views/trips/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,21 @@

<% @trips.each do |trip| %>
<ul>
<%= render "trip", trip: trip %>
<% if trip.bus.services.present? %>
<%= render "services", services: trip.bus.services %>
<li><%= "Отправление: #{trip.start_time}" %></li>
<li><%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %></li>
<li><%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %></li>
<li><%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %></li>
<li><%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %></li>

<% services = trip.bus.services %>
<% if services.present? %>
<li>Сервисы в автобусе:</li>
<ul>
<% services.each do |service| %>
<li><%= "#{service.name}" %></li>
<% end %>
</ul>
<% end %>
</ul>
<%= render "delimiter" %>
====================================================
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://guides.rubyonrails.org/layouts_and_rendering.html#spacer-templates

можно использовать рендеринг коллекций и даже там задать delimiter параметром

так можно и сохранить удобство паршлов и не так сильно проиграть в производительности

<% end %>
9 changes: 9 additions & 0 deletions benchmark/benchmark.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

require 'benchmark'

time = Benchmark.realtime do
system 'bin/rake utils:reload_json[fixtures/medium.json]'
end

puts "Finish in #{time.round(2)}"
154 changes: 154 additions & 0 deletions case-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Case-study оптимизации

## Case 1
Метрика:

`ruby benchmark/benchmark.rb`

Время работы rake таски на medium.json
Finish in 69.99

## Бюджет
Снизить время выполнения rake задачи до 10 секунд на файле large.json

## Фиксация текущей метрики
`bundle exec rspec spec/task/utils_performace_spec.rb:5`

## Профилировщик
Воспользовался ruby-profiler

`ruby profiling/ruby_prof.rb`

**Точки роста:**
```
99.66% 0.10% 24.91 0.02 0.00 24.89 7421 *ActiveRecord::ConnectionAdapters::DatabaseStatements#transaction
15.19 0.00 0.00 15.19 4265/4265 ActiveRecord::Validations#save
```

Создается множество отдельных операций к базе. Выполняются валидации, запуск транзакций и персист записей.

**Решение**

Пробую воспользоваться bulk_insert

После оптимизации:
Результат снизился до 8 секунд при загрузки данных из файла large.json

## Case 2
Метрика:
Загрузка страницы( /автобусы/Самара/Москва) происходит ~ 6.8 секунд, посмотрел в логи + rack-mini-profiler

## Бюджет
Снизить загрузку страницы до 0.4 секунд

**Точки роста**
N+1 в логах
Много раз рендеринг паршалов.

**Решение**
```ruby
@trips = Trip.includes(bus: :services).where(from: @from, to: @to).order(:start_time)
```

Для каждого объекта Trip подгружаю объекты Bus и для каждого объекта Bus подгружаю объекты Service
Также пока решил убрать паршелы и перенести во вьюху

После оптимизации:
Скорость загрузки страницы снизилась до 330ms ~ 0.3 секунды

## Case 3

Бюджет снизизить загрузку страницы до 0.1 секунды

## Профилировщик
Установил PGHero
В дашборде на вкладке Overview не показал что нужно создать индексы

`
No long running queries
Connections healthy 7
Vacuuming healthy
No columns near integer overflow
No invalid indexes or constraints
No duplicate indexes
No suggested indexes
No slow queries
`

На вкладке Queries отображает два запроса:

1. Выполняется 30% от общего времени

```ruby
SELECT COUNT(*) FROM "trips" WHERE "trips"."from_id" = $1 AND "trips"."to_id" = $2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

count по итогу нам вообще не нужен, тк мы грузим все данные и можем просто взять size

```

2. Выполняется 16% от общего времени

```ruby
SELECT "trips".* FROM "trips" WHERE "trips"."from_id" = $1 AND "trips"."to_id" = $2 ORDER BY "trips"."start_time" ASC
```
Добавил в explain
```ruby
SELECT "trips".* FROM "trips" WHERE "trips"."from_id" = 4 AND "trips"."to_id" = 2 ORDER BY "trips"."start_time" ASC
```

**Решение**
Добавил композитный индекс по двум полям
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

В результате:
для запроса:

```ruby
SELECT "trips".* FROM "trips" WHERE "trips"."from_id" = 4 AND "trips"."to_id" = 2 ORDER BY "trips"."start_time" ASC
```

```
Sort (cost=953.40..955.84 rows=977 width=34)
Sort Key: start_time
-> Bitmap Heap Scan on trips (cost=14.31..904.88 rows=977 width=34)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

лайк за explain

Recheck Cond: ((from_id = 4) AND (to_id = 2))
-> Bitmap Index Scan on index_trips_on_from_id_and_to (cost=0.00..14.06 rows=977 width=0)
Index Cond: ((from_id = 4) AND (to_id = 2))
```

для запроса:
```ruby
SELECT COUNT(*) FROM "trips" WHERE "trips"."from_id" = 4 AND "trips"."to_id" = 2
```
```
Aggregate (cost=26.27..26.29 rows=1 width=8)
-> Index Only Scan using index_trips_on_from_id_and_to on trips (cost=0.29..23.83 rows=977 width=0)
Index Cond: ((from_id = 4) AND (to_id = 2))
```

Скорость закрузки страницы снизилась от 144ms до 290 ms
Думаю на большом объеме данных результат будет более наглядным
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

да, вообще тут на время рендеринга страницы эти запросы не так уж влияют на фоне паршлов

но если бы мы подходили с точки зрения оптимизации БД - это были бы топовые запросы и нам индексы помогли бы их из этого топа скинуть


## Case 4
PGHero

Беру запрос который на первом месте в разделе Queries

37% от общего времени
```ruby
SELECT "buses_services".* FROM "buses_services" WHERE "buses_services"."bus_id" IN ($1, $2,......)
```

Explain отдал
```
CREATE INDEX CONCURRENTLY ON buses_services (bus_id)
Rows: 4534
Row progression: 4534, 5

Row estimates
- bus_id (=): 5

Existing indexes
- id PRIMARY
```

Добавляю индекс по bus_id

Скорость загрузки стариницы осталась примерно на том же уровне иногда опускаясь до 129 ms
Хотя PGHero показывает по прежнему 37% от общего времени выполнение запроса на получение сервисов, решил остановиться.
В бюджет почти уложился(хотел до 0.1 сек)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

11 changes: 11 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
Rails.application.configure do
=begin
config.after_initialize do
Bullet.enable = true
Bullet.alert = false
Bullet.bullet_logger = false
Bullet.console = false
Bullet.rails_logger = true
Bullet.add_footer = true
end
=end

# Settings specified here will take precedence over those in config/application.rb.

# In the development environment your application's code is reloaded on
Expand Down
Loading