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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/tmp
/log
/public
.byebug_history
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.6.3
2.7.6
31 changes: 27 additions & 4 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,49 @@
# frozen_string_literal: true

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.3'
ruby '2.7.6'

gem 'rails', '~> 5.2.3'

gem 'pg', '>= 0.18', '< 2.0'
gem 'puma', '~> 3.11'

gem 'bootsnap', '>= 1.1.0', require: false
gem 'mimemagic', '~> 0.3.10'

gem 'activerecord-import'

gem 'fast_jsonparser'
gem 'oj'

gem 'strong_migrations'

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 'benchmark', '~> 0.2.1'
gem 'byebug', platforms: %i[mri mingw x64_mingw]
gem 'rspec-benchmark', '~> 0.6.0'
# gem 'rspec-rails', '~> 5.0.0'

gem 'bullet'
gem 'pghero'
gem 'pg_query', '>= 2'

gem 'memory_profiler'
gem 'rack-mini-profiler', require: false
gem 'stackprof'
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 'web-console', '>= 3.3.0'
end

group :test do
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
71 changes: 64 additions & 7 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ GEM
activemodel (= 5.2.3)
activesupport (= 5.2.3)
arel (>= 9.0)
activerecord-import (1.4.1)
activerecord (>= 4.2)
activestorage (5.2.3)
actionpack (= 5.2.3)
activerecord (= 5.2.3)
Expand All @@ -43,17 +45,27 @@ GEM
minitest (~> 5.1)
tzinfo (~> 1.1)
arel (9.0.0)
benchmark (0.2.1)
benchmark-malloc (0.2.0)
benchmark-perf (0.6.0)
benchmark-trend (0.4.0)
bindex (0.6.0)
bootsnap (1.4.2)
msgpack (~> 1.0)
bootsnap (1.16.0)
msgpack (~> 1.2)
builder (3.2.3)
bullet (7.0.7)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
byebug (11.0.1)
concurrent-ruby (1.1.5)
crass (1.0.4)
diff-lcs (1.5.0)
erubi (1.8.0)
ffi (1.10.0)
fast_jsonparser (0.6.0)
ffi (1.15.5)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-protobuf (3.22.2)
i18n (1.6.0)
concurrent-ruby (~> 1.0)
listen (3.1.5)
Expand All @@ -67,18 +79,28 @@ GEM
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
memory_profiler (1.0.1)
method_source (0.9.2)
mimemagic (0.3.3)
mimemagic (0.3.10)
nokogiri (~> 1)
rake
mini_mime (1.0.1)
mini_portile2 (2.4.0)
minitest (5.11.3)
msgpack (1.2.9)
msgpack (1.6.1)
nio4r (2.3.1)
nokogiri (1.10.2)
mini_portile2 (~> 2.4.0)
oj (3.14.2)
pg (1.1.4)
pg_query (4.2.0)
google-protobuf (>= 3.19.2)
pghero (2.8.3)
activerecord (>= 5)
puma (3.12.1)
rack (2.0.6)
rack-mini-profiler (3.0.0)
rack (>= 1.2.0)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.3)
Expand Down Expand Up @@ -109,6 +131,24 @@ GEM
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
rspec-mocks (~> 3.12.0)
rspec-benchmark (0.6.0)
benchmark-malloc (~> 0.2)
benchmark-perf (~> 0.6)
benchmark-trend (~> 0.4)
rspec (>= 3.0)
rspec-core (3.12.1)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-support (3.12.0)
ruby_dep (1.5.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
Expand All @@ -117,10 +157,14 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
stackprof (0.2.24)
strong_migrations (1.4.4)
activerecord (>= 5.2)
thor (0.20.3)
thread_safe (0.3.6)
tzinfo (1.2.5)
thread_safe (~> 0.1)
uniform_notifier (1.16.0)
web-console (3.7.0)
actionview (>= 5.0)
activemodel (>= 5.0)
Expand All @@ -134,17 +178,30 @@ PLATFORMS
ruby

DEPENDENCIES
activerecord-import
benchmark (~> 0.2.1)
bootsnap (>= 1.1.0)
bullet
byebug
fast_jsonparser
listen (>= 3.0.5, < 3.2)
memory_profiler
mimemagic (~> 0.3.10)
oj
pg (>= 0.18, < 2.0)
pg_query (>= 2)
pghero
puma (~> 3.11)
rack-mini-profiler
rails (~> 5.2.3)
rspec-benchmark (~> 0.6.0)
stackprof
strong_migrations
tzinfo-data
web-console (>= 3.3.0)

RUBY VERSION
ruby 2.6.3p62
ruby 2.7.6p219

BUNDLED WITH
2.0.2
2.3.24
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
task:
bundle exec rake reload_json[fixtures/large.json]

test:
bundle exec rails test

.PHONY: test
4 changes: 3 additions & 1 deletion app/controllers/trips_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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.preload(bus: :services)
.where(from: @from, to: @to)
.order(:start_time)
end
end
3 changes: 2 additions & 1 deletion app/models/bus.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class Bus < ApplicationRecord
].freeze

has_many :trips
has_and_belongs_to_many :services, join_table: :buses_services
has_many :buses_services
has_many :services, through: :buses_services

