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 @@ -25,3 +25,4 @@

# Ignore master key for decrypting credentials and more.
/config/master.key
database.yml
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ gem 'pg', '>= 0.18', '< 2.0'
gem 'puma', '~> 3.11'
gem 'bootsnap', '>= 1.1.0', require: false

gem 'pghero'
Copy link
Owner

Choose a reason for hiding this comment

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

👍

gem 'oj'
gem 'activerecord-import'
gem 'strong_migrations'
Copy link
Owner

Choose a reason for hiding this comment

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

👍 👍


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]
Expand Down
11 changes: 11 additions & 0 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.0.1)
activerecord (>= 3.2)
activestorage (5.2.3)
actionpack (= 5.2.3)
activerecord (= 5.2.3)
Expand Down Expand Up @@ -76,7 +78,10 @@ GEM
nio4r (2.3.1)
nokogiri (1.10.2)
mini_portile2 (~> 2.4.0)
oj (3.7.11)
pg (1.1.4)
pghero (2.2.0)
activerecord
puma (3.12.1)
rack (2.0.6)
rack-test (1.1.0)
Expand Down Expand Up @@ -117,6 +122,8 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
strong_migrations (0.3.1)
activerecord (>= 3.2.0)
thor (0.20.3)
thread_safe (0.3.6)
tzinfo (1.2.5)
Expand All @@ -134,12 +141,16 @@ PLATFORMS
ruby

DEPENDENCIES
activerecord-import
bootsnap (>= 1.1.0)
byebug
listen (>= 3.0.5, < 3.2)
oj
pg (>= 0.18, < 2.0)
pghero
puma (~> 3.11)
rails (~> 5.2.3)
strong_migrations
tzinfo-data
web-console (>= 3.3.0)

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/trips_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.where(from: @from, to: @to).order(:start_time).eager_load(bus: :services)
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
Copy link
Owner

Choose a reason for hiding this comment

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

👍

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
18 changes: 13 additions & 5 deletions app/views/trips/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@

<% @trips.each do |trip| %>
<ul>
<%= render "trip", trip: trip %>
<% if trip.bus.services.present? %>
<%= render "services", services: trip.bus.services %>
<% end %>
<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>
<% next unless trip.bus.services.present? %>
<li>Сервисы в автобусе:</li>
<ul>
<% trip.bus.services.each do |service| %>
<li><%= "#{service.name}" %></li>
<% end %>
</ul>
</ul>
<%= render "delimiter" %>
====================================================
<% end %>
132 changes: 132 additions & 0 deletions case-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
## Актуальная проблема
В нашем проекте возникла серьёзная проблема.

### Импорт данных
При выполнении `bin/setup` в базу данных загружаются данные о рейсах из файла fixtures/small.json
Сама загрузка данных из файла делается очень наивно.

В комплекте с заданием поставляются файлы
```
31M large.json
3,2M medium.json
308K small.json
```

Нужно оптимизировать механизм перезагрузки расписания из файла так, чтобы он обрабатывал файл large.json в пределах минуты.

### Отображение расписаний
Сами страницы расписаний тоже формируются не эффективно и при росте объёмов начинают сильно тормозить.

Нужно найти и устранить проблемы, замедляющие формирование этих страниц.

## Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы буду использовать такую метрику:
- Время выполнения программы на файле: small.json

Время выполнения исходного кода:
```
Loading data from fixtures/small.json
7.531087 0.446847 7.977934 ( 9.376662)
```

```
# ab -n 10 -c 10 http://localhost:3000/автобусы/Самара/Москва

Concurrency Level: 10
Time taken for tests: 1.874 seconds
Complete requests: 10
Failed requests: 0
Total transferred: 88112 bytes
HTML transferred: 81130 bytes
Requests per second: 5.34 [#/sec] (mean)
Time per request: 1874.175 [ms] (mean)
Time per request: 187.417 [ms] (mean, across all concurrent requests)
Transfer rate: 45.91 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 268 1127 449.1 943 1606
Waiting: 268 1127 449.1 943 1606
Total: 268 1127 449.1 943 1606

Percentage of the requests served within a certain time (ms)
50% 943
66% 1571
75% 1596
80% 1603
90% 1606
95% 1606
98% 1606
99% 1606
100% 1606 (longest request)
```

