diff --git a/Gemfile b/Gemfile
index e20b1260..721e3e40 100644
--- a/Gemfile
+++ b/Gemfile
@@ -24,3 +24,10 @@ end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+
+gem 'perfolab', github: 'Tab10id/perfolab', branch: :temp_26
+
+gem "activerecord-import", "~> 1.4"
+
+gem "rack-mini-profiler"
+gem "oj", "~> 3.14"
diff --git a/Gemfile.lock b/Gemfile.lock
index fccf6f5f..75c13d47 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,3 +1,15 @@
+GIT
+ remote: https://github.com/Tab10id/perfolab.git
+ revision: b75ed48af9aa69affc28a4034d47221a1690db26
+ branch: temp_26
+ specs:
+ perfolab (0.1.0)
+ memory_profiler (~> 1.0)
+ ruby-prof (~> 1.4, < 1.4.4)
+ stackprof (~> 0.2.0)
+ tabulo (~> 2.0)
+ zeitwerk (~> 2.6)
+
GEM
remote: https://rubygems.org/
specs:
@@ -33,6 +45,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)
@@ -67,8 +81,11 @@ 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)
@@ -76,9 +93,12 @@ GEM
nio4r (2.3.1)
nokogiri (1.10.2)
mini_portile2 (~> 2.4.0)
+ oj (3.14.2)
pg (1.1.4)
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)
@@ -109,6 +129,7 @@ GEM
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
+ ruby-prof (1.4.3)
ruby_dep (1.5.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
@@ -117,10 +138,16 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
+ stackprof (0.2.24)
+ tabulo (2.8.2)
+ tty-screen (= 0.8.1)
+ unicode-display_width (~> 2.2)
thor (0.20.3)
thread_safe (0.3.6)
+ tty-screen (0.8.1)
tzinfo (1.2.5)
thread_safe (~> 0.1)
+ unicode-display_width (2.4.2)
web-console (3.7.0)
actionview (>= 5.0)
activemodel (>= 5.0)
@@ -129,16 +156,21 @@ GEM
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
+ zeitwerk (2.6.7)
PLATFORMS
ruby
DEPENDENCIES
+ activerecord-import (~> 1.4)
bootsnap (>= 1.1.0)
byebug
listen (>= 3.0.5, < 3.2)
+ oj (~> 3.14)
+ perfolab!
pg (>= 0.18, < 2.0)
puma (~> 3.11)
+ rack-mini-profiler
rails (~> 5.2.3)
tzinfo-data
web-console (>= 3.3.0)
diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb
index acb38be2..eb2dee12 100644
--- a/app/controllers/trips_controller.rb
+++ b/app/controllers/trips_controller.rb
@@ -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).preload(bus: :services).order(:start_time)
end
end
diff --git a/app/views/trips/_services.html.erb b/app/views/trips/_services.html.erb
deleted file mode 100644
index 2de639fc..00000000
--- a/app/views/trips/_services.html.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-
Сервисы в автобусе:
-
- <% services.each do |service| %>
- <%= render "service", service: service %>
- <% end %>
-
diff --git a/app/views/trips/_trip.html.erb b/app/views/trips/_trip.html.erb
index fa1de9aa..08a8eb09 100644
--- a/app/views/trips/_trip.html.erb
+++ b/app/views/trips/_trip.html.erb
@@ -1,5 +1,13 @@
-<%= "Отправление: #{trip.start_time}" %>
-<%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %>
-<%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %>
-<%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %>
-<%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %>
+
+ - <%= "Отправление: #{trip.start_time}" %>
+ - <%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %>
+ - <%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %>
+ - <%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %>
+ - <%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %>
+ <% if trip.bus.services.present? %>
+ - Сервисы в автобусе:
+
+ <%= render partial: 'service', collection: trip.bus.services %>
+
+ <% end %>
+
\ No newline at end of file
diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb
index a60bce41..d123e89e 100644
--- a/app/views/trips/index.html.erb
+++ b/app/views/trips/index.html.erb
@@ -5,12 +5,4 @@
<%= "В расписании #{@trips.count} рейсов" %>
-<% @trips.each do |trip| %>
-
- <%= render "trip", trip: trip %>
- <% if trip.bus.services.present? %>
- <%= render "services", services: trip.bus.services %>
- <% end %>
-
- <%= render "delimiter" %>
-<% end %>
+<%= render partial: 'trip', collection: @trips, spacer_template: 'delimiter' %>
diff --git a/case-study.md b/case-study.md
new file mode 100644
index 00000000..093b25cc
--- /dev/null
+++ b/case-study.md
@@ -0,0 +1,316 @@
+# Оптимизация взаимодействия с БД
+
+## User story
+
+Тут должны быть описаны пользовательские истории которые определят целевые
+значения метрик, но так как их нет, то я их придумаю.
+
+### Импорт данных
+
+Для проведения нагрузочного тестирования на тестовом стенде необходимо регулярно
+импортировать различные наборы данных о расписании автобусов из больших
+текстовых файлов, файлы получаются из внешних систем поэтому нет возможности
+осуществлять накат предварительно сформированного sql-дампа.
+Текущее решение не позволяет выполять эту операцию быстро из-за чего подготовка
+тестовой среды происходит непозволительно долгое время чем блокирует всю
+дальнейшую работу пайплайна.
+
+### Отображение расписания
+
+Система веб-аналитики показывает, что при просмотре расписаний на популярных
+направлениях пользователи не дожидаются загрузки страницы и уходят с сайта.
+Британские учёные доказали, что финансово успешные web-проекты должны
+формировать основной документ страницы не больше чем за 300 миллисекунд.
+
+## Что сделать
+
+* Нужно оптимизировать механизм перезагрузки расписания из файла
+ * файл large.json должен обрабатывался не более минуты.
+* Необходимо ускорить отображение расписаний
+ * страница автобусы/Самара/Москва должна открываться менее чем за 300мс
+
+# Оптимизация импорта
+
+## Основные инструменты для исследования
+
+* perfolab (чуть доведенный до ума фреймворк разработанный мной в рамках предыдущего задания)
+* stackprof
+* speedscope
+
+## Основные проблемы на начальном этапе
+
+* Отсутствие тестов.
+ * Нет возможности проводить оптимизацию без опасения что-либо сломать.
+* Вся процедура выполняется в едином блоке кода без разбивки на методы.
+ * Сложно выяснить что именно вызывает основные проблемы.
+
+## Первичный анализ
+
+Замеры времени импорта показывют что оно растёт приблизительно линейно в
+зависимости от размера файла, что несложно увидеть и при обычном чтении
+оригинального скрипта. Инструменты показывают что большую часть времени
+выполняются запросы на поиск и создание объектов.
+
+Уже на данном этапе очевидно что необходимо кардинально уменьшать количество
+отправляемых в БД запросов. Внимательное изучение скрипта показывает что
+от запросов на поиск данных в бд можно полностью избавиться, так как изначально
+в БД нет никаких данных. Уменьшения же количества запросов на вставку данных
+можно добиться с помощью массового создания объектов.
+
+Для удобства можно воспользоваться библиотекой activerecord-import.
+
+## Первые шаги
+
+* Код импорта был выделен в отдельный класс для удобства анализа и тестрования.
+* Был написан минимальный тест на корректнось работы скрипта.
+* Произведен первичный анализ скрипта на файле small.json
+
+| benchmark | Previous | Current | Diff % |
+|--------------------------------|----------|---------|-----------------|
+| total | | 3014ms | |
+
+## Выделение методов
+
+Хоть у нас и есть инсайдерская информация о частоте появления уникальных записей
+различных моделей, всё же будет намного удобнее ориентироваться в результатах
+профилировщиков если выделить создание разных объектов в отельные методы.
+Как и ожидалось, выделение методов не оказало значительного влияния на время
+работы импорта.
+
+| benchmark | Previous | Current | Diff % |
+|--------------------------------|-----------|---------|----------------|
+| total | 3014ms | 2976ms | -1% |
+
+Кроме того, сразу видно что 59% времени уходит на создание и обновление записей
+об автобусах.
+
+## Оптимизация создания автобусов
+
+При внимательном изучении механизма создания автобусов становится очевидным
+что выполняются множество лишних действий:
+* лишнее обновление записи, имя модели выставляется уже после создания;
+* обновление модели и услуг происходит каждый раз, даже если автобус уже есть в БД;
+* поиск автобусов в БД, хотя их создание происходило в этом же скрипте.
+
+Результат исправления:
+
+| benchmark | Previous | Current | Diff % |
+|--------------------------------|----------|---------|-----------------|
+| total | 2976ms | 2628ms | -11% |
+
+## Оптимизация выставления связей между автобусами и услугами
+
+Результат улучшения оказался не столь существенным для файла small.json,
+на обновление данных об услугах в автобусах всё ещё уходит около 45% времени,
+это говорит о том, что нужно оптимизировать выставление связей между автобусами
+и сервисами.
+Оптимизировать это можно несколькими способами, но самым эффективным будет
+массовое выставление связей уже после создания записей автобусов.
+
+Результат исправления:
+
+| benchmark | Previous | Current | Diff % |
+|--------------------------------|----------|----------|-----------------|
+| total | 2628ms | 1450ms | -44% |
+
+## Создание сервисов
+
+На поиск и создание сервисов уходит 34% времени. При этом заранее известно что
+их ровно 10 (валидация на имя в модели и подсказка в Readme.md).
+Вместо того чтобы осуществлять поиск и создание, можно было бы заранее создать
+все 10 записей игнорируя данные из json. Но так как у нас есть требование на
+полное отсутствие функциональных изменений, то отказываемся от этой идеи, так
+как для файла example.json должно быть создано только 3 сервиса.
+Так или иначе можно избавиться от необходимости поиска записей в БД.
+
+| benchmark | Previous | Current | Diff % |
+|--------------------------------|----------|----------|-----------------|
+| total | 1450ms | 885ms | -38% |
+
+## Импорт расписаний
+
+Наконец дошли до основной модели. Замеры показывают что 39% времени уходит на
+создание записей модели Trip.
+Так же как и с привязкой услуг к автобусам используем массовый импорт заранее
+подготовленных данных.
+
+| benchmark | Previous | Current | Diff % |
+|--------------------------------|----------|----------|-----------------|
+| total | 885ms | 626ms | -29% |
+
+## Оптимизация создания городов
+
+Теперь 38% времени уходит на создание записей модели City. Так же как и ранее
+убираем поиск сохраненных записей в БД.
+
+| benchmark | Previous | Current | Diff % |
+|--------------------------------|----------|----------|-----------------|
+| total | 626ms | 357ms | -43% |
+
+## Предварительные итоги
+
+Проверка иморта файла large.json показала что время импорта составляет около
+12 секунд, что уже существенно ниже исходных требований.
+На файле small.json stackprof показывает что около 50% времени уходит на
+создание записей модели автобуса, однако на файлах medium.json и large.json
+на данное действие уходит намного меньше времени в процентном отношении (24%
+и 3,5% соответственно).
+
+На больших файла на первый план выходит уже работа массового импорта записей.
+Причём много времени уходит на валидации записей.
+Можно было бы отключить их, но строго говоря это действие является
+функциональным изменением, так как оригинальный скрипт просто бы пропустил
+навалидные записи.
+
+Дальнейший анализ будет производиться на файле medium.json, но уже после
+выполнения второй части задания. Вероятно решение второй части зададания
+несколько замедлит работу импорта, так как наверняка в рамках изменений
+потребуется добавить индексы, что должно несколько замедлить добавление записей.
+
+# Ускорение отображения расписаний
+
+## Основные инструменты для исследования
+
+* rails log
+* rack-mini-profiler
+
+## Первичный анализ
+
+Страница расписания рейсов из Самары в Москву генерируется около 2 секунд
+в dev-окружении и около 1,2 секунды в production окружении.
+При этом время на работу с БД относительно небольшое:
+Production: Views: 934.6ms | ActiveRecord: 356.4ms
+Development: Views: 1714.1ms | ActiveRecord: 242.4ms.
+Вызывает вопросы почему время на ActiveRecord в Development-окружении
+отображается как меньшее, пока решил отложить этот вопрос.
+Очевидно тут стоило бы заняться в первую очередь вопросом генерации вьюх,
+но так как тема задания касается оптимизации БД, то пока проигнорируем это.
+
+## Анализ с использованием rack-mini-profiler
+
+Для удобства решил выполнять основную работу по оптимизации в development.
+Подключение rack-mini-profiler сразу же ухудшило показатели
+Views: 5273.9ms | ActiveRecord: 737.0ms, тем не менее общая картина должна
+быть той же.
+
+## Двойная проблема N+1 (автобусы и сервисы)
+
+Даже без установки bullet в логе видно огромное количество однотипных запросов.
+Добавил preload на обе модели.
+Время генерации страницы сократилось вдвое, при этом на работу ActiveRecord
+теперь уходит всего 22ms (если верить логу rails)
+Views: 2928.7ms | ActiveRecord: 22.0ms.
+Предполагаю что уменьшение времени на вьюхи обусловлено меньшим временем для
+формирования отчёта rack-mini-profiler, но это не точно, так как в production
+время тоже значительно уменьшилось.
+С отключенным rack-mini-profiler:
+Production: Views: 345.8ms | ActiveRecord: 22.2ms
+Development: Views: 984.0ms | ActiveRecord: 17.3ms
+
+## Оптимизация рендеринга
+
+Как бы ни хотелось продолжить, но в дальнейшей оптимизации запросов не вижу
+смысла, можно было бы еще добавить индекс на start_time таблицы trips,
+но это ускорило бы и без того быстрый запрос (на моём ноутбуке этот запрос
+выполняется за 3мс) и в любом случае это не позволит добиться придуманного мной
+целевого показателя.
+
+Поэтому единственным способом достижения цели является оптимизация реднеринга.
+Используя механизм render collection удалось добиться формирования страницы
+в production окружении в пределах 150-200 мс без использования кэширования,
+что соответствует целевым показателям.
+
+# Оптимизация импорта: империя наносит ответный удар
+
+Снова придумываю пользовательскую историю.
+Отдел продаж начал продавать функционалость позволяющую провести миграцию
+с системы конкурентов.
+
+Объем данных в исходной системе может быть произвольным.
+Формат данных для импорта тот же и не может быть изменён.
+
+## Что сделать
+
+Нужно оптимизировать механизм загрузки данных из файла таким образом, чтобы
+потребление памяти не зависило от объёма данных.
+
+## Общий план действий
+
+Необходимо реализизовать потоковое чтение и импорт данных.
+Основными проблемами является то, что данные нужно импортировать в несколько
+таблиц, нет возможности продолжительное время "накапливать" данные для
+последующего импорта.
+До начала работ по оптимизации желательно уточнить фунциональные требования:
+* необходимось сохранения вызова валидаций при создании объектов;
+* необходимость выполнения всех действий в рамках единой транзакции.
+
+Для интереса я буду исходить из необходимости сохранения старого поведения,
+поэтому вариант с импортом данных напрямую в PG из Readme.md будет
+нецелесообразен, так как всё равно портребует создания объектов для запуска
+валидаций.
+
+Текущая идея состоит в том, чтобы при потоковом чтении из исходного файла
+формировать буфер записей определённого размера с последующим импортом
+с помощью activerecord-import.
+
+## Потоковое чтение и импорт блоками
+
+Для реализации потокового чтения была использована библиотека oj.
+Благодаря использованию метода `Oj.sc_parse` и написанию простого
+вспомогательного класса удалось сохранить код практически в неизменном виде.
+Кроме того была использована встроенная библиотека zlib для возможности
+поточного чтения из gz-файла, тем самым избавляя от необходимости распаковки
+файлов большого размера.
+
+Оверхед добавленный механизмом потокового чтения оказался незначительным.
+
+| benchmark | Previous | Current | Diff % |
+|--------------------------------|-----------|---------|-----------------|
+| total | 1498ms | 1531ms | 2% |
+
+## Оптимизация activerecord-import
+
+Инструменты показывают что большую часть времени скрипт производит импорт данных
+с помощью библиотеки activerecord-import.
+К сожалению возможности настройки параметров иморта доволько ограничены.
+Большая часть времени уходит на валидации, но, как было сказано ранее, от них я
+отказываться не плананирую.
+Можно было бы на время импорта отключить часть валидаций в тех случаях когда мы
+заранее уверены в корректности данных, но этим мне уже заниматься довольно
+лениво, к тому же это действие вряд ли бы дало больше 5 процентов уменшения
+времени работы скрипта (валидации занимаю 17% от полного времени работы).
+
+## Создание AR-объектов
+
+На создание объектов уходит 20% времени. Существенного улучшения результатов
+можно добиться только если произвести полный отказ от ActiveRecord,
+а следовательно и от встроенных валидаций.
+По примерной оценке такая работа могда бы уменьшить время работы импорта
+на 60-70%, но данная работа сделала бы скрипт менее поддерживаемым из-за
+расхождения валидаций в скрипте импорта и реальных моделей. Вынесение же
+валидаций в отдельный модуль не выглядит целесообразным.
+
+## Возвращаемся к импорту автобусов
+
+На создание автобусов уходит 26% времени. На текущий момент автобусы создаются
+сразу же по одной записи. Можно было бы произвести массовый импорт автобусов
+по аналогии с другими моделями, но это может создать проблемы в будущем.
+Причина в том, что связь с автобусами явно указывается в модели Trip.
+А следовательно записи модели Bus должны быть добавлены до записей модели Trip.
+Как известно, на 1000 записей Trip приходится около 1 записи модели Bus,
+следовательно для того чтобы массово сохранить хотя бы 10 записей модели Bus
+на нужно было бы накопить 10000 записей модели Trip.
+
+От данного ограничения можно было бы избавиться если вручную выставить
+идентификаторы модели Bus до фактического сохранения, но и тут есть проблема,
+так как если в будущем кто-то настроит внешние ключи на колонки в БД,
+то скрипт внезапно перестанет работать и придётся настраивать отключение
+проверки внешних ключей.
+
+Таким образом, данную оптимизацию так же считаю нецелесообразной.
+
+## Результаты
+
+* large.json: 12 секунд
+* 1M.json.gz: 2,5 минуты
+* 10M.json.gz: предположительно 25-30 минут.
diff --git a/fixtures/example.json b/fixtures/example.json
index 510b4c2d..2ce50934 100644
--- a/fixtures/example.json
+++ b/fixtures/example.json
@@ -46,10 +46,10 @@
},
{
"bus": {
- "model": "Икарус",
- "number": "123",
+ "model":"ГАЗ",
+ "number":"584",
"services": [
- "Туалет",
+ "Кондиционер общий",
"WiFi"
]
},
diff --git a/lib/my_app/import.rb b/lib/my_app/import.rb
new file mode 100644
index 00000000..7f423ef5
--- /dev/null
+++ b/lib/my_app/import.rb
@@ -0,0 +1,147 @@
+module MyApp
+ class Import
+ attr_reader :file_name, :batch_size
+ BusesService = Class.new(ActiveRecord::Base)
+
+ class TripsJsonStreamHandler < ::Oj::ScHandler
+ def initialize(&block)
+ @root = true
+ @block = block
+ end
+
+ def hash_start
+ {}
+ end
+
+ def hash_set(h, k, v)
+ h[k] = v
+ end
+
+ def array_start
+ if @root
+ @root = false
+ return
+ else
+ []
+ end
+ end
+
+ def array_append(a, v)
+ if a
+ a << v
+ else
+ @block.call(v)
+ end
+ end
+ end
+
+ def initialize(file_name, batch_size: 1000)
+ @file_name = file_name
+ @batch_size = batch_size
+ @imported_buses = {}
+ @imported_services = {}
+ @imported_cities = {}
+ @imported_trips = []
+ @buses_services = []
+ end
+
+ def call
+ ActiveRecord::Base.transaction do
+ db_clear
+ import
+ end
+ end
+
+ private
+
+ def import
+ parse do |trip|
+ from = find_or_create_city(trip['from'])
+ to = find_or_create_city(trip['to'])
+ service_names = trip['bus']['services']
+ services = find_or_create_services(service_names)
+ bus_number = trip['bus']['number']
+ bus_model = trip['bus']['model']
+ bus = find_or_create_bus(bus_number, bus_model, services)
+
+ start_time = trip['start_time']
+ duration = trip['duration_minutes']
+ price = trip['price_cents']
+ add_trip(bus, duration, from, price, start_time, to)
+ end
+ BusesService.import(@buses_services)
+ Trip.import(@imported_trips)
+ end
+
+ def db_clear
+ City.delete_all
+ Bus.delete_all
+ Service.delete_all
+ Trip.delete_all
+ ActiveRecord::Base.connection.execute('delete from buses_services;')
+ end
+
+ def parse(&block)
+ io =
+ if File.extname(file_name).downcase == '.gz'
+ Zlib::GzipReader.open(file_name)
+ else
+ File.open(file_name, 'r')
+ end
+ Oj.sc_parse(TripsJsonStreamHandler.new(&block), io)
+ ensure
+ io.close
+ end
+
+ def find_or_create_city(city_name)
+ @imported_cities[city_name] ||= City.create(name: city_name)
+ end
+
+ def find_or_create_services(service_names)
+ service_names.map do |service|
+ find_or_create_service(service)
+ end
+ end
+
+ def find_or_create_service(service_name)
+ @imported_services[service_name] ||= Service.create(name: service_name)
+ end
+
+ def find_or_create_bus(bus_number, model, services)
+ bus = @imported_buses[bus_number]
+ unless bus
+ bus = Bus.create!(number: bus_number, model: model)
+ services.each do |service|
+ add_bus_service(bus, service)
+ end
+ @imported_buses[bus_number] = bus
+ end
+ bus
+ end
+
+ def add_bus_service(bus, service)
+ @buses_services << { bus_id: bus.id, service_id: service.id }
+ if @buses_services.size == batch_size
+ BusesService.import(@buses_services)
+ @buses_services = []
+ end
+ end
+
+ def add_trip(bus, duration, from, price, start_time, to)
+ @imported_trips <<
+ Trip.new(
+ from: from,
+ to: to,
+ bus: bus,
+ start_time: start_time,
+ duration_minutes: duration,
+ price_cents: price,
+ )
+
+ if @imported_trips.size == batch_size
+ Trip.import(@imported_trips)
+ @imported_trips = []
+ end
+ end
+ end
+end
diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake
index 540fe871..2b63a9bc 100644
--- a/lib/tasks/utils.rake
+++ b/lib/tasks/utils.rake
@@ -1,34 +1,7 @@
# Наивная загрузка данных из json-файла в БД
# rake reload_json[fixtures/small.json]
task :reload_json, [:file_name] => :environment do |_task, args|
- json = JSON.parse(File.read(args.file_name))
+ require Rails.root.join('lib/my_app/import')
- 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
+ MyApp::Import.new(args.file_name, batch_size: ENV.fetch('BATCH_SIZE', 1000).to_i).call
end
diff --git a/loop.sh b/loop.sh
new file mode 100755
index 00000000..a8a02ee9
--- /dev/null
+++ b/loop.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+
+bundle exec rake test
+
+export RAILS_ENV=production
+bundle exec ruby bin/rails runner perfolab/my_app/import_stand.rb
\ No newline at end of file
diff --git a/perfolab/my_app/import_stand.rb b/perfolab/my_app/import_stand.rb
new file mode 100644
index 00000000..91ae06f6
--- /dev/null
+++ b/perfolab/my_app/import_stand.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'my_app/import'
+require 'perfolab'
+
+loop =
+ PerfoLab::Loop.new do |toolbox|
+ toolbox.add_tool(
+ :stackprof,
+ type: :stackprof,
+ config: {
+ raw: true
+ },
+ runner_options: {
+ arguments: ['medium']
+ }
+ )
+ toolbox.add_tool(
+ :benchmark,
+ type: :benchmark,
+ runner_options: {
+ gc_disable: false,
+ warmup: 1,
+ arguments: ['medium']
+ }
+ )
+ end
+
+loop.analyze do |filename|
+ MyApp::Import.new(Rails.root.join('fixtures', "#{filename}.json")).call
+end
diff --git a/test/lib/my_app/import_test.rb b/test/lib/my_app/import_test.rb
new file mode 100644
index 00000000..565b05b8
--- /dev/null
+++ b/test/lib/my_app/import_test.rb
@@ -0,0 +1,29 @@
+require 'test_helper'
+require 'my_app/import'
+
+class TestMe < Minitest::Test
+ def test_result
+ MyApp::Import.new(Rails.root.join("fixtures", 'example.json')).call
+ assert_equal 10, Trip.count
+ assert_equal 2, Bus.count
+ assert_equal 3, Service.count
+ assert_equal 2, City.count
+ gaz = Bus.find_by!(number: '584', model: 'ГАЗ')
+ icarus = Bus.find_by!(number: '123', model: 'Икарус')
+ assert_equal ['Кондиционер общий', 'WiFi'], gaz.services.map(&:name)
+ assert_equal ['Туалет', 'WiFi'], icarus.services.map(&:name)
+ assert_equal 1, gaz.trips.size
+ gaz_trip = gaz.trips.first
+ gaz_trip_attributes = gaz_trip.attributes
+ assert_equal(
+ {
+ 'duration_minutes' => 315,
+ 'price_cents' => 969,
+ 'start_time' => '18:30',
+ },
+ gaz_trip_attributes.slice('duration_minutes', 'price_cents', 'start_time')
+ )
+ assert_equal 'Самара', gaz_trip.from.name
+ assert_equal 'Москва', gaz_trip.to.name
+ end
+end
\ No newline at end of file