Skip to content

Task 3 #29

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 3 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
8 changes: 6 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -7,16 +7,20 @@ gem 'rails', '~> 5.2.3'
gem 'pg', '>= 0.18', '< 2.0'
gem 'puma', '~> 3.11'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'activerecord-import'

group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'pry'
gem 'rspec-rails'
gem 'rspec-benchmark'
end

group :development do
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
gem 'web-console', '>= 3.3.0'
gem 'listen', '>= 3.0.5', '< 3.2'
gem 'rack-mini-profiler'
gem 'bullet'
end

group :test do
50 changes: 48 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -33,6 +33,8 @@ GEM
activemodel (= 5.2.3)
activesupport (= 5.2.3)
arel (>= 9.0)
activerecord-import (1.0.4)
activerecord (>= 3.2)
activestorage (5.2.3)
actionpack (= 5.2.3)
activerecord (= 5.2.3)
@@ -43,13 +45,20 @@ GEM
minitest (~> 5.1)
tzinfo (~> 1.1)
arel (9.0.0)
benchmark-malloc (0.1.0)
benchmark-perf (0.5.0)
benchmark-trend (0.3.0)
bindex (0.6.0)
bootsnap (1.4.2)
msgpack (~> 1.0)
builder (3.2.3)
byebug (11.0.1)
bullet (6.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
coderay (1.1.2)
concurrent-ruby (1.1.5)
crass (1.0.4)
diff-lcs (1.3)
erubi (1.8.0)
ffi (1.10.0)
globalid (0.4.2)
@@ -77,8 +86,13 @@ GEM
nokogiri (1.10.2)
mini_portile2 (~> 2.4.0)
pg (1.1.4)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
puma (3.12.1)
rack (2.0.6)
rack-mini-profiler (1.1.6)
rack (>= 1.2.0)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.3)
@@ -109,6 +123,32 @@ GEM
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-benchmark (0.5.1)
benchmark-malloc (~> 0.1.0)
benchmark-perf (~> 0.5.0)
benchmark-trend (~> 0.3.0)
rspec (>= 3.0.0, < 4.0.0)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-expectations (3.9.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (3.9.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-support (~> 3.9.0)
rspec-support (3.9.2)
ruby_dep (1.5.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
@@ -121,6 +161,7 @@ GEM
thread_safe (0.3.6)
tzinfo (1.2.5)
thread_safe (~> 0.1)
uniform_notifier (1.13.0)
web-console (3.7.0)
actionview (>= 5.0)
activemodel (>= 5.0)
@@ -134,12 +175,17 @@ PLATFORMS
ruby

DEPENDENCIES
activerecord-import
bootsnap (>= 1.1.0)
byebug
bullet
listen (>= 3.0.5, < 3.2)
pg (>= 0.18, < 2.0)
pry
puma (~> 3.11)
rack-mini-profiler
rails (~> 5.2.3)
rspec-benchmark
rspec-rails
tzinfo-data
web-console (>= 3.3.0)

2 changes: 1 addition & 1 deletion app/controllers/trips_controller.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions app/models/buses_services.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class BusesServices < ApplicationRecord
belongs_to :bus
belongs_to :service
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.

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

<% @trips.each do |trip| %>
<ul>
<%= render "trip", trip: trip %>
<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? %>
<%= render "services", services: trip.bus.services %>
<li>Сервисы в автобусе:</li>
<ul>
<% trip.bus.services.each do |service| %>
<li><%= "#{service.name}" %></li>
<% end %>
</ul>
<% end %>
</ul>
<%= render "delimiter" %>
<%= '====================================================' %>
<% end %>
155 changes: 155 additions & 0 deletions case-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Case-study оптимизации

## Актуальная проблема
Дано веб-приложение для поиска рейсовых междугородних автобусов. В нем есть две основные проблемы.
1. Для наполнения базы данными по рейсам используется рейк-таска, которая импортрует информацию о маршрутах из
json файла. Операция импорта на больших файлах занимает слишком много времени. Необходимо снизить время этой операции.
2. Необходимо оптимизировать рендер страницы со списком маршрутов. Сейчас она загружается слишком долго.

## Формирование метрики
В обоих проблема ключевой метрикой является время работы. Для поставленых задач определим для себя такие цели:
1. Загрузка файла со 100_000 рейсов (large.json) в пределах 1 минуты.
2. Устранить освновные проблемы при рендере индекса рейсов, загруженных из `large.json`.

## Гарантия корректности работы оптимизированной программы
Перед тем, как дербанить программу, напишем простой тест, чтобы убедиться, что выдача ручки с индексом рейсов
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

не изменилась.
```
describe 'reload json task' do
...
it 'creates all instances' do
expect(City.count).to eq 2
expect(Service.count).to eq 2
expect(Bus.count).to eq 1
expect(Trip.count).to eq 10
end
it 'creates correct instances' do
bus_attrs.each do |k, v|
expect(Bus.first.attributes[k]).to eq v
end
expect((Service.pluck(:name) & service_names).size).to eq 2
expect((City.pluck(:name) & city_names).size).to eq 2
first_trip_attrs.each do |k, v|
expect(Trip.first.attributes[k]).to eq v
end
end
end
```

# Проблема 1
## Feedback loop
Для эффективного фидбек лупа напишем бенчмарк тест импорта данных.
```
describe 'large data import' do
it 'works under 1 minute' do
expect do
`rake "reload_json[fixtures/large.json]"`
end.to perform_under(60).sec
end
end
```

При первом прогоне он ожидаемо не проходит.

## Вникаем в детали системы, чтобы найти главные точки роста
### Итерация 1
Посмотрим на код таски импорта. Почти вся работа происходит в итерации по трипам.
Попробуем включить ActiveRecord логгер и записать вывод в файл.
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

```ruby
ActiveRecord::Base.logger = Logger.new(STDOUT)
```

```
$ touch asdf
$ bundle exec rake 'reload_json[fixtures/small.json]' > asdf
```

Посчитаем количество инсертов и селектов, которые делаются скриптом при импорте `small.json` (1000 трипов).
```
$ grep INSERT asdf | wc -l
4265
$ grep SELECT asdf | wc -l
9239
```

Проделаем то же самое с файлом `medium.json` (10_000 трипов).
```
$ grep INSERT asdf | wc -l
15705
$ grep SELECT asdf | wc -l
96639
```

Как видим, количество инсертов и селектов огромно, и растет вместе с количеством трипов. И хотя каждый
запрос достаточно быстр, в основном это считаные миллисекунды, все же огромное количество запросов
сильно замедляет импорт. Воспользуемся библиотечкой `activerecord-import`, чтобы драматически сократить
количество обращений в базу.

Activerecord-import умеет возвращать информацию о количестве инсертов - сразу спросим его, скольо инсертов он
сделал на те же инпуты, что в примерах выше.
```
$ bundle exec rake 'reload_json[fixtures/small.json]'
5
$ bundle exec rake 'reload_json[fixtures/medium.json]'
5
```

Чудно! количество инсертов не растет с инпутом.
Наш тест на корректность зеленый, поэтому попробуем тест на время импорта большого файла.
```
$ bundle exec rspec spec/tasks/reload_json_performance_spec.rb
.
Finished in 34.96 seconds (files took 0.51447 seconds to load)
1 example, 0 failures
```

Отлично! Импорт проходит за 35 секунд. Если попробовать утилиту `time`, то она покажет еще более
оптимистичный результат в 18 секунд:
```
$ time bundle exec rake 'reload_json[fixtures/large.json]'
bundle exec rake 'reload_json[fixtures/large.json]' 17.34s user 0.24s system 94% cpu 18.621 total
```

Воспользуемся в таске полезной фичей гема и сразу добавим вывод варнингов если есть зафейленые инсерты:
```
fails = []
...
fails += Service.import(services).failed_instances
fails += City.import(cities.to_a).failed_instances
fails += Bus.import(buses.to_a).failed_instances
...
if fails.any?
puts "Failed instances: #{fails}"
else
puts 'Everything is fine.'
end
```
И перейдем ко второй проблеме.

# Проблема 2
## Feedback loop
Поставим `rack-mini-profiler`, чтобы отслеживать время рендера. Первый результат - больше 6 секунд.

## Вникаем в детали системы, чтобы найти главные точки роста
Попробуем посмотреть детальные отчеты мини профайлера, а так же поставим `bullet` чтобы избежать лишних запросов.

### Итерация 1
Сразу же видим подсказки от буллета - добавим к трипам `includes(bus: :services)`
Время пребывания в базе сразу сократилось с примерно полутора секунд до 60мс.
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

Теперь основное время занимает рендеринг паршиалов.

### Итерация 2
Попробуем избавиться от них полностью, перенеся все их содержимое прямо в `index.html.erb`.
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#rendering-collections

Время рендера вьюх сократилось почти вдвое!
```
Completed 200 OK in 3959ms (Views: 3890.6ms | ActiveRecord: 52.4ms)
```

Улучшать рендеринг далее, без вмешательства в работу рендера в самой рельсе скорее всего не удастся,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Плюс за то, что не имеет смысла уменьшать 50мс на фоне 4000 👍

а улучшать время работы в базе на фоне времени вьюхи не имеет смысла, поэтому остановимся на таком результате.

# Результаты
1. Время импорта `large.json` сократилось до 19 секунд.
2. Время рендера индекса для этих данных сократилось до 4 секунд.
6 changes: 6 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
@@ -58,4 +58,10 @@
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker

config.after_initialize do
Bullet.enable = true
Bullet.add_footer = true
end
end

1 change: 0 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
get "/" => "statistics#index"
get "автобусы/:from/:to" => "trips#index"
end
Loading