## Feedback-Loop
Для того, чтобы иметь возможность быстро проверять гипотезы я создал задачу `rails feedback:start`, которая позволит мне получать обратную связь по эффективности сделанных изменений за время ~7,5 секунд.
Copy link
Owner

Choose a reason for hiding this comment

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

👍 удобно!


## Вникаем в детали системы, чтобы найти 20% точек роста
Для того, чтобы найти "точки роста" для оптимизации я воспользовался библиотеками benchmark, ab.

Вот какие проблемы удалось найти и решить.

## Оптимизация 1
Импорт данных: замена парсинга на Oj и индексы таблиц бд, существенного изменения метрики не дали.
Чего нельзя сказать про использование гема activerecord-import.

```
Loading data from fixtures/small.json
0.462224 0.002725 0.464949 ( 0.520389)
```

## Оптимизация 2
Рендеринг: избавляемся от N+1 и рендеринга лишних партиалов.

```
# ab -n 10 -c 10 http://localhost:3000/автобусы/Самара/Москва

Concurrency Level: 10
Time taken for tests: 0.244 seconds
Complete requests: 10
Failed requests: 0
Total transferred: 92284 bytes
HTML transferred: 85340 bytes
Requests per second: 41.03 [#/sec] (mean)
Time per request: 243.738 [ms] (mean)
Time per request: 24.374 [ms] (mean, across all concurrent requests)
Transfer rate: 369.75 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 45 143 49.7 142 198
Waiting: 45 143 49.8 142 198
Total: 45 143 49.7 142 198

Percentage of the requests served within a certain time (ms)
50% 142
66% 182
75% 194
80% 197
90% 198
95% 198
98% 198
99% 198
100% 198 (longest request)
```

## Результаты
В результате проделанной оптимизации удалось улучшить метрику системы **c 7.5s до 0.5s**

```
#rails asymptotics:start

Loading data from fixtures/small.json
0.472344 0.007469 0.479813 ( 0.543996)

Loading data from fixtures/medium.json
2.892941 0.012608 2.905549 ( 3.054357)

Loading data from fixtures/large.json
28.034505 0.102898 28.137403 ( 29.604921)
```
85 changes: 0 additions & 85 deletions config/database.yml

This file was deleted.

1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Rails.application.routes.draw do
mount PgHero::Engine, at: "pghero"
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
get "/" => "statistics#index"
get "автобусы/:from/:to" => "trips#index"
Expand Down
9 changes: 9 additions & 0 deletions db/migrate/20190410133341_add_indexes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class AddIndexes < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_index :trips, [:from_id, :to_id], algorithm: :concurrently
Copy link
Owner

Choose a reason for hiding this comment

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

👍 за concurrently

add_index :trips, :bus_id, algorithm: :concurrently
add_index :buses_services, :bus_id, algorithm: :concurrently
add_index :buses_services, :service_id, algorithm: :concurrently
end
end
7 changes: 6 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2019_03_30_193044) do
ActiveRecord::Schema.define(version: 2019_04_10_133341) do

# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "plpgsql"

create_table "buses", force: :cascade do |t|
Expand All @@ -23,6 +24,8 @@
create_table "buses_services", force: :cascade do |t|
t.integer "bus_id"
t.integer "service_id"
t.index ["bus_id"], name: "index_buses_services_on_bus_id"
t.index ["service_id"], name: "index_buses_services_on_service_id"
end

create_table "cities", force: :cascade do |t|
Expand All @@ -40,6 +43,8 @@
t.integer "duration_minutes"
t.integer "price_cents"
t.integer "bus_id"
t.index ["bus_id"], name: "index_trips_on_bus_id"
t.index ["from_id", "to_id"], name: "index_trips_on_from_id_and_to_id"
end

end
Loading