Skip to content

Homework solution #116

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
.DS_Store
2 changes: 2 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--require rails_helper
--color
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ruby 3.4.1
11 changes: 11 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby file: '.ruby-version'

gem 'rails', '~> 8.0.1'
gem "sprockets-rails"
gem 'pg'
gem 'puma'
gem 'listen'
gem 'bootsnap'
gem 'rack-mini-profiler'

gem "pghero"
gem "pg_query"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

group :development, :test do
gem 'rspec-rails', '~> 7.0.0'
gem 'rspec-benchmark'
gem "database_cleaner-active_record"
gem 'factory_bot_rails'
end
61 changes: 61 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,35 @@ GEM
uri (>= 0.13.1)
base64 (0.2.0)
benchmark (0.4.0)
benchmark-malloc (0.2.0)
benchmark-perf (0.6.0)
benchmark-trend (0.4.0)
bigdecimal (3.1.9)
bootsnap (1.18.4)
msgpack (~> 1.2)
builder (3.3.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
crass (1.0.6)
database_cleaner-active_record (2.2.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.4.1)
diff-lcs (1.6.0)
drb (2.2.1)
erubi (1.13.1)
factory_bot (6.5.1)
activesupport (>= 6.1.0)
factory_bot_rails (6.4.4)
factory_bot (~> 6.5)
railties (>= 5.0.0)
ffi (1.17.1-arm64-darwin)
globalid (1.2.1)
activesupport (>= 6.1)
google-protobuf (4.29.3-arm64-darwin)
bigdecimal
rake (>= 13)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
io-console (0.8.0)
Expand Down Expand Up @@ -123,6 +139,10 @@ GEM
nokogiri (1.18.2-arm64-darwin)
racc (~> 1.4)
pg (1.5.9)
pg_query (6.0.0)
google-protobuf (>= 3.25.3)
pghero (3.6.1)
activerecord (>= 6.1)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
Expand Down Expand Up @@ -179,7 +199,40 @@ GEM
psych (>= 4.0.0)
reline (0.6.0)
io-console (~> 0.5)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-benchmark (0.6.0)
benchmark-malloc (~> 0.2)
benchmark-perf (~> 0.6)
benchmark-trend (~> 0.4)
rspec (>= 3.0)
rspec-core (3.13.3)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.0.2)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.2)
securerandom (0.4.1)
sprockets (4.2.1)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
sprockets-rails (3.5.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
stringio (3.1.2)
thor (1.3.2)
timeout (0.4.3)
Expand All @@ -194,15 +247,23 @@ GEM
zeitwerk (2.7.1)

PLATFORMS
arm64-darwin-23
arm64-darwin-24

DEPENDENCIES
bootsnap
database_cleaner-active_record
factory_bot_rails
listen
pg
pg_query
pghero
puma
rack-mini-profiler
rails (~> 8.0.1)
rspec-benchmark
rspec-rails (~> 7.0.0)
sprockets-rails
tzinfo-data

RUBY VERSION
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.includes(bus: :services).where(from: @from, to: @to).order(:start_time)
end
end
1 change: 0 additions & 1 deletion app/views/trips/_service.html.erb

This file was deleted.

6 changes: 0 additions & 6 deletions app/views/trips/_services.html.erb

This file was deleted.

7 changes: 6 additions & 1 deletion app/views/trips/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
<ul>
<%= render "trip", trip: trip %>
<% if trip.bus.services.present? %>
<%= render "services", services: trip.bus.services %>
<li>Сервисы в автобусе:</li>
<ul>
<% trip.bus.services.each do |service| %>
<li><%= "#{service.name}" %></li>
<% end %>
</ul>
<% end %>
</ul>
<%= render "delimiter" %>
Expand Down
83 changes: 83 additions & 0 deletions case-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Case-study оптимизации

## Актуальные проблемы
В проекте возникли несколько серьёзных проблем.
- Долгий импорт данных, при объеме данных более 30 мегабайт
- При большом объеме данных начинает тормозить страница расписаний.

### Импорт данных
В приложении есть rake таска, которая удаляет все ранее загруженные данные, и добавляет новые из предоставленного файла.
```
bin/rake reload_json[file]
```
Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго.
Я решил исправить эту проблему, оптимизировав эту программу.

#### Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я добавил вывод времени выполнения программы (определяю временную метку в начале выполнения и в конце, и смотрю разницу).
Copy link
Collaborator

Choose a reason for hiding this comment

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

простое и достаточное решение 👍

При первом запуске программы с medium.json файлом она отработала за ~ 1 минуту.

#### Гарантия корректности работы оптимизированной программы
Для гарантии был добавлен тест, который в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации.
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

```
bundle exec rspec spec/tasks/utils_spec.rb
```

#### Профилирование
Чтобы понять какие проблемы с программой, я решил использовать логи, которые записываются в `log/development.log`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

Как говорит Nate Berkopec, если бы все смотрели логи, у меня не было бы работы


#### Поиск проблем
После первого запуска программы, логи показали очень большое количество SELECT запросов для таблиц cities, services и buses, это связано было с тем, что перед созданием записи мы проверяли наличие существующей записи.
Было решено добавить HASH переменные, в которые можно было бы по ключу добавлять записи, чтобы при необходимости получать нужную запись по ключу.
После этого программа для medium.json файла стала отрабатывать за ~ 12 секунд.

С large.json программа отрабатывала за ~ 1 минуту.
Логи показали на большое количество INSERT trips, потому что записи создавались по отдельности. Было решено использовать метод upsert_all, который добавляет записи одним запросом.
После этого программа для large.json файла стала отрабатывать за ~ 7 секунд.

Логи показали что осталисось много INSERT INTO "buses_services", но их пока не понятно как загружать
```
Bulk insert or upsert is currently not supported for has_many through association
```

#### Результаты
В результате проделанной оптимизации наконец удалось обработать файл с данными.
Удалось улучшить метрику с более 1 минуты, до примерно 7 секунд и уложиться в заданный бюджет.

#### Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы был добавлен тест.
```
bundle exec rspec spec/tasks/utils_spec.rb
```

### Отображение расписаний
Веб приложение отображает страницу расписания автобусов по направлениям. Она быстро отвечает при небольшом количестве данных, но при большом количестве данных начинаеат долго загружаться. Я решил исправить эту проблему оптимизировав загрузку страницы.
Для того, чтобы иметь возможность быстро проверять гипотезы я решил загрузить большое количество данных, найти самый популярный маршрут и посмотеть за сколько страница загружается.

#### Формирование метрики
Стандартный набор в браузере - панель разработчиков, в разделе Network, определять за сколько загружается страница. Дополнительно я добавил гем "mini-profiler-resources" который так же показывает время загрузки страницы.
Я загрузил данные из large.json файла и открыл страницу самого популярного маршрута Волгоград – Рыбинск, 1095 рейсов, страница грузилась ~ 8 секунд. Считается что лучшем временем для загрузки страницы является менее 2 секунд.

#### Гарантия корректности работы оптимизированной страницы
Для гарантии был добавлен тест, который в фидбек-лупе позволяет не допустить ошибок при оптимизации.
```
bundle exec rspec spec/controllers/trips_controller_spec.rb
```

#### Профилирование
Чтобы понимать какие возможны проблемы на странице я добавил "mini-profiler-resources", который показывает что происходит при загрузке страницы. Дополнительно я установил pghero, чтобы анализировать sql запросы.
Copy link
Collaborator

Choose a reason for hiding this comment

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

не пойму что такое mini-profiler-resources? rack-mini-profiler имеется в виду?

погуглил "mini-profiler-resources" - ничего не нашёл

Copy link
Author

Choose a reason for hiding this comment

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

Лол, даже сам не понимаю от куда я мог это взять))) да, должен быть rack-mini-profiler