validates :number, presence: true, uniqueness: true
validates :model, inclusion: { in: MODELS }
Expand Down
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
3 changes: 2 additions & 1 deletion app/models/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ class Service < ApplicationRecord
'Можно не печатать билет',
].freeze

has_and_belongs_to_many :buses, join_table: :buses_services
has_many :buses_services
has_many :buses, through: :buses_services

validates :name, presence: true
validates :name, inclusion: { in: SERVICES }
Expand Down
19 changes: 14 additions & 5 deletions app/views/trips/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@
<%= "Автобусы #{@from.name}#{@to.name}" %>
</h1>
<h2>
<%= "В расписании #{@trips.count} рейсов" %>
<%= "В расписании #{@trips.load.size} рейсов" %>
Copy link
Collaborator

Choose a reason for hiding this comment

The 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>
<% if trip.bus.services.any? %>
<li>Сервисы в автобусе:</li>
<ul>
<% trip.bus.services.each do |service| %>
<li><%= "#{service.name}" %></li>
<% end %>
</ul>
<% 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.

Можно ещё с рендерингом коллекций и даже указать шаблон для разделителя https://guides.rubyonrails.org/layouts_and_rendering.html#spacer-templates

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

## Актуальная проблема
1. Долгий импорт данных в БД
2. Долгое отображение расписаний

## Формирование метрики
Mетрика: *время импорта данных в БД в секундах, расчитываемое на файле `large.json`.*
Бюджет: 60 секунд.

## Гарантия корректности работы оптимизированной программы
Тест соответствия скорости импорта бюджету
Тест корректности импорта (`fixtures/example.json`)

## Вникаем в детали системы, чтобы найти главные точки роста
Стартовый benchmark:
# small.json - 4.13s
# medium.json - 57.1s

### Неоптимальная запись в БД
Использование гема activerecord-import:
small.json - 2.26s
medium.json - 18.78s

Забавно, что использование гема oj для загрузки json дало обратный результат:
small.json - 2.31s
medium.json - 19.25s
large.json - 193.09s
В итоге остановился на FastJsonParse, добавили batch_size
small.json - 2.11s
medium.json - 18.28s
large.json - 182.09s

Осталось закэшировать bus_services, воспользуемся отсутствием false: id в schema
small.json - 0.8s
medium.json - 1.92s
large.json - 8.34s
Разница драматическая, на этом эту часть заканчиваем

Поставил pg_hero - eму все нравится, полезной инфы - нет
Copy link
Collaborator

Choose a reason for hiding this comment

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

Хмм, обычно он подсказывает индексы создать, если погонять запросы

Rails panel - не заработал
Bullet разумеется помог, но он меня достал уже на рабочем Проекте), здесь все гораздо хуже, ибо он лезет ото всюду - из поискового окна, прямо со страницы... там конечно все настраивается, но он все равно мне не нравится
rack-mini-profiler - великолепен: ненавязчивый и удобный, всегда им пользуюсь

Render time, start:
5746.0 ms 1886 sql - RMP
Completed 200 OK in 5737ms (Views: 4483.1ms | ActiveRecord: 1247.9ms)

### Рендеринг страницы
- Добавляем индексы поисковым полям: City#name, Trip#from/to (составной)
- size вместо count для trips
- в trip.html прелоадим bus для trip и services для bus
- #any? вместо #presence?
Completed 200 OK in 2865ms (Views: 2834.6ms | ActiveRecord: 24.0ms)
2874.2 ms 7 sql - запросы выглядят хорошо, но выигрыш по времени небольшой

Bullet молчит - но по RMP видно что многовато времени в сумме на паршиалы, объединим их в один
Completed 200 OK in 223ms (Views: 194.9ms | ActiveRecord: 22.5ms)
231.5 ms 7 sql - разница драматическая, не все паршиалы одинаково полезны). Заканчиваем.

Render time, finish
Completed 200 OK in 223ms (Views: 194.9ms | ActiveRecord: 22.5ms)
231.5 ms 7 sql

## Результаты
- импорт файла `fixtures/large.json`: 8-9 s
- время рендеринга страницы `автобусы/Самара/Москва` на базе файла large.json: 200-300 ms

*Какими ещё результами можете поделиться*
- Bullet ожидаемо утомителен
- RMP ожидаемо хорош
- паршиалы - красиво и архитектурно, но ни дороговато ли они стоят подчас? Это главный вывод для меня сегодня.
Copy link
Collaborator

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#rendering-collections

там не такая критичная просадка по производительности по сравнению с циклом

P.S. На работе пользуюсь AppSignal вместо Rollbar - пока доволен

## Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написал тесты:
Тест соответствия скорости импорта бюджету
Тест корректности импорта (`fixtures/example.json`)
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 = false
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 = false
Bullet.bullet_logger = true
Bullet.raise = false # 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
9 changes: 9 additions & 0 deletions config/initializers/rack_profiler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

if Rails.env.development?
require 'rack-mini-profiler'

# initialization is skipped so trigger it
Rack::MiniProfiler.config.show_total_sql_count = true
Rack::MiniProfilerRails.initialize!(Rails.application)
end
Loading