diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..5ec60dd7 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +DB_USERNAME=postgres +DB_PASSWORD=postgres +DB_HOST=db +DB_PORT=5432 +DB_POOL=5 +DB_NAME=optimization_3 + +PGHERO_USERNAME=postgres +PGHERO_PASSWORD=postgres + +RAILS_ENV=development diff --git a/.gitignore b/.gitignore index 59c74047..40188e3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ /.bundle +/.idea /tmp /log /public +.env +fixtures/1M.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..a7ac5b35 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM ruby:2.6.3-alpine + +RUN apk update && apk upgrade && apk add --update --no-cache \ + build-base libc-dev tzdata bash htop shared-mime-info \ + postgresql-dev postgresql-client + +WORKDIR /opt/app + +COPY Gemfile* ./ + +RUN gem install bundler -v 2.0.2 +RUN bundle install + +COPY . . diff --git a/Gemfile b/Gemfile index e20b1260..eacc035e 100644 --- a/Gemfile +++ b/Gemfile @@ -3,10 +3,21 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.6.3' +gem 'activerecord-import' +gem 'dotenv-rails' 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 'memory_profiler' +gem 'rspec' +gem 'rspec-benchmark' +gem 'ruby-prof' +gem 'stackprof' +gem 'pghero', '>= 2' +gem 'rack-mini-profiler', require: false +gem 'bullet' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index fccf6f5f..074747d9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -43,13 +45,24 @@ GEM minitest (~> 5.1) tzinfo (~> 1.1) arel (9.0.0) + 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) 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) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) + railties (>= 3.2) erubi (1.8.0) ffi (1.10.0) globalid (0.4.2) @@ -67,8 +80,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) @@ -77,8 +93,12 @@ GEM nokogiri (1.10.2) mini_portile2 (~> 2.4.0) pg (1.1.4) + pghero (2.8.3) + activerecord (>= 5) puma (3.12.1) rack (2.0.6) + rack-mini-profiler (3.1.0) + rack (>= 1.2.0) rack-test (1.1.0) rack (>= 1.0, < 3) rails (5.2.3) @@ -109,6 +129,25 @@ 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.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-support (3.12.0) + ruby-prof (1.4.3) ruby_dep (1.5.0) sprockets (3.7.2) concurrent-ruby (~> 1.0) @@ -117,10 +156,12 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + stackprof (0.2.24) 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) @@ -134,12 +175,23 @@ PLATFORMS ruby DEPENDENCIES + activerecord-import bootsnap (>= 1.1.0) + bullet byebug + dotenv-rails listen (>= 3.0.5, < 3.2) + memory_profiler + mimemagic (= 0.3.10) pg (>= 0.18, < 2.0) + pghero (>= 2) puma (~> 3.11) + rack-mini-profiler rails (~> 5.2.3) + rspec + rspec-benchmark + ruby-prof + stackprof tzinfo-data web-console (>= 3.3.0) diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb index acb38be2..fd45b1a0 100644 --- a/app/controllers/trips_controller.rb +++ b/app/controllers/trips_controller.rb @@ -1,7 +1,29 @@ 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) + @from = params[:from] + @to = params[:to] + @trips_count = trips_query.count + @trips = trips_query.order(:start_time) + .joins(bus: :services) + .select(trips_select.join(',')) + .group('trips.id, buses.id') + end + + private + + def trips_query + @cities ||= City.where(name: [@from, @to]).pluck(:name, :id).to_h + Trip.where(from_id: @cities[@from], to_id: @cities[@to]) + end + + def trips_select + [ + 'trips.start_time', + 'trips.duration_minutes', + 'trips.price_cents', + 'array_agg(services.name) as service_names', + 'buses.number as bus_number', + 'buses.model as bus_model' + ] end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 10a4cba8..767a072b 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end diff --git a/app/models/bus.rb b/app/models/bus.rb index 1dcc54cb..0e5b3cc5 100644 --- a/app/models/bus.rb +++ b/app/models/bus.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Bus < ApplicationRecord MODELS = [ 'Икарус', diff --git a/app/models/city.rb b/app/models/city.rb index 19ec7f36..dc8306cc 100644 --- a/app/models/city.rb +++ b/app/models/city.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class City < ApplicationRecord validates :name, presence: true, uniqueness: true validate :name_has_no_spaces diff --git a/app/models/service.rb b/app/models/service.rb index 9cbb2a32..23c6aca2 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Service < ApplicationRecord SERVICES = [ 'WiFi', diff --git a/app/models/trip.rb b/app/models/trip.rb index 9d63dfff..347446b0 100644 --- a/app/models/trip.rb +++ b/app/models/trip.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true class Trip < ApplicationRecord - HHMM_REGEXP = /([0-1][0-9]|[2][0-3]):[0-5][0-9]/ + HHMM_REGEXP = /([0-1][0-9]|[2][0-3]):[0-5][0-9]/.freeze belongs_to :from, class_name: 'City' belongs_to :to, class_name: 'City' diff --git a/app/views/trips/_delimiter.html.erb b/app/views/trips/_delimiter.html.erb deleted file mode 100644 index 3f845ad0..00000000 --- a/app/views/trips/_delimiter.html.erb +++ /dev/null @@ -1 +0,0 @@ -==================================================== diff --git a/app/views/trips/_service.html.erb b/app/views/trips/_service.html.erb deleted file mode 100644 index 178ea8c0..00000000 --- a/app/views/trips/_service.html.erb +++ /dev/null @@ -1 +0,0 @@ -<li><%= "#{service.name}" %></li> 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 @@ -<li>Сервисы в автобусе:</li> -<ul> - <% services.each do |service| %> - <%= render "service", service: service %> - <% end %> -</ul> diff --git a/app/views/trips/_trip.html.erb b/app/views/trips/_trip.html.erb deleted file mode 100644 index fa1de9aa..00000000 --- a/app/views/trips/_trip.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<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> diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb index a60bce41..2c7b0d6c 100644 --- a/app/views/trips/index.html.erb +++ b/app/views/trips/index.html.erb @@ -1,16 +1,26 @@ <h1> - <%= "Автобусы #{@from.name} – #{@to.name}" %> + <%= "Автобусы #{@from} – #{@to}" %> </h1> <h2> - <%= "В расписании #{@trips.count} рейсов" %> + <%= "В расписании #{@trips_count} рейсов" %> </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.service_names.present? %> + <li>Сервисы в автобусе:</li> + <ul> + <% trip.service_names.each do |service| %> + <li><%= "#{service}" %></li> + <% end %> + </ul> <% end %> </ul> - <%= render "delimiter" %> + ==================================================== <% end %> diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..01366282 --- /dev/null +++ b/case-study.md @@ -0,0 +1,93 @@ +Первым шагом при выполнении задания была настройка запуска в докер и проверка корректности работы. +После первого запуска скрипта на экспорт данных было заметно что он работает не очень быстро. + +### 1 часть задания. Оптимизация скрипта + +Скрипт решила оптимизировать первоначально по памяти, т.к. имеет место обработка файла + active record наверняка может создавать лишние объекты +ну и хотелось в этом больше потренироваться. +Как и ожидалось после запуска memoryprofiler больше всего памяти приходится на activerecord, поэтому так же как и во втором +задании поэтапно начинаю оптимизацию. + +#### Замеры до оптимизации: +- Время выполнения (fixtures/small.json): 6.97 сек +- Потребляемая память (fixtures/small.json): 119 MB + +Далее перечислю основные моменты: +1. Первое что решаю сделать это добавить frozen_string_literal, что дает небольшое улучшение. +2. Далее, исходя из отчета самое большое количество памяти выделено на /activerecord-5.2.3/lib/active_record/log_subscriber.rb +Заменила log_level на error для скрипта. Здесь же убрала bootsnap, т.к. периодически падал профилировщик. Точка роста сместилась +3. Т.к далее основная нагрузка приходится на active record, решаю по максимуму убрать использование объектов active record. +Для этого выношу код в отдельный класс, заменяю запросы удаления на truncate, подключаю гем activerecord-import и с помощью него уже строю импорт данных +В результате время выполнения сократилась, но потребление памяти не очень. На первом месте остались объекты active record +4. Далее решила в импорте не использовать recursive, а собрать все в массив и импортировать его и это уже принесло хороший результат: +- Время выполнения (fixtures/small.json): 0.81 сек +- Потребляемая память (fixtures/small.json): 113 MB + + +- Время выполнения (fixtures/medium.json): 4.07 сек +- Потребляемая память (fixtures/small.json): 112 MB + + +- Время выполнения (fixtures/large.json): 34.86 сек +- Потребляемая память (fixtures/small.json): 335 MB + +В результате оптимизации active record перестал быть главной точкой роста. + +5. После профилирования по времени на первом месте оказался PG::Connection#async_exec, судя по записанным данным самое большое количество запросов +приходится на добавление связи bus и service. Ради интереса решила попробовать сделать их запись в одном запросе, что +привело к снижению времени обработки. После этого решила попробовать по аналогии записывать и данные по trip и в итоге получила +значительное снижение по времени, т.о от гема activerecord-import отказалась. Результаты: + +- Время выполнения (fixtures/small.json): 0.39 сек +- Потребляемая память (fixtures/small.json): 110 MB + + +- Время выполнения (fixtures/medium.json): 1.5 сек +- Потребляемая память (fixtures/medium.json): 116 MB + + +- Время выполнения (fixtures/large.json): 13.28 сек +- Потребляемая память (fixtures/large.json): 330 MB + + +- Время выполнения (fixtures/1M.json): 137.6 сек +- Потребляемая память (fixtures/1M.json): 1895 MB + +По памяти много, но по времени кажется неплохо. На этом результате решила остановиться пока, потому что времени катастрофически не хватает. +Потоковая обработка интересна, если получится как нибудь попробую предложенный пример записи в БД. + +### 2 часть задания. Оптимизация страницы расписаний + +После импорта файла large.json страница загружалась примерно 24 секунды. +Первым делом решила попробовать поставить pgHero, т.к. было интересно попробовать этот инструмент. + +Сразу после первого прогона был найден медленный запрос: +```sql +SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1 +``` + +Поставила bullet и rack-mini-profiler. +- Далее поправила запросы исходя из алертов и добавила . +- из профайлера видно что очень много запросов вида: +```sql +SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1; +``` +то же показывает и pgHero, поэтому поправила этот момент тем то изменила выборку по trips - добавила в select только нужные для +отображения аттрибуты и добавила joins. +- объединила рендеры в один для удобства поиска необходимых аттрибутов +- заменила поиск города по названию (влияет не сильно но тем не менее) + +В результате время отображения на сократилось до 182ms. По pgHero и bullet проблем больше не выявлено. + +Дальше я решила посмотреть анализ самого долгого запроса, который стал основным. +По нему получается что самым долгим является часть джойна с buses_services, т.к там нет индексов. Такая же проблема на +таблицах buses и trips. Добавляю индексы. + +В результате время отображения на сократилось до 130ms (с включенным профилировщиком) + +### Итоги + +Очень понравилось использовать pgHero, действительно удобный и визуально приятный инструмент. +С bullet и rack-mini-profiler немного уже работала, поэтому они остаются полезными) + +P.S Прошу прощения за такую задержку с выполнением дз. Постараюсь наверстать все пропущенное до конца курса. diff --git a/config/application.rb b/config/application.rb index 9c331097..ea1a1364 100644 --- a/config/application.rb +++ b/config/application.rb @@ -10,6 +10,7 @@ module Task4 class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.2 + config.log_level = :error # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/config/boot.rb b/config/boot.rb index b9e460ce..b6915e55 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,4 +1,5 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. -require 'bootsnap/setup' # Speed up boot time by caching expensive operations. +# отключила т.к. падал профилировщик +# require 'bootsnap/setup' # Speed up boot time by caching expensive operations. diff --git a/config/database.yml b/config/database.yml index e116cfa6..11508d48 100644 --- a/config/database.yml +++ b/config/database.yml @@ -17,13 +17,15 @@ default: &default adapter: postgresql encoding: unicode - # For details on connection pooling, see Rails configuration guide - # http://guides.rubyonrails.org/configuring.html#database-pooling - pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + username: <%= ENV.fetch('DB_USERNAME') { 'postgres' } %> + password: <%= ENV.fetch('DB_PASSWORD') { 'postgres' } %> + host: <%= ENV.fetch('DB_HOST') { 'db' } %> + port: <%= ENV.fetch('DB_PORT') { 5432 } %> + pool: <%= ENV.fetch('DB_POOL') { 5 } %> development: <<: *default - database: task-4_development + database: <%= ENV.fetch('DB_NAME') %> # The specified database role being used to connect to postgres. # To create additional roles in postgres see `$ createuser --help`. @@ -57,7 +59,7 @@ development: # Do not set this db to the same as development or production. test: <<: *default - database: task-4_test + database: optimization_3_test # As with config/secrets.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is @@ -80,6 +82,4 @@ test: # production: <<: *default - database: task-4_production - username: task-4 - password: <%= ENV['TASK-4_DATABASE_PASSWORD'] %> + database: task-optimization_3_production diff --git a/config/environments/development.rb b/config/environments/development.rb index 1311e3e4..903a8661 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,4 +1,13 @@ Rails.application.configure do + config.after_initialize do + Bullet.enable = true + 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 diff --git a/config/environments/production.rb b/config/environments/production.rb index 613d8289..1b5e478c 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -51,7 +51,7 @@ # Use the lowest log level to ensure availability of diagnostic information # when problems arise. - config.log_level = :debug + config.log_level = :error # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] diff --git a/config/environments/test.rb b/config/environments/test.rb index 0a38fd3c..bc5971ab 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,4 +1,10 @@ Rails.application.configure do + config.after_initialize do + Bullet.enable = true + Bullet.bullet_logger = true + Bullet.raise = true # 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 diff --git a/config/initializers/rack_mini_profiler.rb b/config/initializers/rack_mini_profiler.rb new file mode 100644 index 00000000..14e63a30 --- /dev/null +++ b/config/initializers/rack_mini_profiler.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +if Rails.env.development? + require "rack-mini-profiler" + + # The initializer was required late, so initialize it manually. + Rack::MiniProfilerRails.initialize!(Rails.application) +end diff --git a/config/routes.rb b/config/routes.rb index a2da6a7b..c60fe546 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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" diff --git a/db/migrate/20230419131514_enable_pg_statements.rb b/db/migrate/20230419131514_enable_pg_statements.rb new file mode 100644 index 00000000..d5ec3fa2 --- /dev/null +++ b/db/migrate/20230419131514_enable_pg_statements.rb @@ -0,0 +1,5 @@ +class EnablePgStatements < ActiveRecord::Migration[5.2] + def change + enable_extension 'pg_stat_statements' + end +end \ No newline at end of file diff --git a/db/migrate/20230421134615_create_index_on_bus_services.rb b/db/migrate/20230421134615_create_index_on_bus_services.rb new file mode 100644 index 00000000..5db9ef50 --- /dev/null +++ b/db/migrate/20230421134615_create_index_on_bus_services.rb @@ -0,0 +1,6 @@ +class CreateIndexOnBusServices < ActiveRecord::Migration[5.2] + def change + add_index :buses_services, :bus_id + add_index :buses_services, :service_id + end +end \ No newline at end of file diff --git a/db/migrate/20230421134619_create_index_on_trips.rb b/db/migrate/20230421134619_create_index_on_trips.rb new file mode 100644 index 00000000..585cd9df --- /dev/null +++ b/db/migrate/20230421134619_create_index_on_trips.rb @@ -0,0 +1,7 @@ +class CreateIndexOnTrips < ActiveRecord::Migration[5.2] + def change + add_index :trips, :bus_id + add_index :trips, [:from_id, :to_id] + add_index :trips, :start_time + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index f6921e45..58f9dab1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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: 2023_04_21_134619) 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| @@ -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| @@ -40,6 +43,9 @@ 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" + t.index ["start_time"], name: "index_trips_on_start_time" end end diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..4e7e63f1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3.4' +services: + db: + image: postgres:10 + environment: + POSTGRES_USER: $DB_USERNAME + POSTGRES_PASSWORD: $DB_PASSWORD + ports: + - "5432:5432" + command: + - "postgres" + - "-c" + - "shared_preload_libraries=pg_stat_statements" + - "-c" + - "pg_stat_statements.track=all" + - "-p" + - "5432" + volumes: + - db:/var/lib/postgresql/data + web: + tty: true + stdin_open: true + image: rails-optimization-task3 + build: + context: . + command: sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" + volumes: + - .:/opt/app:cached +# tmpfs: +# - /opt/app/tmp/ +# - /opt/app/log/ + ports: + - "3000:3000" + depends_on: + - db + environment: + RAILS_ENV: $RAILS_ENV +volumes: + db: diff --git a/lib/operations/import.rb b/lib/operations/import.rb new file mode 100644 index 00000000..5939a4e4 --- /dev/null +++ b/lib/operations/import.rb @@ -0,0 +1,102 @@ +module Operations + class Import + def initialize + @services = {} + @cities = {} + @first_trips_line = true + @first_buses_line = true + end + + def truncate_tables! + ActiveRecord::Base.connection.execute('truncate table cities, buses, services, trips, buses_services RESTART IDENTITY;') + end + + def find_city_id(name) + @cities[name] ||= ActiveRecord::Base.connection.execute("insert into cities (name) values ('#{name}') ON CONFLICT DO NOTHING RETURNING id").first['id'] + end + + def import_services! + ActiveRecord::Base.connection.execute("insert into services (name) values ('#{Service::SERVICES.join('\'),(\'')}') RETURNING id, name").each do |result| + @services[result['name']] = result['id'] + end + end + + def insert_bus(model:, number:) + ActiveRecord::Base.connection.execute("insert into buses (model, number) values ('#{model}', '#{number}') ON CONFLICT DO NOTHING RETURNING id").first['id'] + end + + def insert_bus_services(bus_id:, service_names:) + service_names.map! { |n| "(#{bus_id}, #{find_service_id(n)})" } + ActiveRecord::Base.connection.execute("insert into buses_services (bus_id, service_id) values #{service_names.join(',')} ON CONFLICT DO NOTHING") + end + + def buses_services_sql_file + # bus_id, service_id + @buses_services_sql_file ||= File.open('log/buses_services_sql.txt', 'w+') + end + + def trips_sql_file + # start_time, duration_minutes, price_cents, from_id, to_id, bus_id + @trips_sql_file ||= File.open('log/trips_sql.txt', 'w+') + end + + def write_trip(bus_id, raw_data) + if @first_trips_line + trips_sql_file.write 'INSERT INTO trips (start_time, duration_minutes, price_cents, from_id, to_id, bus_id) VALUES ' + @first_trips_line = false + else + trips_sql_file.write ',' + end + + trips_sql_file.write <<SQL +('#{raw_data['start_time']}', +#{raw_data['duration_minutes']}, +#{raw_data['price_cents']}, +#{find_city_id(raw_data['from'])}, +#{find_city_id(raw_data['to'])}, +#{bus_id}) +SQL + end + + def write_bus_service(bus_id, service_name) + if @services[service_name] + if @first_buses_line + buses_services_sql_file.write 'INSERT INTO buses_services (bus_id, service_id) VALUES ' + @first_buses_line = false + else + buses_services_sql_file.write ',' + end + + buses_services_sql_file.write "(#{bus_id},#{@services[service_name]})" + end + end + + def work(file) + json = JSON.parse(File.read(file)) + truncate_tables! + + ActiveRecord::Base.transaction do + # т.к. сервисов ограниченное небольшое количество их можно сразу добавить + import_services! + + json.each do |raw_data| + bus_id = insert_bus(model: raw_data['bus']['model'], number: raw_data['bus']['number']) + + raw_data['bus']['services'].each do |service_name| + write_bus_service bus_id, service_name + end + + write_trip bus_id, raw_data + end + + buses_services_sql_file.rewind + trips_sql_file.rewind + ActiveRecord::Base.connection.execute(buses_services_sql_file.read) + ActiveRecord::Base.connection.execute(trips_sql_file.read) + end + + buses_services_sql_file.close + trips_sql_file.close + end + end +end \ No newline at end of file diff --git a/lib/tasks/profile.rake b/lib/tasks/profile.rake new file mode 100644 index 00000000..0028eff4 --- /dev/null +++ b/lib/tasks/profile.rake @@ -0,0 +1,43 @@ +namespace :profile do + def target_task + Operations::Import.new.work('fixtures/large.json') + end + + task by_memory: :environment do + report = MemoryProfiler.report do + target_task + end + + report.pretty_print(scale_bytes: true) + end + + task by_wall_time: :environment do + GC.disable + RubyProf.measure_mode = RubyProf::WALL_TIME + + result = RubyProf.profile { target_task } + + printer = RubyProf::FlatPrinter.new(result) + printer.print(File.open('log/ruby_prof_reports/flat.txt', 'w+')) + + printer = RubyProf::GraphHtmlPrinter.new(result) + printer.print(File.open('log/ruby_prof_reports/graph.html', 'w+')) + + # printer = RubyProf::CallStackPrinter.new(result) + # printer.print(File.open('log/ruby_prof_reports/callstack.html', 'w+')) + + GC.enable + end + + task benchmark: :environment do + time = Benchmark.realtime { target_task } + puts time.round(2) + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + + puts Bus.count + puts City.count + puts Service.count + puts Trip.count + puts Service.last.buses.count + end +end \ No newline at end of file diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index 540fe871..d2418833 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -1,34 +1,7 @@ +# frozen_string_literal: true +require 'operations/import' # Наивная загрузка данных из 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 + Operations::Import.new.work(args.file_name) end