#### Поиск проблем
Используя mini-profiler-resources я обнаруж что на странице делается 1975 sql запросов. Pghero так же показал что есть 2 запроса, которые делаюся более 1900 раз - Автобусы и Сервисы автобусов. Предполагаю что проблема на странице N+1 проблема. Для исправления добавил includes к Trip, для того чтобы формировались несколько запросов на все данные. После этого страница стала грузится за ~2.5 секунды, а количество sql запросов стало 5.

После mini-profiler-resources показал что на странице очень много рендрерится шаблонов - списка услуг и сами услуги. Я решил избавится от шаблонов и перенести всё основной шаблон. После чего страница стала грузится за 0,6 секунд
Copy link
Collaborator

Choose a reason for hiding this comment

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

я уже видел в чатике, что вы уже об этом узнали, но напишу что можно было бы использовать render collection API, там можно даже задать шаблон делимитера параметром https://guides.rubyonrails.org/layouts_and_rendering.html#spacer-templates

так получается не так сильно тормозит, но при этом можно сохранить удобство разбивки вьюх по паршлам


#### Результаты
Copy link
Collaborator

Choose a reason for hiding this comment

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

можно было бы ещё индексов накинуть; именно для рендеринга страницы тут это не так критично, хотя тоже помогает

но если бы мы заходили с точки зрения оптимизации БД - там бы это очень сильно помогло

(в тч составной индекс можно на trips(from, to))

Copy link
Author

Choose a reason for hiding this comment

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

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

Copy link
Author

Choose a reason for hiding this comment

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

Старался следовать философии оптимизации) оптимизировать только Главные точки и остановится когда достигли желаемого)

В результате проделанной оптимизации страница стала грузиться быстрее.
Удалось улучшить метрику с более 8 секунд, до менее 1 секунды и уложиться в заданный бюджет.

#### Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях был добавлен тест.
```
bundle exec rspec spec/controllers/trips_controller_spec.rb
```
2 changes: 1 addition & 1 deletion config/initializers/assets.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Be sure to restart your server when you modify this file.

# Version of your assets, change this if you want to expire all your assets.
# Rails.application.config.assets.version = '1.0'
Rails.application.config.assets.version = '1.0'

# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
Expand Down
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
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
mount PgHero::Engine, at: "pghero"
get "автобусы/:from/:to" => "trips#index"
end
15 changes: 15 additions & 0 deletions db/migrate/20250213181526_create_pghero_query_stats.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class CreatePgheroQueryStats < ActiveRecord::Migration[8.0]
def change
create_table :pghero_query_stats do |t|
t.text :database
t.text :user
t.text :query
t.integer :query_hash, limit: 8
t.float :total_time
t.integer :calls, limit: 8
t.timestamp :captured_at
end

add_index :pghero_query_stats, [:database, :captured_at]
end
end
28 changes: 19 additions & 9 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# 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[8.0].define(version: 2025_02_13_181526) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_catalog.plpgsql"
enable_extension "pg_stat_statements"

create_table "buses", force: :cascade do |t|
t.string "number"
Expand All @@ -29,6 +29,17 @@
t.string "name"
end

create_table "pghero_query_stats", force: :cascade do |t|
t.text "database"
t.text "user"
t.text "query"
t.bigint "query_hash"
t.float "total_time"
t.bigint "calls"
t.datetime "captured_at", precision: nil
t.index ["database", "captured_at"], name: "index_pghero_query_stats_on_database_and_captured_at"
end

create_table "services", force: :cascade do |t|
t.string "name"
end
Expand All @@ -41,5 +52,4 @@
t.integer "price_cents"
t.integer "bus_id"
end

end
Loading