- <%= render "delimiter" %>
+<%= render partial: "trip", collection: @trips, spacer_template: 'delimiter' %>
+
+<% if @trips.length > 0 %>
+ <%= render "delimiter" %>
<% end %>
diff --git a/case-study-a.md b/case-study-a.md
new file mode 100644
index 00000000..d8c4f863
--- /dev/null
+++ b/case-study-a.md
@@ -0,0 +1,31 @@
+# Case-study оптимизации
+
+## Актуальная проблема
+В нашем проекте возникла серьёзная проблема.
+
+Необходимо было обработать файл с 1кк данных.
+
+У нас уже была программа на `ruby`, которая умела делать нужную обработку.
+
+Она успешно работала с файлом до 1к записей, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время.
+
+Я решил исправить эту проблему, оптимизировав эту программу.
+
+## Формирование метрики
+Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время загрузки medium файла (10к записей, 77 секунд в первой итерации)
+
+## Гарантия корректности работы оптимизированной программы
+Программа не поставлялась с тестом, поэтому перед выполнением оптимизации я добавил его самостоятельно: загрузка example файла с дальнейшим сравнением загруженных в бд данных с эталоном. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации.
+
+## Feedback-Loop
+Вот как я построил `feedback_loop`: профилирование - изменение кода - тестирование – бенчмаркинг – откат при отсутствии разницы от оптимизации/сохранение результатов
+
+## Вникаем в детали системы, чтобы найти главные точки роста
+Главной точкой роста является изменение загрузки на потоковую: посимвольное чтение файла, формирование целостного объекта trip и загрузка в систему путем использования функционала COPY в PG.
+
+## Результаты
+В результате проделанной оптимизации наконец удалось обработать файл с данными.
+Удалось улучшить метрику системы с с 77 секунд до 1.4с для medium и уложиться в заданный бюджет.
+Файл large грузится за 6.5 секунд
+Файл 1м стал грузится за 56 секунд.
+
diff --git a/case-study-b.md b/case-study-b.md
new file mode 100644
index 00000000..4e275460
--- /dev/null
+++ b/case-study-b.md
@@ -0,0 +1,55 @@
+# Case-study оптимизации
+
+## Актуальная проблема
+В нашем проекте возникла серьёзная проблема.
+Время загрузки страницы `автобусы/Самара/Москва` при наличии уже 100к поездок в базе данных превышало любой уровень терпения.
+
+Я решил исправить эту проблему, оптимизировав эту программу.
+
+## Формирование метрики
+Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время загрузки страницы `автобусы/Самара/Москва` при наличии 100к поездок в базе данных. Начальное измерение – 13.3с.
+
+## Гарантия корректности работы оптимизированной программы
+Программа не поставлялась с тестом, поэтому перед выполнением оптимизации я добавил его самостоятельно: результат работы страницы `автобусы/Самара/Москва` для данных из файла `fixtures/example.json` сравнивается с тем, который был сформирован до изменений.
+
+## Feedback-Loop
+Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось*
+
+Вот как я построил `feedback_loop`: профилирование - изменение кода - тестирование – бенчмаркинг – откат при отсутствии разницы от оптимизации/сохранение результатов
+
+## Вникаем в детали системы, чтобы найти главные точки роста
+Для того, чтобы найти "точки роста" для оптимизации я воспользовался rack mini profiler, bullet
+
+Вот какие проблемы удалось найти и решить
+
+### Ваша находка №1
+- bullet показал, что N+1 проблему `SELECT "buses".* FROM "buses" WHERE "buses"."id" = $1 LIMIT $2; ` + `SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1;`
+- делаю `.preload(bus: :services)` для `trips`
+- метрика снизилась до 6 секунд
+- количество sql запросов для `trips/index.html.erb` сократилось до 12
+
+### Ваша находка № 2
+- rack mini profiler (и логи веб сервера) показал, что основное время тратится на рендеринг шаблонов `trips/index.html.erb 2091.1
+4917.9`.
+- рендереринг всех коллекций через `render partial:`
+- метрика снизилась до 600мс
+- `Rendering: trips/index.html.erb 716.2 975.7`
+
+### Ваша находка № 3
+- rack mini profiler показал, что производится и запрос по count, и идентичный по выборке данных.
+- меняю count на length.
+- метрика особенно не снизилась
+- запрос count пропал из логов профилировщика
+
+### Ваша находка № 4
+- rack mini profiler показал, что долго выполняются запрос ` SELECT "trips".* FROM "trips" WHERE "trips"."from_id" = $1 AND "trips"."to_id" = $2 ORDER BY "trips"."start_time" ASC` (делаю explain, он показывает `Seq Scan on trips` как основную точку роста),
+- Добавляю индекс на связку `from_id`/`to_id`/`start_time`
+- метрика особенно не снизилась
+- время работы запроса снизилось с 18 до 2.5мс
+
+## Результаты
+В результате проделанной оптимизации наконец удалось обработать файл с данными.
+Удалось улучшить метрику системы с 13.3с до 0.6с.
+
+## Защита от регрессии производительности
+Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали*
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 1311e3e4..2c651d75 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -58,4 +58,8 @@
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+
+ # Bullet.enable = true
+ # Bullet.bullet_logger = true
+ # Bullet.console = true
end
diff --git a/config/initializers/strong_migrations.rb b/config/initializers/strong_migrations.rb
new file mode 100644
index 00000000..73a0aac4
--- /dev/null
+++ b/config/initializers/strong_migrations.rb
@@ -0,0 +1,26 @@
+# Mark existing migrations as safe
+StrongMigrations.start_after = 20240515192014
+
+# Set timeouts for migrations
+# If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user
+StrongMigrations.lock_timeout = 10.seconds
+StrongMigrations.statement_timeout = 1.hour
+
+# Analyze tables after indexes are added
+# Outdated statistics can sometimes hurt performance
+StrongMigrations.auto_analyze = true
+
+# Set the version of the production database
+# so the right checks are run in development
+# StrongMigrations.target_version = 10
+
+# Add custom checks
+# StrongMigrations.add_check do |method, args|
+# if method == :add_index && args[0].to_s == "users"
+# stop! "No more indexes on the users table"
+# end
+# end
+
+# Make some operations safe by default
+# See https://github.com/ankane/strong_migrations#safe-by-default
+# StrongMigrations.safe_by_default = true
diff --git a/db/migrate/20240515205445_add_from_id_to_id_index_to_trips.rb b/db/migrate/20240515205445_add_from_id_to_id_index_to_trips.rb
new file mode 100644
index 00000000..297bf4ee
--- /dev/null
+++ b/db/migrate/20240515205445_add_from_id_to_id_index_to_trips.rb
@@ -0,0 +1,7 @@
+class AddFromIdToIdIndexToTrips < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def change
+ add_index(:trips, %i[from_id to_id start_time], order: {start_time: :asc}, algorithm: :concurrently)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f6921e45..272eea24 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# 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: 2024_05_15_205445) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -40,6 +40,7 @@
t.integer "duration_minutes"
t.integer "price_cents"
t.integer "bus_id"
+ t.index ["from_id", "to_id", "start_time"], name: "index_trips_on_from_id_and_to_id_and_start_time"
end
end
diff --git a/lib/tasks/build_fixtures.rake b/lib/tasks/build_fixtures.rake
new file mode 100644
index 00000000..7e610ec6
--- /dev/null
+++ b/lib/tasks/build_fixtures.rake
@@ -0,0 +1,10 @@
+task :build_fixtures, [:file_name] => :environment do |_task, args|
+ ::UtilsService.call("fixtures/#{args.file_name}.json")
+
+ %i[cities buses services trips buses_services].each do |table_name|
+ File.open("test/fixtures/files/#{args.file_name}_#{table_name}.json", 'w+') do |file|
+ collection = ActiveRecord::Base.connection.execute("SELECT * FROM #{table_name} ORDER BY ID;").to_a.to_json
+ file.write(collection)
+ end
+ end
+end
diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake
index 540fe871..24ff057a 100644
--- a/lib/tasks/utils.rake
+++ b/lib/tasks/utils.rake
@@ -1,34 +1,5 @@
# Наивная загрузка данных из json-файла в БД
# rake reload_json[fixtures/small.json]
task :reload_json, [:file_name] => :environment do |_task, args|
- json = JSON.parse(File.read(args.file_name))
-
- ActiveRecord::Base.transaction do
- City.delete_all
- Bus.delete_all
- Service.delete_all
- Trip.delete_all
- ActiveRecord::Base.connection.execute('delete from buses_services;')
-
- json.each do |trip|
- from = City.find_or_create_by(name: trip['from'])
- to = City.find_or_create_by(name: trip['to'])
- services = []
- trip['bus']['services'].each do |service|
- s = Service.find_or_create_by(name: service)
- services << s
- end
- bus = Bus.find_or_create_by(number: trip['bus']['number'])
- bus.update(model: trip['bus']['model'], services: services)
-
- Trip.create!(
- from: from,
- to: to,
- bus: bus,
- start_time: trip['start_time'],
- duration_minutes: trip['duration_minutes'],
- price_cents: trip['price_cents'],
- )
- end
- end
+ UtilsService.call(args.file_name)
end
diff --git a/test/controllers/trips/index_test.rb b/test/controllers/trips/index_test.rb
new file mode 100644
index 00000000..d1025fc8
--- /dev/null
+++ b/test/controllers/trips/index_test.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require_relative '../../test_helper'
+
+class TripsController::IndexTest < ActionController::TestCase
+ def setup
+ ::UtilsService.call('fixtures/example.json')
+ end
+
+ def test_html
+ get(:index, params: {from: 'Самара', to: 'Москва'})
+
+ assert_response(:success)
+ assert_equal(@response.body.squish, File.read('test/fixtures/files/example_index.html').squish)
+ end
+end
diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/test/fixtures/files/example_buses.json b/test/fixtures/files/example_buses.json
new file mode 100644
index 00000000..effe6e3c
--- /dev/null
+++ b/test/fixtures/files/example_buses.json
@@ -0,0 +1 @@
+[{"id":1,"number":"123","model":"Икарус"}]
\ No newline at end of file
diff --git a/test/fixtures/files/example_buses_services.json b/test/fixtures/files/example_buses_services.json
new file mode 100644
index 00000000..0e5e177d
--- /dev/null
+++ b/test/fixtures/files/example_buses_services.json
@@ -0,0 +1 @@
+[{"id":1,"bus_id":1,"service_id":1},{"id":2,"bus_id":1,"service_id":2}]
\ No newline at end of file
diff --git a/test/fixtures/files/example_cities.json b/test/fixtures/files/example_cities.json
new file mode 100644
index 00000000..c5c1a303
--- /dev/null
+++ b/test/fixtures/files/example_cities.json
@@ -0,0 +1 @@
+[{"id":1,"name":"Москва"},{"id":2,"name":"Самара"}]
\ No newline at end of file
diff --git a/test/fixtures/files/example_index.html b/test/fixtures/files/example_index.html
new file mode 100644
index 00000000..f5c4814c
--- /dev/null
+++ b/test/fixtures/files/example_index.html
@@ -0,0 +1,112 @@
+
+
+
+ Task4
+
+
+
+
+
+
+
+
+