Skip to content
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
9 changes: 9 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ gem 'puma'
gem 'listen'
gem 'bootsnap'
gem 'rack-mini-profiler'
gem 'activerecord-import'
gem 'pghero'
Copy link
Collaborator

Choose a reason for hiding this comment

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

<3

gem 'pg_query'
gem 'bullet'

group :development, :test do
gem 'rspec-rails', '~> 7.0.0'
gem 'rspec-benchmark'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
56 changes: 56 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ GEM
activemodel (= 8.0.1)
activesupport (= 8.0.1)
timeout (>= 0.4.0)
activerecord-import (2.1.0)
activerecord (>= 4.2)
activestorage (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
Expand All @@ -74,19 +76,33 @@ GEM
uri (>= 0.13.1)
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)
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)
ffi (1.17.1-x86_64-darwin)
globalid (1.2.1)
activesupport (>= 6.1)
google-protobuf (4.29.3-arm64-darwin)
bigdecimal
rake (>= 13)
google-protobuf (4.29.3-x86_64-darwin)
bigdecimal
rake (>= 13)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
io-console (0.8.0)
Expand Down Expand Up @@ -122,7 +138,13 @@ GEM
nio4r (2.7.4)
nokogiri (1.18.2-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.2-x86_64-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)
Expand Down Expand Up @@ -179,12 +201,39 @@ GEM
psych (>= 4.0.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.0.2)
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)
securerandom (0.4.1)
stringio (3.1.2)
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)
Expand All @@ -195,14 +244,21 @@ GEM

PLATFORMS
arm64-darwin-24
x86_64-darwin-24

DEPENDENCIES
activerecord-import
bootsnap
bullet
listen
pg
pg_query
pghero
puma
rack-mini-profiler
rails (~> 8.0.1)
rspec-benchmark
rspec-rails (~> 7.0.0)
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.where(from: @from, to: @to).order(:start_time).preload(bus: :services).load
end
end
4 changes: 4 additions & 0 deletions app/models/buses_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class BusesService < ApplicationRecord
belongs_to :bus
belongs_to :service
end
19 changes: 14 additions & 5 deletions app/views/trips/_trip.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
<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>
<ul>
<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>
<% if trip.bus.services.present? %>
<li>Сервисы в автобусе:</li>
<ul>
<%= render partial: "service", collection: trip.bus.services %>
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

</ul>
<% end %>
</ul>

13 changes: 3 additions & 10 deletions app/views/trips/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@
<%= "Автобусы #{@from.name} – #{@to.name}" %>
</h1>
<h2>
<%= "В расписании #{@trips.count} рейсов" %>
<%= "В расписании #{@trips.size} рейсов" %>
</h2>

<% @trips.each do |trip| %>
<ul>
<%= render "trip", trip: trip %>
<% if trip.bus.services.present? %>
<%= render "services", services: trip.bus.services %>
<% 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.

кажется делимитер потерялся

его тоже можно параметров в render collection задать, https://guides.rubyonrails.org/layouts_and_rendering.html#spacer-templates

<% end %>

<%= render partial: "trip", collection: @trips %>
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 👍

158 changes: 158 additions & 0 deletions case-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Case-study оптимизации

## Актуальные проблемы
В нашем проекте возникло несколько серьёзных проблем.
1) Долгий и не оптимальный импорт данных, при при увеличении объема
2) Страница Отображение расписаний, начи нает тормозить при увеличении данных.

## Импорт данных
В приложении есть rake таска, которая удаляет все ранее загруженные данные, и добавляет новые из выбранного файла.
```
bin/rake reload_json[file]
```
Возникла проблема что с увеличением данных программа выполнятеся долго.
Я решил исправить эту проблему, оптимизировав эту программу.
### Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику, программа должна выполнять файл large.json меньше чем за 60 сек.

Предворительно фиксируем что
- small.json: "Result Time 21.576042000000598"
- medium.json: "Result Time 125.41547200000787"

### Гарантия корректности работы оптимизированной программы
Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации.

Так же добавился тест чтобы проверить лоигку работы и скорость работы на разных объемах данных


### Feedback-Loop
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений.

Вот как я построил `feedback_loop`:
1) запустил программа на разных кол-вах данных
2) проверил логи, чтобы отследить проблемные места
3) внес изменения
4) прогнал тесты
5) зафиксировал результат


### Ваша находка
- запустил импорт exmaple.json файла и начал смотреть логи
Copy link
Collaborator

Choose a reason for hiding this comment

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

как говорит Nate Berkopec, если бы все смотрели логи, у меня бы не было работы

