-
Notifications
You must be signed in to change notification settings - Fork 115
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
base: master
Are you sure you want to change the base?
Task 3 #29
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
--require spec_helper |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
class BusesServices < ApplicationRecord | ||
belongs_to :bus | ||
belongs_to :service | ||
end |
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
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`. | ||
|
||
## Гарантия корректности работы оптимизированной программы | ||
Перед тем, как дербанить программу, напишем простой тест, чтобы убедиться, что выдача ручки с индексом рейсов | ||
не изменилась. | ||
``` | ||
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 логгер и записать вывод в файл. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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мс. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
Теперь основное время занимает рендеринг паршиалов. | ||
|
||
### Итерация 2 | ||
Попробуем избавиться от них полностью, перенеся все их содержимое прямо в `index.html.erb`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Можно и так, но чище было бы сделать render collection |
||
Время рендера вьюх сократилось почти вдвое! | ||
``` | ||
Completed 200 OK in 3959ms (Views: 3890.6ms | ActiveRecord: 52.4ms) | ||
``` | ||
|
||
Улучшать рендеринг далее, без вмешательства в работу рендера в самой рельсе скорее всего не удастся, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Плюс за то, что не имеет смысла уменьшать 50мс на фоне 4000 👍 |
||
а улучшать время работы в базе на фоне времени вьюхи не имеет смысла, поэтому остановимся на таком результате. | ||
|
||
# Результаты | ||
1. Время импорта `large.json` сократилось до 19 секунд. | ||
2. Время рендера индекса для этих данных сократилось до 4 секунд. |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