-
Notifications
You must be signed in to change notification settings - Fork 115
ДЗ 3 #20
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?
ДЗ 3 #20
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,2 @@ | ||
class BusesService < ApplicationRecord | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# frozen_string_literal: true | ||
|
||
class UploadData | ||
def self.call(file_name) | ||
json = JSON.parse(File.read(file_name)) | ||
|
||
services = [] | ||
Service::SERVICES.each do |service| | ||
services << { name: service } | ||
end | ||
Service.import services | ||
uploaded_services = Service.all.inject({}) { |acc, elem| acc.merge!(elem.name => elem.id) } | ||
# uploaded_services == {"Ремни безопасности"=> Service, ...} | ||
|
||
cities = Set.new | ||
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. Кажется не очень хорошая идея здесь использовать |
||
buses = [] | ||
bus_numbers = {} | ||
json.each do |trip| | ||
# handle cities | ||
cities << { name: trip['from'] } | ||
cities << { name: trip['to'] } | ||
# handle buses | ||
next if bus_numbers[trip['bus']['number']].present? | ||
buses << Bus.new( | ||
number: trip['bus']['number'], | ||
model: trip['bus']['model'], | ||
service_names: trip['bus']['services'] | ||
) | ||
bus_numbers[trip['bus']['number']] = 1 | ||
end | ||
# save to DB | ||
result = City.import(cities.to_a, returning: [:id, :name]) | ||
uploaded_cities = result.results.inject({}) { |acc, elem| acc.merge!(elem[1] => elem[0]) } | ||
# uploaded_cities == {"Сочи"=>1, "Тула"=>2, "Самара"=>3, "Красноярск"=>4, "Волгоград"=>5, "Рыбинск"=>6, "Саратов"=>7, "Москва"=>8, "Ярославль"=>9, "Ростов"=>10} | ||
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. 👍 Плюс за коммент, без него было бы сложновато понять что получается |
||
result = Bus.import(buses, recursive: true, returning: [:id, :number]) | ||
uploaded_buses = result.results.inject({}) { |acc, elem| acc.merge!(elem[1] => elem[0]) } | ||
|
||
trips = [] | ||
buses_services = Set.new | ||
until json.empty? | ||
trip = json.shift | ||
|
||
trip['bus']['services'].each do |service| | ||
buses_services << { bus_id: uploaded_buses[trip['bus']['number']], service_id: uploaded_services[service] } | ||
end | ||
trips << { | ||
bus_id: uploaded_buses[trip['bus']['number']], | ||
from_id: uploaded_cities[trip['from']], | ||
to_id: uploaded_cities[trip['to']], | ||
start_time: trip['start_time'], | ||
duration_minutes: trip['duration_minutes'], | ||
price_cents: trip['price_cents'] | ||
} | ||
end | ||
BusesService.import [:bus_id, :service_id], buses_services.to_a | ||
Trip.import trips | ||
end | ||
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 |
---|---|---|
|
@@ -2,15 +2,24 @@ | |
<%= "Автобусы #{@from.name} – #{@to.name}" %> | ||
</h1> | ||
<h2> | ||
<%= "В расписании #{@trips.count} рейсов" %> | ||
<%= "В расписании #{@trips.size} рейсов" %> | ||
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. 👍 |
||
</h2> | ||
|
||
<% @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> | ||
<% unless trip.bus.service_names.empty? %> | ||
<li>Сервисы в автобусе:</li> | ||
<ul> | ||
<% trip.bus.service_names.each do |service| %> | ||
<li><%= "#{service}" %></li> | ||
<% end %> | ||
</ul> | ||
<% end %> | ||
</ul> | ||
<%= render "delimiter" %> | ||
==================================================== | ||
<% end %> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
# Case-study оптимизации | ||
|
||
## Актуальная проблема | ||
Оптимизация rails приложения | ||
|
||
1. Медленная загрузка изначальных данных | ||
2. Не оптимальная работа приложения по выводу информации | ||
|
||
## Формирование метрики | ||
Для первой части задания метрикой является время загрузка в БД изначальных данных | ||
Бюджет метрики - large файл с данными должен загружаться быстрее минуты | ||
|
||
Для второй части необходимо провести оптимизацию вывод информации | ||
После загрузки данных был произведен просмотр страницы | ||
|
||
результаты времени выполнение для large данных | ||
Rendered trips/index.html.erb within layouts/application (3082.9ms) | ||
Completed 200 OK in 3703ms (Views: 3358.3ms | ActiveRecord: 302.6ms) | ||
|
||
## Feedback-Loop | ||
Вот как я построил `feedback_loop` для проверки загрузки данных: | ||
т.к. идея на данном этапе понятна - это использовать activerecord-import и делать как можно меньше запросов, то: | ||
- проверить время загрузки | ||
- внести изменения в алгоритм | ||
- повторить | ||
|
||
`feedback_loop` для оптимизации вывода информации: | ||
- предварительная проверка через bullet | ||
- цикличная проверка через PgHero | ||
- поиск точки роста | ||
- модификация (добавление индексов, оптимизация запросов) | ||
- проверка результата | ||
- повторение | ||
|
||
## Находка 1 | ||
Раздумия над алгоритмом загрузки показали, что: | ||
- должно быть слишком много запросов на создание/поиск городов, т.к. их всего 10, то проще и быстрее создать их за 1 прозод через файл, сформировать список загруженных и затем использовать его вместо постоянного обращения к БД | ||
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. Совершенно верно 👍 |
||
- с сервисами такая же проблема, к тому же в описании задания сказано, что их всегда 10, можно заранее загрузить | ||
- для загрузки автобусов и маршрутов использовать activerecord-import с подставлением заранее загруженных данных по городам и сервисам | ||
- в БД нет индексов, поэтому загрузка данных будет максимально быстрой | ||
- добавил валидацию на уникальность для сервисов по названию (особо не имеет смысла, если сервисов всегда 10 и они не меняются, но пусть будет) | ||
|
||
### Решение 1 | ||
- гипотеза: использовать activerecord-import, | ||
- применение: сперва переделал алгоритм загрузка с использованием гема, в итоге время загрузка small файла уменьшилось, но даже medium файл загружается больше минуты | ||
|
||
### Решение 2 | ||
- гипотеза: проходить через json несколько раз, с загрузкой части данных | ||
- применение: | ||
- во время первого прохода через json формируются массивы для городов и сервисов, затем список городов формируется в хэш, { название => айди }, для загрузки путешествий нужен будет только айди города, сервисы - { название => объект }, т.к. для создания записей через has_and_belongs_to_many для автобусов нужно будет передавать массив объектов | ||
- во время второго прохода формируется список автобусов, для сервисов используются заранее загруженные данные | ||
- для городов и автобусов - данные формируются через результат работы гема, без доп запросов в БД | ||
- при третьем проходе формируются маршруты | ||
- результат | ||
- medium файл стал загружаться за 7 секнуд | ||
- large - 23 секунды | ||
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. 👍 |
||
|
||
### Решение 3 | ||
- вспомнил, что сервисы постоянны | ||
- применение: | ||
- сервисы создаются заранее, без прозода по массиву данных | ||
- на первом проходе через данные теперь формируется список городов и автобусов | ||
- на втором проходе - формируются маршруты | ||
результат | ||
- время обработки визуально не изменилось, но должно быть чуточку быстрее, особенно на гигантских данных | ||
- последние данные по времени загрузки - 22 секунды | ||
|
||
## Находка 2 | ||
- сообщение от bullet | ||
USE eager loading detected | ||
Trip => [:bus] | ||
Add to your query: .includes([:bus]) | ||
|
||
- добавил .includes(:bus) | ||
- сообщение от bullet | ||
USE eager loading detected | ||
Bus => [:services] | ||
Add to your query: .includes([:services]) | ||
- изменил на .includes(bus: :services) | ||
- результат: | ||
- выполняется всего 6 запросов в БД | ||
- много рендеринга паршиалов | ||
- загрузка из БД ускорилась в 10 раз | ||
- рендеринг в 2 раза | ||
Rendered trips/index.html.erb within layouts/application (1825.6ms) | ||
Completed 200 OK in 1862ms (Views: 1802.2ms | ActiveRecord: 38.3ms) | ||
|
||
## Находка 3 | ||
- теперь пора добавлять индексы для поиска | ||
- для поиска города по имени | ||
- для поиска маршрутов по двум полям, from/to, составной | ||
- для маршрутов отдельные индексы для bus_id и для start_time | ||
- составной индекс для таблицы buses_services | ||
|
||
Результаты PgHero (после внесение изменений в конфиг pg, для сбора статистики) | ||
No long running queries | ||
Connections healthy 7 | ||
Vacuuming healthy | ||
No columns near integer overflow | ||
No invalid indexes or constraints | ||
No duplicate indexes | ||
No slow queries | ||
|
||
Space вкладка | ||
- индексы для bus_id и start_time в trips бесполезные, надо удалить | ||
|
||
Queries вкладка | ||
- основные запросы - на создание индексов и какие-то рельсовые | ||
- запросы для улучшения | ||
- 8 мс 4 раза - получение маршрутов | ||
- 8 мс 4 раза - подсчет count для маршрутов (изменить на size, думаю лишний запрос) | ||
- остальное незначительно | ||
|
||
результат: | ||
- загрузка из БД ускорилась в 3 раза, общее ускорение БД - 30 раз | ||
Rendered trips/index.html.erb within layouts/application (1771.6ms) | ||
Completed 200 OK in 1784ms (Views: 1769.6ms | ActiveRecord: 12.7ms) | ||
|
||
## Находка 4 | ||
ПРОБЛЕМА | ||
- обнаружил, что не создались записи для связанной таблицы buses_services | ||
- решение исправил файл загрузки + создал класс BusesService для запуска импорта | ||
- результат: время загрузки уменьшилось до 20 секунд | ||
- видимо автобусы стали быстрее загружаться без дополнительных данных, а загрузка связанной таблицы занимает меньше времени | ||
|
||
Рендеринг с сервисами из БД | ||
Rendered trips/index.html.erb within layouts/application (4275.7ms) | ||
Completed 200 OK in 4299ms (Views: 4262.1ms | ActiveRecord: 28.4ms) | ||
|
||
Рендеринг без сервисов | ||
Rendered trips/index.html.erb within layouts/application (841.3ms) | ||
Completed 200 OK in 854ms (Views: 836.4ms | ActiveRecord: 15.7ms) | ||
|
||
Видно, что значительное время тратится на рендеринг названий сервисов для автобусов | ||
имеет смысл сделать сервисы статичными без использования БД и хранить их прямо в автобусах, а при изменении сервиса вызывать обновление всех связанных автобусов | ||
|
||
- проделал миграцию | ||
- результат импорта данных уменьшился до 19 секунд | ||
|
||
результаты рендеринга | ||
Rendered trips/index.html.erb within layouts/application (705.2ms) | ||
Completed 200 OK in 717ms (Views: 702.6ms | ActiveRecord: 12.0ms) | ||
|
||
## Заметки | ||
- пока таблицы services, buses_services выглядят бесполезными | ||
- но сервис может обновится, соответственно надо будет обновить список сервисов всех автобусов, связанных с ним | ||
- и если вдруг добавится/удалится сервис из автобуса | ||
- если удалить эти 2 таблицы, то ускорится время импорта исходных данных, но пока всё укладывается в метрику | ||
|
||
## Выводы | ||
- время импорта large данных - 19 секунд | ||
- рендеринг страницы | ||
Rendered trips/index.html.erb within layouts/application (705.2ms) | ||
Completed 200 OK in 717ms (Views: 702.6ms | ActiveRecord: 12.0ms) | ||
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. 👍 nice! |
||
- использовалось | ||
- [x] `bullet` | ||
- [x] `pghero` | ||
- здравый смысл и опыт | ||
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. 👍 |
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.
Грузим в память весь
json
, который может быть большимПлюс можно выиграть за счёт
Oj