- по логами и логике видно что оснавное время тратится на лишние взаимодейстия с базой Select или сразу Insert.
- Оптимальным решением будет собрать данные сперва и потом импортировать их через active-storage-import, если не хочется тянуть гем то можно и чесез insert_all
+ так же пришлось для удобсва создать можель на связь BusesService
+ все объекты сохранял сразу в переменную и hash
```
to = cities[trip['to']] ||= City.new(name: trip['to'])
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

```
1) в переменную которая использутся в других обектах
2) аггрегатор который собиарет всё вместе
PS. Если не использовать одиночную переменную, а вытаскивать из агрегатора(везде это Hash), то тратится много времени на поиск объекта(если использовать только Hash)
```
"1 0.75399"
"2 20.596119"
"3 20.603491"
"4 20.698374"
"5 20.700828"
"6 76.098364"
"7 99.592368"
"Result Time 99.59396600000036"
```
- как результат я уложился в поставленную задачу, весь импорт large файла занимает 29.5 сек
```
"1 0.954356"
"2 7.650278"
"3 7.654027"
"4 7.734918"
"5 7.752177"
"6 8.211075"
"7 29.495458"
"Result Time 29.50383200000215"
```
## Страница Отображение расписаний
Когда рейсов становится слишком много страница с расписанием грузится очень медленно.
С файлом `medium.json` время страницы `http://localhost:3000/автобусы/Самара/Москва` - 595ms
С файлом `large.json` время страницы `http://localhost:3000/автобусы/Самара/Москва` - 11363ms
### Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы, найти очевидые слабые точки и решить их.
### Ваша находка 1
- на данных из medium.json, посещаю страницу
- для начала смотрим на rack-mini-profiler
```
Rendering: trips/index.html.erb 295.5 +8.0 138 sql 57.6
Rendering: trips/_trip.html.erb 3.0 +16.0 1 sql 0.5
```
так же смотрим через bullet, он подсвечивает что есть N+1
```
USE eager loading detected
Trip => [:bus]
Add to your query: .includes([:bus])
```
- добавим preload, чтобы заранее подтянуть все необходимые данные:
```
@trips = Trip.where(from: @from, to: @to).order(:start_time).preload(bus: :services)
```
- смотрим как результата bullet больше не выдаёт предупреждение, а в rack-mini-profiler видно что мы избивились от N+1
```
Executing: trips#index 5.4 +3.0 2 sql 0.5
Rendering: trips/index.html.erb 228.3 +7.5 5 sql 8.5
```
время запроса уменьшилось для medium.json с 600ms до 357ms, а для large.json c 11363ms до 8529.1 ms
### Ваша находка 2
- для начала смотрим на rack-mini-profiler, видно что лишних запросов больше нету, но тратится много времени на рендеринг _services
-
```
GET http://localhost:3000/%D0%B0%D0%B2%D1%82%... 1109.9 +0.0
Executing: trips#index 17.0 +2.0 2 sql 1.2
Rendering: trips/index.html.erb 3060.9 +7.3 5 sql 95.2
Rendering: trips/_services.html.erb 2.0 +1405.0
Rendering: trips/_services.html.erb 2.0 +1410.9
```
- заменяем обычный rednder на `render partial`
- скорость загрузки страницы упала с 8529ms до 3485.5ms
```
GET http://localhost:3000/%D0%B0%D0%B2%D1%82%... 1131.7 +0.0
Executing: trips#index 5.2 +3.0 2 sql 0.6
Rendering: trips/index.html.erb 2348.6 +7.4 5 sql 52.8
```
### Ваша находка 3
1) через rack-mini-profiler видим что есть один запрос с фильтрами, можем попробовать ускорить его через индексы
```
T+18.3 ms
SELECT "trips".* FROM "trips" WHERE "trips"."from_id" = $1 AND "trips"."to_id" = $2 ORDER BY "trips"."start_time" ASC;
```
2) добавил индекс на фильтруемые данные сразу отсортированные
`add_index :trips, %i[from_id to_id start_time], order: {start_time: :asc}, algorithm: :concurrently`
Copy link
Collaborator

Choose a reason for hiding this comment

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

я бы start_time не стал в этот составной индекс добавлять

[from_id, to_id] можно, а start_time лучше отдельно

3) как результат запрос стал немного быстрее
```
T+9.5 ms
SELECT "trips".* FROM "trips" WHERE "trips"."from_id" = $1 AND "trips"."to_id" = $2 ORDER BY "trips"."start_time" ASC;
```
но так же прибавил в памяти, для нас пока не влияет но надо учитывать index_trips_on_from_id_and_to_id_and_start_time 2.19 MB.
Выйгрыш по скорости не такой большой
Copy link
Collaborator

Choose a reason for hiding this comment

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

в данном случае маленькая изначально доля уходит на выполнение этого SQL-запроса, поэтому выигрыша большого не может быть

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

### Ваша находка 4
1) через rack-mini-profiler видим что есть запрос на count, попробуем заменить его на обработку через ruby.
```
T+8.6 ms
SELECT COUNT(*) FROM "trips" WHERE "trips"."from_id" = $1 AND "trips"."to_id" = $2;
```
общее время 3194.9 ms
2) заменил count на size (пришлось добавить еще load в trips controller: `@trips = Trip.where(from: @from, to: @to).order(:start_time).preload(bus: :services).load` )
3) как результат запрос в базу пропал, скорость всего запроса упала в среднем до 2993.5 ms
## Результаты
- В результате проделанной оптимизации наконец удалось обработать файл с данными и уложиться в поставленные нами метрики.
- Удалось улучшить отрисовку страницы расписаний с 11.3с до 0.3с.
Copy link
Collaborator

Choose a reason for hiding this comment

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

0,3 или 3?

9 changes: 9 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
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 on
Expand Down
6 changes: 6 additions & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
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.

# The test environment is used exclusively to run your application's
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
mount PgHero::Engine, at: "pghero"

get "автобусы/:from/:to" => "trips#index"
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddIndexToTripsFromIdAndToId < ActiveRecord::Migration[8.0]
disable_ddl_transaction!

def change
add_index :trips, %i[from_id to_id start_time], order: {start_time: :asc}, algorithm: :concurrently
end
end
Loading