diff --git a/.gitignore b/.gitignore index 59c74047..6c3edac2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ /tmp /log /public + +.byebug_history + +reports/tmp/* \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/Gemfile b/Gemfile index e20b1260..1e448ca6 100644 --- a/Gemfile +++ b/Gemfile @@ -1,14 +1,24 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '2.6.3' +ruby '3.2.2' -gem 'rails', '~> 5.2.3' +gem 'rails', '~> 6.0' gem 'pg', '>= 0.18', '< 2.0' gem 'puma', '~> 3.11' gem 'bootsnap', '>= 1.1.0', require: false +gem 'activerecord-import' +gem 'active_interaction' +gem 'fast_jsonparser', require: false + +# Profiling +gem 'pghero' +gem 'rack-mini-profiler' +gem 'memory_profiler' +gem 'stackprof' group :development, :test do + gem 'bullet' # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end @@ -20,6 +30,10 @@ group :development do end group :test do + gem 'rspec' + gem 'rspec-rails' + gem 'rspec-benchmark' + gem 'rspec-sqlimit' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem diff --git a/Gemfile.lock b/Gemfile.lock index fccf6f5f..baae26ac 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,150 +1,240 @@ GEM remote: https://rubygems.org/ specs: - actioncable (5.2.3) - actionpack (= 5.2.3) + actioncable (6.1.7.3) + actionpack (= 6.1.7.3) + activesupport (= 6.1.7.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) + actionmailbox (6.1.7.3) + actionpack (= 6.1.7.3) + activejob (= 6.1.7.3) + activerecord (= 6.1.7.3) + activestorage (= 6.1.7.3) + activesupport (= 6.1.7.3) + mail (>= 2.7.1) + actionmailer (6.1.7.3) + actionpack (= 6.1.7.3) + actionview (= 6.1.7.3) + activejob (= 6.1.7.3) + activesupport (= 6.1.7.3) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.3) - actionview (= 5.2.3) - activesupport (= 5.2.3) - rack (~> 2.0) + actionpack (6.1.7.3) + actionview (= 6.1.7.3) + activesupport (= 6.1.7.3) + rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.3) - activesupport (= 5.2.3) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.1.7.3) + actionpack (= 6.1.7.3) + activerecord (= 6.1.7.3) + activestorage (= 6.1.7.3) + activesupport (= 6.1.7.3) + nokogiri (>= 1.8.5) + actionview (6.1.7.3) + activesupport (= 6.1.7.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.3) - activesupport (= 5.2.3) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + active_interaction (5.2.0) + activemodel (>= 5.2, < 8) + activesupport (>= 5.2, < 8) + activejob (6.1.7.3) + activesupport (= 6.1.7.3) globalid (>= 0.3.6) - activemodel (5.2.3) - activesupport (= 5.2.3) - activerecord (5.2.3) - activemodel (= 5.2.3) - activesupport (= 5.2.3) - arel (>= 9.0) - activestorage (5.2.3) - actionpack (= 5.2.3) - activerecord (= 5.2.3) - marcel (~> 0.3.1) - activesupport (5.2.3) + activemodel (6.1.7.3) + activesupport (= 6.1.7.3) + activerecord (6.1.7.3) + activemodel (= 6.1.7.3) + activesupport (= 6.1.7.3) + activerecord-import (1.4.1) + activerecord (>= 4.2) + activestorage (6.1.7.3) + actionpack (= 6.1.7.3) + activejob (= 6.1.7.3) + activerecord (= 6.1.7.3) + activesupport (= 6.1.7.3) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (6.1.7.3) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - arel (9.0.0) - bindex (0.6.0) - bootsnap (1.4.2) - msgpack (~> 1.0) - builder (3.2.3) - byebug (11.0.1) - concurrent-ruby (1.1.5) - crass (1.0.4) - erubi (1.8.0) - ffi (1.10.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.6.0) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + benchmark-malloc (0.2.0) + benchmark-perf (0.6.0) + benchmark-trend (0.4.0) + bindex (0.8.1) + bootsnap (1.16.0) + msgpack (~> 1.2) + builder (3.2.4) + bullet (7.0.7) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11) + byebug (11.1.3) + concurrent-ruby (1.2.2) + crass (1.0.6) + date (3.3.3) + diff-lcs (1.5.0) + erubi (1.12.0) + fast_jsonparser (0.6.0) + ffi (1.15.5) + globalid (1.1.0) + activesupport (>= 5.0) + i18n (1.13.0) concurrent-ruby (~> 1.0) - listen (3.1.5) + listen (3.0.8) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) - loofah (2.2.3) + loofah (2.20.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.1) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (0.9.2) - mimemagic (0.3.3) - mini_mime (1.0.1) - mini_portile2 (2.4.0) - minitest (5.11.3) - msgpack (1.2.9) - nio4r (2.3.1) - nokogiri (1.10.2) - mini_portile2 (~> 2.4.0) - pg (1.1.4) - puma (3.12.1) - rack (2.0.6) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (5.2.3) - actioncable (= 5.2.3) - actionmailer (= 5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - activemodel (= 5.2.3) - activerecord (= 5.2.3) - activestorage (= 5.2.3) - activesupport (= 5.2.3) - bundler (>= 1.3.0) - railties (= 5.2.3) + net-imap + net-pop + net-smtp + marcel (1.0.2) + memory_profiler (1.0.1) + method_source (1.0.0) + mini_mime (1.1.2) + minitest (5.18.0) + msgpack (1.7.0) + net-imap (0.3.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.3.3) + net-protocol + nio4r (2.5.9) + nokogiri (1.14.3-x86_64-darwin) + racc (~> 1.4) + pg (1.5.2) + pghero (3.3.3) + activerecord (>= 6) + puma (3.12.6) + racc (1.6.2) + rack (2.2.7) + rack-mini-profiler (3.1.0) + rack (>= 1.2.0) + rack-test (2.1.0) + rack (>= 1.3) + rails (6.1.7.3) + actioncable (= 6.1.7.3) + actionmailbox (= 6.1.7.3) + actionmailer (= 6.1.7.3) + actionpack (= 6.1.7.3) + actiontext (= 6.1.7.3) + actionview (= 6.1.7.3) + activejob (= 6.1.7.3) + activemodel (= 6.1.7.3) + activerecord (= 6.1.7.3) + activestorage (= 6.1.7.3) + activesupport (= 6.1.7.3) + bundler (>= 1.15.0) + railties (= 6.1.7.3) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.0.4) - loofah (~> 2.2, >= 2.2.2) - railties (5.2.3) - actionpack (= 5.2.3) - activesupport (= 5.2.3) + rails-html-sanitizer (1.5.0) + loofah (~> 2.19, >= 2.19.1) + railties (6.1.7.3) + actionpack (= 6.1.7.3) + activesupport (= 6.1.7.3) method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - rake (12.3.2) - rb-fsevent (0.10.3) - rb-inotify (0.10.0) + rake (>= 12.2) + thor (~> 1.0) + rake (13.0.6) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) ffi (~> 1.0) - ruby_dep (1.5.0) - sprockets (3.7.2) + 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.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) + 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-rails (6.0.1) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.11) + rspec-expectations (~> 3.11) + rspec-mocks (~> 3.11) + rspec-support (~> 3.11) + rspec-sqlimit (0.0.5) + activerecord (> 4.2, < 7.1) + rspec (~> 3.0) + rspec-support (3.12.0) + sprockets (4.2.0) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) sprockets (>= 3.0.0) - thor (0.20.3) - thread_safe (0.3.6) - tzinfo (1.2.5) - thread_safe (~> 0.1) - web-console (3.7.0) - actionview (>= 5.0) - activemodel (>= 5.0) + stackprof (0.2.25) + thor (1.2.1) + timeout (0.3.2) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uniform_notifier (1.16.0) + web-console (4.2.0) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) - websocket-driver (0.7.0) + railties (>= 6.0.0) + websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) + websocket-extensions (0.1.5) + zeitwerk (2.6.7) PLATFORMS - ruby + x86_64-darwin-22 DEPENDENCIES + active_interaction + activerecord-import bootsnap (>= 1.1.0) + bullet byebug + fast_jsonparser listen (>= 3.0.5, < 3.2) + memory_profiler pg (>= 0.18, < 2.0) + pghero puma (~> 3.11) - rails (~> 5.2.3) + rack-mini-profiler + rails (~> 6.0) + rspec + rspec-benchmark + rspec-rails + rspec-sqlimit + stackprof tzinfo-data web-console (>= 3.3.0) RUBY VERSION - ruby 2.6.3p62 + ruby 3.2.2p53 BUNDLED WITH - 2.0.2 + 2.4.12 diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..f5090aba --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +reload_example: + bundle exec rake reload_json[fixtures/example.json] +reload_large: + bundle exec rake reload_json[fixtures/large.json] +reload_medium: + bundle exec rake reload_json[fixtures/medium.json] +reload_small: + bundle exec rake reload_json[fixtures/small.json] + +open: + open http://localhost:3000/автобусы/Самара/Москва +pghero: + open http://localhost:3000/pghero + +profile_memory: + bundle exec rake reload_json[fixtures/example.json,memory] + +mv_reports: + mv reports/tmp reports/${step} \ No newline at end of file diff --git a/Readme.md b/Readme.md index 20b4eda3..29f877de 100644 --- a/Readme.md +++ b/Readme.md @@ -3,14 +3,15 @@ В этом задании вам предлагается оптимизировать учебное `rails`-приложение. Для запуска потребуется: -- `ruby 2.6.3` -- `postgres` +- `ruby 3.2.0` +- `docker` Запуск и использование: - `bundle install` - `bin/setup` +- `docker-compose up` - `rails s` -- `open http://localhost:3000/автобусы/Самара/Москва` +- `make open` ## Описание учебного приложения Зайдя на страницу `автобусы/Самара/Москва` вы увидите расписание автобусов по этому направлению. @@ -43,10 +44,11 @@ Нужно найти и устранить проблемы, замедляющие формирование этих страниц. Попробуйте воспользоваться -- [ ] `rack-mini-profiler` +- [x] `rack-mini-profiler` - [ ] `rails panel` -- [ ] `bullet` +- [x] `bullet` - [ ] `explain` запросов +- [x] `rspec-sqlimit` ### Сдача задания `PR` в этот репозиторий с кодом и case-study наподобие первых двух недель. На этот раз шаблона нет, законспектируйте ваш процесс оптимизации в свободной форме. diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb index acb38be2..ae822ef7 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.preload(bus: :services).where(from: @from, to: @to).order(:start_time).load end end diff --git a/app/interactions/import_data.rb b/app/interactions/import_data.rb new file mode 100644 index 00000000..e02b6447 --- /dev/null +++ b/app/interactions/import_data.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true +require 'fast_jsonparser' + +class ImportData < ActiveInteraction::Base + string :file_name + + attr_reader :trips, :services, :buses, :cities + + def execute + start_time = Time.now + + ActiveRecord::Base.transaction do + drop_data + import_data + end + + puts "\nFinished! Time: #{Time.now - start_time}" + print_memory_usage + end + + private + + def drop_data + City.delete_all + Bus.delete_all + Service.delete_all + Trip.delete_all + BusesService.delete_all + end + + def import_data + @trips = [] + @buses = {} + @services = {} + @cities = {} + + json = FastJsonparser.parse(File.read(file_name), symbolize_keys: false) + + json.each do |trip| + trips << trip_new(trip) + print '.' + end + + Service.import services.values + Bus.import buses.values, recursive: true + City.import cities.values + Trip.import trips + end + + def trip_new(trip) + Trip.new( + from: fetch_city(trip['from']), + to: fetch_city(trip['to']), + bus: fetch_bus(trip), + start_time: trip['start_time'], + duration_minutes: trip['duration_minutes'], + price_cents: trip['price_cents'], + ) + end + + def fetch_city(name) + cities[name] ||= City.new(name: name) + end + + def fetch_bus(trip) + bus = buses[trip['bus']['number']] + return bus if bus + + buses[trip['bus']['number']] = + Bus.new( + number: trip['bus']['number'], + model: trip['bus']['model'], + services: fetch_bus_services(trip) + ) + end + + def fetch_bus_services(trip) + trip['bus']['services'].map do |service| + services[service] ||= Service.new(name: service) + end + end + + def print_memory_usage + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + end +end \ No newline at end of file diff --git a/app/models/bus.rb b/app/models/bus.rb index 1dcc54cb..5cdab4c7 100644 --- a/app/models/bus.rb +++ b/app/models/bus.rb @@ -13,7 +13,9 @@ class Bus < ApplicationRecord ].freeze has_many :trips - has_and_belongs_to_many :services, join_table: :buses_services + + has_many :buses_services + has_many :services, through: :buses_services validates :number, presence: true, uniqueness: true validates :model, inclusion: { in: MODELS } diff --git a/app/models/buses_service.rb b/app/models/buses_service.rb new file mode 100644 index 00000000..6219d44e --- /dev/null +++ b/app/models/buses_service.rb @@ -0,0 +1,4 @@ +class BusesService < ApplicationRecord + belongs_to :bus + belongs_to :service +end diff --git a/app/models/service.rb b/app/models/service.rb index 9cbb2a32..1781543c 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -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 } diff --git a/app/views/trips/_delimiter.html b/app/views/trips/_delimiter.html new file mode 100644 index 00000000..18c9291f --- /dev/null +++ b/app/views/trips/_delimiter.html @@ -0,0 +1 @@ +==================================================== \ No newline at end of file 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 index 178ea8c0..a2a4af0f 100644 --- a/app/views/trips/_service.html.erb +++ b/app/views/trips/_service.html.erb @@ -1 +1 @@ -
  • <%= "#{service.name}" %>
  • +
  • <%= "#{service.name}" %>
  • \ No newline at end of file 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 @@ -
  • Сервисы в автобусе:
  • - diff --git a/app/views/trips/_trip.html.erb b/app/views/trips/_trip.html.erb index fa1de9aa..6e51b686 100644 --- a/app/views/trips/_trip.html.erb +++ b/app/views/trips/_trip.html.erb @@ -1,5 +1,12 @@ -
  • <%= "Отправление: #{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}" %>
  • + diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb index a60bce41..648aa4b7 100644 --- a/app/views/trips/index.html.erb +++ b/app/views/trips/index.html.erb @@ -2,15 +2,7 @@ <%= "Автобусы #{@from.name} – #{@to.name}" %>

    - <%= "В расписании #{@trips.count} рейсов" %> + <%= "В расписании #{@trips.size} рейсов" %>

    -<% @trips.each do |trip| %> - - <%= render "delimiter" %> -<% end %> +<%= render partial: 'trip', collection: @trips, spacer_template: 'delimiter' %> diff --git a/bin/bootsnap b/bin/bootsnap new file mode 100755 index 00000000..0f5dd65c --- /dev/null +++ b/bin/bootsnap @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bootsnap' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("bootsnap", "bootsnap") diff --git a/bin/bundle b/bin/bundle index f19acf5b..42c7fd7c 100755 --- a/bin/bundle +++ b/bin/bundle @@ -1,3 +1,109 @@ #!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -load Gem.bin_path('bundler', 'bundle') +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/byebug b/bin/byebug new file mode 100755 index 00000000..abc90dbf --- /dev/null +++ b/bin/byebug @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'byebug' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("byebug", "byebug") diff --git a/bin/htmldiff b/bin/htmldiff new file mode 100755 index 00000000..0aeaec87 --- /dev/null +++ b/bin/htmldiff @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'htmldiff' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("diff-lcs", "htmldiff") diff --git a/bin/ldiff b/bin/ldiff new file mode 100755 index 00000000..8173edec --- /dev/null +++ b/bin/ldiff @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'ldiff' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("diff-lcs", "ldiff") diff --git a/bin/listen b/bin/listen new file mode 100755 index 00000000..613171d2 --- /dev/null +++ b/bin/listen @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'listen' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("listen", "listen") diff --git a/bin/nokogiri b/bin/nokogiri new file mode 100755 index 00000000..c00ec262 --- /dev/null +++ b/bin/nokogiri @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'nokogiri' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("nokogiri", "nokogiri") diff --git a/bin/puma b/bin/puma new file mode 100755 index 00000000..01a92a32 --- /dev/null +++ b/bin/puma @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'puma' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("puma", "puma") diff --git a/bin/pumactl b/bin/pumactl new file mode 100755 index 00000000..c93cff21 --- /dev/null +++ b/bin/pumactl @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'pumactl' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("puma", "pumactl") diff --git a/bin/racc b/bin/racc new file mode 100755 index 00000000..81900158 --- /dev/null +++ b/bin/racc @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'racc' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("racc", "racc") diff --git a/bin/rackup b/bin/rackup new file mode 100755 index 00000000..0af6fafd --- /dev/null +++ b/bin/rackup @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rackup' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rack", "rackup") diff --git a/bin/rails b/bin/rails index 5badb2fd..a93ac1a5 100755 --- a/bin/rails +++ b/bin/rails @@ -1,9 +1,27 @@ #!/usr/bin/env ruby -begin - load File.expand_path('../spring', __FILE__) -rescue LoadError => e - raise unless e.message.include?('spring') +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rails' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end end -APP_PATH = File.expand_path('../config/application', __dir__) -require_relative '../config/boot' -require 'rails/commands' + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("railties", "rails") diff --git a/bin/rake b/bin/rake index d87d5f57..4eb7d7bf 100755 --- a/bin/rake +++ b/bin/rake @@ -1,9 +1,27 @@ #!/usr/bin/env ruby -begin - load File.expand_path('../spring', __FILE__) -rescue LoadError => e - raise unless e.message.include?('spring') +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rake' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end end -require_relative '../config/boot' -require 'rake' -Rake.application.run + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rake", "rake") diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 00000000..cb53ebe5 --- /dev/null +++ b/bin/rspec @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") diff --git a/bin/ruby-memory-profiler b/bin/ruby-memory-profiler new file mode 100755 index 00000000..4f90dde1 --- /dev/null +++ b/bin/ruby-memory-profiler @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'ruby-memory-profiler' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("memory_profiler", "ruby-memory-profiler") diff --git a/bin/sprockets b/bin/sprockets new file mode 100755 index 00000000..0068cd75 --- /dev/null +++ b/bin/sprockets @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'sprockets' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("sprockets", "sprockets") diff --git a/bin/stackprof b/bin/stackprof new file mode 100755 index 00000000..17c181e7 --- /dev/null +++ b/bin/stackprof @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'stackprof' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("stackprof", "stackprof") diff --git a/bin/stackprof-flamegraph.pl b/bin/stackprof-flamegraph.pl new file mode 100755 index 00000000..517079e8 --- /dev/null +++ b/bin/stackprof-flamegraph.pl @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'stackprof-flamegraph.pl' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("stackprof", "stackprof-flamegraph.pl") diff --git a/bin/stackprof-gprof2dot.py b/bin/stackprof-gprof2dot.py new file mode 100755 index 00000000..bf8007fa --- /dev/null +++ b/bin/stackprof-gprof2dot.py @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'stackprof-gprof2dot.py' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("stackprof", "stackprof-gprof2dot.py") diff --git a/bin/thor b/bin/thor new file mode 100755 index 00000000..ec401151 --- /dev/null +++ b/bin/thor @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'thor' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thor", "thor") diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..90588507 --- /dev/null +++ b/case-study.md @@ -0,0 +1,207 @@ +# Case-study оптимизации + +## Актуальная проблема + +В нашем проекте возникла серьёзная проблема. + +Страница расписаний формируется не эффективно, механизм перезагрузки расписаний из файла занимает очень много времени(больше минуты) + +## Задачи + +### №1 Сократить время выполнения импорта данных + +### №2 Оптимизировать загрузку отображения расписаний + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время исполнения rake таски и страницы с рейсами + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался: + +- pghero +- rack-mini-profiler +- memory-profiler +- bullet +- rspec-benchmark +- rspec-sqlimit + +### Шаг №0 + +Прогнал rake reload_json для всех файлов в fixtures, записал результаты +large.json отрабатывал слишком долго, не стал ждать +Буду ориентироваться на small.json, результат: 30 секунд + +Решил сразу воспользоваться советом использовать гем `activerecord-import` + +Поставил его, применил для импорта Trip +После прогона make reload_small время работы сократилось и составило 26 сек + +### Шаг №1 + +В этом шаге решил сконцентрироваться на загрузке данных из json в базу + +#### Подготовка: + +Добавил в проект +- pghero +- rack-mini-profiler +- memory-profiler +- bullet +- rspec-benchmark +- rspec-sqlimit + +Перенес код по импорту данных из rake task в отдельный интерактор `ImportData` + +Для того, чтобы лучше разобраться в коде и защитить его, написал rspec тесты и сделал рефакторинг + +#### Профилирование: + +С помощью тестов `rspec-sqlimit` смог увидеть количество запросов, которые я делаю в базу + +Как оказалось, использовать `activerecord-import` только лишь для Trip было не так корректно, так как к другим таблицам все равно было очень много запросов. + +#### Изменения: + +Сделал хранение всей информации в хеше. Это позволит нам не делать так много запросов SELECT к базе, чтобы найти запись. + +Использовал метод `import` из гема `activerecord-import` для других моделей: +Service, Bus, City + +#### Результаты: + +Импорт данных: +| |ДО |ПОСЛЕ | +|------------------|--------------------------|--------------------------| +| example.json | 0.547757 сек | 0.257931 сек | +| large.json | очень долго | 56 сек | +| medium.json | 229 сек | 10 сек | +| small.json | 30 сек | 2.990186 сек | + +#### Выводы: + +Нагрузка на память выросла за счет того, что все данные стали хранится в хешах(MEMORY USAGE: 1067 MB) +Но я смог сократить время работы за счет уменьшения количества запросов в базу. +Так что по метрике задача №1 была выполнена + +Думаю, что можно сократить и показатели по памяти, но тогда нужно будет отказаться от `activerecord-import` и стримить записи сразу в бд + +### Шаг №3 + +В этом шаге решил сконцентрироваться на оптимизации загрузки страницы с рейсами + +Импортировал данные из файла large.json в базу + +#### 1. + +Страница с рейсами загрузилась за 24158.2 ms +`bullet` сразу дал подсказку 'USE eager loading detected' для `app/views/trips/_trip.html.erb` + +Результаты rack-mini-profiler так же указали на то, что здесь происходит очень много запросов на получении информации по каждому автобусу +``` +Rendering: trips/index.html.erb 10686.2 +18.8 650 sql 1815.9 +``` + +Добавил `includes([:bus])` в запрос на получение @trips + +#### 2. + +Дальше `bullet` стал жаловаться на такую же проблему N+1 и для `services` + +Изменил запрос на получение @trips, чтобы дополнительно загружать еще и services: +``` +@trips = Trip.preload(bus: :services).where(from: @from, to: @to).order(:start_time).load +``` + +После этого ActiveRecord больше не был главной точкой роста + +Результаты rack-mini-profiler: +``` +Rendering: trips/index.html.erb 6081.0 +2326.9 1 sql 9.3 +``` + +Логи Rails: +``` +Completed 200 OK in 17511ms (Views: 15219.8ms | ActiveRecord: 100.8ms) +``` + +#### 3. + +По логам и результатам профилировщика rack-mini-profiler можно было заметить, что теперь основное время тратилось на загрузку вьюх + +Решил избавиться от partials и перенести весь код из них в trips/index.html.erb +После изменений страница стала загружаться быстрее: +``` +Completed 200 OK in 6027ms (Views: 3736.2ms | ActiveRecord: 56.0ms) +``` + +#### 4. + +Нашел в trips/index.html.erb вызов count, заменил его на size +Время загрузки ActiveRecord изменилось в лучшую сторону: +``` +Completed 200 OK in 6313ms (Views: 4103.4ms | ActiveRecord: 43.3ms) +``` + +#### 5. + +Вернул обратно partials, но сделал их вызов с указанием коллекции +Это сделало код более читабельным, но при этом мы не потеряли в производительности + +#### Результаты: + +Загрузка страницы с расписанием: +|ДО |ПОСЛЕ | +|--------------------------|--------------------------| +| 24158.2 ms | 6313.3 ms | + +#### Выводы + +Страница с расписаниями стала намного быстрее загружаться, но 6 сек - это все равно очень большое время +Не думаю, что людям нужно сразу отображать всю информацию по расписанию, так как на странице загружается сразу 1004 рейса +В таком случае можно сделать пагинацию или последовательную загрузку данных по рейсам + +### Шаг №4 + +В этом шаге решил обновить ruby и rails, чтобы посмотреть как измениться производительность + +Обновил ruby с 2.6.3 до 3.2.2 +Rails с 5.2.3 до ~> 6.0 + +#### Результаты + +Импорт данных: +| |ДО |ПОСЛЕ ПЕРВОГО ШАГА |ПОСЛЕ ОБНОВЛЕНИЯ RUBY | +|------------------|--------------------------|--------------------------|----------------------| +| example.json | 0.547757 сек | 0.257931 сек | 0.221117 сек | +| large.json | очень долго | 56 сек | 45.511732 сек | +| medium.json | 229 сек | 10 сек | 7.469367 сек | +| small.json | 30 сек | 2.990186 сек | 2.487498 сек | + +Загрузка страницы с расписанием: +``` +Completed 200 OK in 7124ms (Views: 5059.9ms | ActiveRecord: 39.5ms | Allocations: 6974902) +``` + +#### Выводы: + +Не забывайте держать версию ruby в актуальном состоянии +Зачастую обновить язык - это один из самых эффективных способов повысить производительность + +### Наблюдения + +В рабочем проекте используем pghero как хороший инструмент для выявления медленных запросов, подсказок по индексам, занятой памяти +В этом проекте pghero показывает все зеленым. Пробовал делать много запросов - все равно его все устраивает. + +#### Совет: как посчитать кол-во строк в файле +``` +wc -l data_large.rb # (3250940) total line count +``` + +#### Совет: как создать меньший файл из большего, оставив первые N строк +``` +head -n N data_large.txt > dataN.txt +``` + +## Защита от регрессии производительности + +Были написаны performace тесты на количество запросов в базу и скорость выполнения задачи \ No newline at end of file diff --git a/config/database.yml b/config/database.yml index e116cfa6..c5a00f15 100644 --- a/config/database.yml +++ b/config/database.yml @@ -16,7 +16,11 @@ # default: &default adapter: postgresql + host: localhost encoding: unicode + port: 5434 + username: postgres + password: postgres # For details on connection pooling, see Rails configuration guide # http://guides.rubyonrails.org/configuring.html#database-pooling pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 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/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/routes.rb b/config/routes.rb index a2da6a7b..7f620876 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,4 +2,5 @@ # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html get "/" => "statistics#index" get "автобусы/:from/:to" => "trips#index" + mount PgHero::Engine, at: "pghero" end diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..e28f7b93 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.9' +services: + postgres: + container_name: "optimization_task_3_postgres" + hostname: 'postgres' + image: 'postgres:14' + environment: + POSTGRES_USER: 'postgres' + POSTGRES_PASSWORD: 'postgres' + ports: + - "5434:5432" + command: postgres -c shared_preload_libraries='pg_stat_statements' -c pg_stat_statements.track=all diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index 540fe871..d2d8d500 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -1,34 +1,16 @@ # Наивная загрузка данных из 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'], - ) +task :reload_json, [:file_name, :profiler] => :environment do |_task, args| + case args.profiler + when 'memory' + report = MemoryProfiler.report do + ImportData.run!(file_name: args.file_name) end + + report.pretty_print(scale_bytes: true, color_output: true) + report.pretty_print(scale_bytes: true, to_file: 'reports/tmp/memory_profiler.txt') + else + ImportData.run!(file_name: args.file_name) end end diff --git a/spec/interactions/import_data_spec.rb b/spec/interactions/import_data_spec.rb new file mode 100644 index 00000000..2436e4d4 --- /dev/null +++ b/spec/interactions/import_data_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +RSpec.describe ImportData do + subject { described_class.run!(file_name: file_name) } + let(:file_name) { 'fixtures/example.json' } + + describe '#execute' do + context 'common' do + it 'creates trips' do + expect { subject } + .to change { Trip.count }.by(10) + .and change { City.count }.by(2) + .and change { Service.count }.by(2) # 10 + .and change { Bus.count }.by(1) + .and change { BusesService.count }.by(2) + end + + it 'does not send unnecessary INSERT requests to db' do + expect { subject }.not_to exceed_query_limit(5).with(/^INSERT/) + end + + it 'does not send unnecessary SELECT requests to db' do + expect { subject }.not_to exceed_query_limit(0).with(/^SELECT/) + end + + it 'does not send unnecessary DELETE requests to db' do + expect { subject }.not_to exceed_query_limit(5).with(/^DELETE/) + end + + it 'works with better performance' do + expect { subject }.to perform_under(8).us.warmup(2).times.sample(10).times + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000..b6317b5a --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,64 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..fc2ba41e --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,99 @@ +require 'rspec-sqlimit' +require 'rspec-benchmark' + +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers + + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end