-
Notifications
You must be signed in to change notification settings - Fork 115
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
Homework 3 (Lutovinova) #102
base: master
Are you sure you want to change the base?
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,11 @@ | ||
DB_USERNAME=postgres | ||
DB_PASSWORD=postgres | ||
DB_HOST=db | ||
DB_PORT=5432 | ||
DB_POOL=5 | ||
DB_NAME=optimization_3 | ||
|
||
PGHERO_USERNAME=postgres | ||
PGHERO_PASSWORD=postgres | ||
|
||
RAILS_ENV=development |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,7 @@ | ||
/.bundle | ||
/.idea | ||
/tmp | ||
/log | ||
/public | ||
.env | ||
fixtures/1M.json |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
FROM ruby:2.6.3-alpine | ||
|
||
RUN apk update && apk upgrade && apk add --update --no-cache \ | ||
build-base libc-dev tzdata bash htop shared-mime-info \ | ||
postgresql-dev postgresql-client | ||
|
||
WORKDIR /opt/app | ||
|
||
COPY Gemfile* ./ | ||
|
||
RUN gem install bundler -v 2.0.2 | ||
RUN bundle install | ||
|
||
COPY . . |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,29 @@ | ||
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) | ||
@from = params[:from] | ||
@to = params[:to] | ||
@trips_count = trips_query.count | ||
@trips = trips_query.order(:start_time) | ||
.joins(bus: :services) | ||
.select(trips_select.join(',')) | ||
.group('trips.id, buses.id') | ||
end | ||
|
||
private | ||
|
||
def trips_query | ||
@cities ||= City.where(name: [@from, @to]).pluck(:name, :id).to_h | ||
Trip.where(from_id: @cities[@from], to_id: @cities[@to]) | ||
end | ||
|
||
def trips_select | ||
[ | ||
'trips.start_time', | ||
'trips.duration_minutes', | ||
'trips.price_cents', | ||
'array_agg(services.name) as service_names', | ||
'buses.number as bus_number', | ||
'buses.model as bus_model' | ||
] | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
# frozen_string_literal: true | ||
class ApplicationRecord < ActiveRecord::Base | ||
self.abstract_class = true | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
# frozen_string_literal: true | ||
class Bus < ApplicationRecord | ||
MODELS = [ | ||
'Икарус', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
# frozen_string_literal: true | ||
class Service < ApplicationRecord | ||
SERVICES = [ | ||
'WiFi', | ||
|
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 |
---|---|---|
@@ -1,16 +1,26 @@ | ||
<h1> | ||
<%= "Автобусы #{@from.name} – #{@to.name}" %> | ||
<%= "Автобусы #{@from} – #{@to}" %> | ||
</h1> | ||
<h2> | ||
<%= "В расписании #{@trips.count} рейсов" %> | ||
<%= "В расписании #{@trips_count} рейсов" %> | ||
</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> | ||
|
||
<% if trip.service_names.present? %> | ||
<li>Сервисы в автобусе:</li> | ||
<ul> | ||
<% trip.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,93 @@ | ||
Первым шагом при выполнении задания была настройка запуска в докер и проверка корректности работы. | ||
После первого запуска скрипта на экспорт данных было заметно что он работает не очень быстро. | ||
|
||
### 1 часть задания. Оптимизация скрипта | ||
|
||
Скрипт решила оптимизировать первоначально по памяти, т.к. имеет место обработка файла + active record наверняка может создавать лишние объекты | ||
ну и хотелось в этом больше потренироваться. | ||
Как и ожидалось после запуска memoryprofiler больше всего памяти приходится на activerecord, поэтому так же как и во втором | ||
задании поэтапно начинаю оптимизацию. | ||
|
||
#### Замеры до оптимизации: | ||
- Время выполнения (fixtures/small.json): 6.97 сек | ||
- Потребляемая память (fixtures/small.json): 119 MB | ||
|
||
Далее перечислю основные моменты: | ||
1. Первое что решаю сделать это добавить frozen_string_literal, что дает небольшое улучшение. | ||
2. Далее, исходя из отчета самое большое количество памяти выделено на /activerecord-5.2.3/lib/active_record/log_subscriber.rb | ||
Заменила log_level на error для скрипта. Здесь же убрала bootsnap, т.к. периодически падал профилировщик. Точка роста сместилась | ||
3. Т.к далее основная нагрузка приходится на active record, решаю по максимуму убрать использование объектов active record. | ||
Для этого выношу код в отдельный класс, заменяю запросы удаления на truncate, подключаю гем activerecord-import и с помощью него уже строю импорт данных | ||
В результате время выполнения сократилась, но потребление памяти не очень. На первом месте остались объекты active record | ||
4. Далее решила в импорте не использовать recursive, а собрать все в массив и импортировать его и это уже принесло хороший результат: | ||
- Время выполнения (fixtures/small.json): 0.81 сек | ||
- Потребляемая память (fixtures/small.json): 113 MB | ||
|
||
|
||
- Время выполнения (fixtures/medium.json): 4.07 сек | ||
- Потребляемая память (fixtures/small.json): 112 MB | ||
|
||
|
||
- Время выполнения (fixtures/large.json): 34.86 сек | ||
- Потребляемая память (fixtures/small.json): 335 MB | ||
|
||
В результате оптимизации active record перестал быть главной точкой роста. | ||
|
||
5. После профилирования по времени на первом месте оказался PG::Connection#async_exec, судя по записанным данным самое большое количество запросов | ||
приходится на добавление связи bus и service. Ради интереса решила попробовать сделать их запись в одном запросе, что | ||
привело к снижению времени обработки. После этого решила попробовать по аналогии записывать и данные по trip и в итоге получила | ||
значительное снижение по времени, т.о от гема activerecord-import отказалась. Результаты: | ||
|
||
- Время выполнения (fixtures/small.json): 0.39 сек | ||
- Потребляемая память (fixtures/small.json): 110 MB | ||
|
||
|
||
- Время выполнения (fixtures/medium.json): 1.5 сек | ||
- Потребляемая память (fixtures/medium.json): 116 MB | ||
|
||
|
||
- Время выполнения (fixtures/large.json): 13.28 сек | ||
- Потребляемая память (fixtures/large.json): 330 MB | ||
|
||
|
||
- Время выполнения (fixtures/1M.json): 137.6 сек | ||
- Потребляемая память (fixtures/1M.json): 1895 MB | ||
|
||
По памяти много, но по времени кажется неплохо. На этом результате решила остановиться пока, потому что времени катастрофически не хватает. | ||
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 часть задания. Оптимизация страницы расписаний | ||
|
||
После импорта файла large.json страница загружалась примерно 24 секунды. | ||
Первым делом решила попробовать поставить pgHero, т.к. было интересно попробовать этот инструмент. | ||
|
||
Сразу после первого прогона был найден медленный запрос: | ||
```sql | ||
SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1 | ||
``` | ||
|
||
Поставила bullet и rack-mini-profiler. | ||
- Далее поправила запросы исходя из алертов и добавила . | ||
- из профайлера видно что очень много запросов вида: | ||
```sql | ||
SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1; | ||
``` | ||
то же показывает и pgHero, поэтому поправила этот момент тем то изменила выборку по trips - добавила в select только нужные для | ||
отображения аттрибуты и добавила joins. | ||
- объединила рендеры в один для удобства поиска необходимых аттрибутов | ||
- заменила поиск города по названию (влияет не сильно но тем не менее) | ||
|
||
В результате время отображения на сократилось до 182ms. По pgHero и bullet проблем больше не выявлено. | ||
|
||
Дальше я решила посмотреть анализ самого долгого запроса, который стал основным. | ||
По нему получается что самым долгим является часть джойна с buses_services, т.к там нет индексов. Такая же проблема на | ||
таблицах buses и trips. Добавляю индексы. | ||
|
||
В результате время отображения на сократилось до 130ms (с включенным профилировщиком) | ||
|
||
### Итоги | ||
|
||
Очень понравилось использовать 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. 👍 |
||
С bullet и rack-mini-profiler немного уже работала, поэтому они остаются полезными) | ||
|
||
P.S Прошу прощения за такую задержку с выполнением дз. Постараюсь наверстать все пропущенное до конца курса. | ||
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.
Можно использовать рендеринг коллекций.
Там не такая просадка производительности по сравнению с рендерингом паршлов в цикле
И даже можно параметром задать шаблон разделителя: https://guides.rubyonrails.org/layouts_and_rendering.html#spacer-templates