From 6fb78d0648fc2c001feaf0adb3150d1c2f1dc86a Mon Sep 17 00:00:00 2001 From: Udalov Igor Date: Sat, 28 Oct 2023 16:26:56 +0300 Subject: [PATCH 01/11] Install rspec --- .rspec | 1 + Gemfile | 1 + Gemfile.lock | 204 +++++++++++++++++++++++++------------------ spec/rails_helper.rb | 64 ++++++++++++++ spec/spec_helper.rb | 94 ++++++++++++++++++++ 5 files changed, 279 insertions(+), 85 deletions(-) create mode 100644 .rspec create mode 100644 spec/rails_helper.rb create mode 100644 spec/spec_helper.rb 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..0060f5b2 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,7 @@ group :development do end group :test do + gem 'rspec-rails' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem diff --git a/Gemfile.lock b/Gemfile.lock index fccf6f5f..1ba71e7b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,134 +1,167 @@ GEM remote: https://rubygems.org/ specs: - actioncable (5.2.3) - actionpack (= 5.2.3) + actioncable (5.2.8.1) + actionpack (= 5.2.8.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) + actionmailer (5.2.8.1) + actionpack (= 5.2.8.1) + actionview (= 5.2.8.1) + activejob (= 5.2.8.1) 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 (5.2.8.1) + actionview (= 5.2.8.1) + activesupport (= 5.2.8.1) + rack (~> 2.0, >= 2.0.8) 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) + actionview (5.2.8.1) + activesupport (= 5.2.8.1) 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) + activejob (5.2.8.1) + activesupport (= 5.2.8.1) globalid (>= 0.3.6) - activemodel (5.2.3) - activesupport (= 5.2.3) - activerecord (5.2.3) - activemodel (= 5.2.3) - activesupport (= 5.2.3) + activemodel (5.2.8.1) + activesupport (= 5.2.8.1) + activerecord (5.2.8.1) + activemodel (= 5.2.8.1) + activesupport (= 5.2.8.1) arel (>= 9.0) - activestorage (5.2.3) - actionpack (= 5.2.3) - activerecord (= 5.2.3) - marcel (~> 0.3.1) - activesupport (5.2.3) + activestorage (5.2.8.1) + actionpack (= 5.2.8.1) + activerecord (= 5.2.8.1) + marcel (~> 1.0.0) + activesupport (5.2.8.1) 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) + bindex (0.8.1) + bootsnap (1.16.0) + msgpack (~> 1.2) + builder (3.2.4) + 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) + ffi (1.16.3) + globalid (1.1.0) + activesupport (>= 5.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) - loofah (2.2.3) + loofah (2.21.4) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + 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) + net-imap + net-pop + net-smtp + marcel (1.0.2) + method_source (1.0.0) + mini_mime (1.1.5) + mini_portile2 (2.8.5) + minitest (5.20.0) + msgpack (1.7.2) + net-imap (0.3.7) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.4.0) + net-protocol + nio4r (2.5.9) + nokogiri (1.13.10) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) + pg (1.5.4) + puma (3.12.6) + racc (1.7.1) + rack (2.2.8) + rack-test (2.1.0) + rack (>= 1.3) + rails (5.2.8.1) + actioncable (= 5.2.8.1) + actionmailer (= 5.2.8.1) + actionpack (= 5.2.8.1) + actionview (= 5.2.8.1) + activejob (= 5.2.8.1) + activemodel (= 5.2.8.1) + activerecord (= 5.2.8.1) + activestorage (= 5.2.8.1) + activesupport (= 5.2.8.1) bundler (>= 1.3.0) - railties (= 5.2.3) + railties (= 5.2.8.1) sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest 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 (5.2.8.1) + actionpack (= 5.2.8.1) + activesupport (= 5.2.8.1) 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 (13.1.0) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) ffi (~> 1.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.6) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-rails (5.1.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + railties (>= 5.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) + rspec-support (3.12.1) ruby_dep (1.5.0) - sprockets (3.7.2) + sprockets (4.2.1) 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) + thor (1.3.0) thread_safe (0.3.6) - tzinfo (1.2.5) + timeout (0.4.0) + tzinfo (1.2.11) thread_safe (~> 0.1) web-console (3.7.0) actionview (>= 5.0) activemodel (>= 5.0) bindex (>= 0.4.0) railties (>= 5.0) - websocket-driver (0.7.0) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) + websocket-extensions (0.1.5) PLATFORMS ruby @@ -140,6 +173,7 @@ DEPENDENCIES pg (>= 0.18, < 2.0) puma (~> 3.11) rails (~> 5.2.3) + rspec-rails tzinfo-data web-console (>= 3.3.0) 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..327b58ea --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,94 @@ +# 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| + # 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 From 82f813067e82522e55bd1c3930b2eb7432c1c015 Mon Sep 17 00:00:00 2001 From: Udalov Igor Date: Sat, 28 Oct 2023 16:58:34 +0300 Subject: [PATCH 02/11] Config db --- config/database.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/database.yml b/config/database.yml index e116cfa6..e56ec196 100644 --- a/config/database.yml +++ b/config/database.yml @@ -20,10 +20,12 @@ default: &default # 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: task_4 + password: task_4 development: <<: *default - database: task-4_development + database: task_4_development # 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: task_4_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 From 7e590b4c1d5c628df0bb13c0f12bc48737cede91 Mon Sep 17 00:00:00 2001 From: Udalov Igor Date: Sun, 29 Oct 2023 13:52:36 +0300 Subject: [PATCH 03/11] Update loader --- Gemfile | 2 + Gemfile.lock | 3 + bin/setup | 7 +- case-study.md | 23 + config/routes.rb | 4 +- ...0231028140206_create_pghero_query_stats.rb | 15 + db/schema.rb | 13 +- lib/tasks/utils.rake | 64 +- out.html | 8100 +++++++++++++++++ 9 files changed, 8213 insertions(+), 18 deletions(-) create mode 100644 case-study.md create mode 100644 db/migrate/20231028140206_create_pghero_query_stats.rb create mode 100644 out.html diff --git a/Gemfile b/Gemfile index 0060f5b2..0cd0bcde 100644 --- a/Gemfile +++ b/Gemfile @@ -25,3 +25,5 @@ end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] + +gem "pghero" diff --git a/Gemfile.lock b/Gemfile.lock index 1ba71e7b..25e89a2f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,6 +90,8 @@ GEM mini_portile2 (~> 2.8.0) racc (~> 1.4) pg (1.5.4) + pghero (2.8.3) + activerecord (>= 5) puma (3.12.6) racc (1.7.1) rack (2.2.8) @@ -171,6 +173,7 @@ DEPENDENCIES byebug listen (>= 3.0.5, < 3.2) pg (>= 0.18, < 2.0) + pghero puma (~> 3.11) rails (~> 5.2.3) rspec-rails diff --git a/bin/setup b/bin/setup index f294207b..affa6766 100755 --- a/bin/setup +++ b/bin/setup @@ -28,8 +28,11 @@ chdir APP_ROOT do puts "\n== Preparing database ==" system! 'bin/rails db:setup' - puts "\n== Loading data from fixtures/small.json ==" - system! 'bin/rake reload_json[fixtures/small.json]' + # puts "\n== Loading data from fixtures/small.json ==" + # system! 'bin/rake reload_json[fixtures/small.json]' + + puts "\n== Loading data from fixtures/large.json ==" + system! 'bin/rake reload_json[fixtures/large.json]' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..9e836fc2 --- /dev/null +++ b/case-study.md @@ -0,0 +1,23 @@ +# Загрузка файла + +Прикрутил логи и загрузил в `pgbadger`. Первое на что обращаешь внимание - кол-во запросов. + +У нас нет `Rails 6`, поэтому вместо `insert_all` будем писать запросы на `SQL`. +``` +City.insert_all(cities_names.map { |name| { name: name } }) +``` + +Готово! большой файл загружается за 6 секунд: +``` +== Loading data from fixtures/large.json == +Done! it took: 6.605176 sec. + + $ rails c +Loading development environment (Rails 5.2.8.1) +irb(main):001:0> Bus.count + (0.7ms) SELECT COUNT(*) FROM "buses" +=> 1000 +irb(main):002:0> Trip.count + (13.2ms) SELECT COUNT(*) FROM "trips" +=> 100000 +``` diff --git a/config/routes.rb b/config/routes.rb index a2da6a7b..371b78fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,7 @@ Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html - get "/" => "statistics#index" + mount PgHero::Engine, at: "pghero" + + # get "/" => "statistics#index" get "автобусы/:from/:to" => "trips#index" end diff --git a/db/migrate/20231028140206_create_pghero_query_stats.rb b/db/migrate/20231028140206_create_pghero_query_stats.rb new file mode 100644 index 00000000..fbf41263 --- /dev/null +++ b/db/migrate/20231028140206_create_pghero_query_stats.rb @@ -0,0 +1,15 @@ +class CreatePgheroQueryStats < ActiveRecord::Migration[5.2] + def change + create_table :pghero_query_stats do |t| + t.text :database + t.text :user + t.text :query + t.integer :query_hash, limit: 8 + t.float :total_time + t.integer :calls, limit: 8 + t.timestamp :captured_at + end + + add_index :pghero_query_stats, [:database, :captured_at] + end +end diff --git a/db/schema.rb b/db/schema.rb index f6921e45..96006cf7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_03_30_193044) do +ActiveRecord::Schema.define(version: 2023_10_28_140206) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -29,6 +29,17 @@ t.string "name" end + create_table "pghero_query_stats", force: :cascade do |t| + t.text "database" + t.text "user" + t.text "query" + t.bigint "query_hash" + t.float "total_time" + t.bigint "calls" + t.datetime "captured_at" + t.index ["database", "captured_at"], name: "index_pghero_query_stats_on_database_and_captured_at" + end + create_table "services", force: :cascade do |t| t.string "name" end diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index 540fe871..9bfbe0ca 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -1,6 +1,11 @@ +def insert_multiple(array) + +end + # Наивная загрузка данных из json-файла в БД # rake reload_json[fixtures/small.json] task :reload_json, [:file_name] => :environment do |_task, args| + time_point = Time.now json = JSON.parse(File.read(args.file_name)) ActiveRecord::Base.transaction do @@ -10,25 +15,56 @@ task :reload_json, [:file_name] => :environment do |_task, args| Trip.delete_all ActiveRecord::Base.connection.execute('delete from buses_services;') + # create cities + cities_names = ['from', 'to'].map { |f| json.pluck(f) }.flatten.uniq + sql = "INSERT INTO cities ( name ) VALUES #{cities_names.map { |n| "( '#{n}' )" }.join(', ') };" + ActiveRecord::Base.connection.execute(sql) + + # create buses + buses = json.map { |trip| [trip['bus']['number'], trip['bus']['model']] }.uniq + sql = "INSERT INTO buses ( number, model ) VALUES #{buses.map { |b| "( '#{b[0]}', '#{b[1]}' )" }.join(', ') };" + ActiveRecord::Base.connection.execute(sql) + + # create services + services_names = json.map { |trip| trip['bus']['services'] }.flatten.uniq + sql = "INSERT INTO services ( name ) VALUES #{services_names.map { |n| "( '#{n}' )" }.join(', ') };" + ActiveRecord::Base.connection.execute(sql) + + # assign services to buses + services_ids_by_name = Service.all.map { |s| [s.name, s.id] }.to_h + buses_ids_by_number = Bus.all.map { |b| [b.number, b.id] }.to_h + 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 + bus = trip['bus'] + bus_id = buses_ids_by_number[bus['number']] + bus['services'].each do |service_name| + service_id = services_ids_by_name[service_name] + buses_services << [bus_id, service_id] end - bus = Bus.find_or_create_by(number: trip['bus']['number']) - bus.update(model: trip['bus']['model'], services: services) + end + sql = <<~HEREDOC + INSERT INTO buses_services ( bus_id, service_id ) VALUES + #{buses_services.map { |b_s| "( #{b_s[0]}, #{b_s[1]} )" }.join(', ') }; + HEREDOC + ActiveRecord::Base.connection.execute(sql) - Trip.create!( - from: from, - to: to, - bus: bus, + # create trips + cities_ids_by_name = City.all.map { |s| [s.name, s.id] }.to_h + trips = json.map do |trip| + { + from_id: cities_ids_by_name[trip['from']], + to_id: cities_ids_by_name[trip['to']], + bus_id: buses_ids_by_number[trip['bus']['number']], start_time: trip['start_time'], duration_minutes: trip['duration_minutes'], - price_cents: trip['price_cents'], - ) + price_cents: trip['price_cents'] + } end + sql = <<~HEREDOC + INSERT INTO trips ( from_id, to_id, start_time, duration_minutes, price_cents, bus_id ) VALUES + #{trips.map { |trip| "( #{trip[:from_id]}, #{trip[:to_id]}, '#{trip[:start_time]}', #{trip[:duration_minutes]}, #{trip[:price_cents]}, #{trip[:bus_id]} )" }.join(', ') }; + HEREDOC + ActiveRecord::Base.connection.execute(sql) end + puts "Done! it took: #{Time.now - time_point} sec." end diff --git a/out.html b/out.html new file mode 100644 index 00000000..2222164b --- /dev/null +++ b/out.html @@ -0,0 +1,8100 @@ + + + +pgBadger :: PostgreSQL Log Analyzer + + + + + + + + + + +
+ + +
+ +
    + +
  • + +
    +

    Global information

    +
    +
    + +
      +
    • Generated on Sun Oct 29 12:14:01 2023
    • +
    • Log file: /Library/PostgreSQL/15/data/log/postgresql-2023-10-29_131256.log
    • +
    • Parsed 60,822 log entries in 5s
    • +
    • Log start from 2023-10-29 13:12:56 to 2023-10-29 13:13:58
    • +
    + +
    +
    +
    +
  • + + +
  • + +

    Overview

    + +
    +

    Global Stats

    +
    + +
    +
    +
      +
    • 11 Number of unique normalized queries
    • +
    • 8,950 Number of queries
    • +
    • 624ms Total query duration
    • +
    • 2023-10-29 13:12:56 First query
    • +
    • 2023-10-29 13:13:58 Last query
    • +
    • 1,428 queries/s at 2023-10-29 13:13:02 Query peak
    • +
    +
    +
    +
      +
    • 624ms Total query duration
    • +
    • 123ms Prepare/parse total duration
    • +
    • 209ms Bind total duration
    • +
    • 291ms Execute total duration
    • +
    +
    +
    +
      +
    • 0 Number of events
    • +
    • 0 Number of unique normalized events
    • +
    • 0 Max number of times the same event was reported
    • +
    • 0 Number of cancellation
    • +
    +
    +
    +
      +
    • 2 Total number of automatic vacuums
    • +
    • 0 Total number of automatic analyzes
    • +
    +
    +
    +
      +
    • 0 Number temporary file
    • +
    • 0 Max size of temporary file
    • +
    • 0.00 B Average size of temporary file
    • +
    +
    +
    +
      +
    • 1 Total number of sessions
    • +
    • 0 sessions at Session peak
    • +
    • 10s174ms Total duration of sessions
    • +
    • 10s174ms Average duration of sessions
    • +
    • 8,950 Average queries per session
    • +
    • 624ms Average queries duration per session
    • +
    • 9s549ms Average idle time per session
    • + +
    +
    +
    +
      +
    • 0 Total number of connections
    • + +
    • 1 Total number of databases
    • +
    +
    +
    +
    +
    + +
    +

    SQL Traffic

    +
    +

    Key values

    +
    +
      +
    • 1,428 queries/s Query Peak
    • +
    • 2023-10-29 13:13:02 Date
    • +
    +
    +
    +
    +
    + + +
    +
    + +
    +

    SELECT Traffic

    +
    +

    Key values

    +
    +
      +
    • 1,092 queries/s Query Peak
    • +
    • 2023-10-29 13:13:02 Date
    • +
    +
    +
    +
    +
    + + +
    +
    + +
    +

    INSERT/UPDATE/DELETE Traffic

    +
    +

    Key values

    +
    +
      +
    • 444 queries/s Query Peak
    • +
    • 2023-10-29 13:12:57 Date
    • +
    +
    +
    +
    +
    + + +
    +
    + +
    +

    Queries duration

    +
    +

    Key values

    +
    +
      +
    • 624ms Total query duration
    • +
    +
    +
    +
    +
    + + +
    +
    + +
    +

    Prepared queries ratio

    +
    +

    Key values

    +
    +
      +
    • 0.00 Ratio of bind vs prepare
    • +
    • 0.00 % Ratio between prepared and "usual" statements
    • +
    +
    +
    +
    +
    + + +
    +
    + +
    +

    General Activity

    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DayHourCountMin durationMax durationAvg durationLatency Percentile(90)Latency Percentile(95)Latency Percentile(99)
    Oct 29138,9500ms0ms0ms171ms171ms171ms
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DayHourSELECTCOPY TOAverage DurationLatency Percentile(90)Latency Percentile(95)Latency Percentile(99)
    Oct 29136,41500ms138ms138ms138ms
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DayHourINSERTUPDATEDELETECOPY FROMAverage DurationLatency Percentile(90)Latency Percentile(95)Latency Percentile(99)
    Oct 29132,5340000ms38ms38ms38ms
    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    DayHourPrepareBindBind/PreparePercentage of prepare
    Oct 29133,5708,9372.5039.89%
    +
    +
    + + + + + + + + + + + + + + + + + +
    DayHourCountAverage / Second
    Oct 291300.00/s
    +
    +
    + + + + + + + + + + + + + + + + + + + +
    DayHourCountAverage DurationAverage idle time
    Oct 2913110s174ms9s882ms
    +
    +
    + Back to the top of the General Activity table +
    + +
    + +
  • + + + + + + + + + +
+ + + + +
+ + + +
+ +
+ + + From 0070879508b2a15129014feb59121a9efee5dda9 Mon Sep 17 00:00:00 2001 From: Udalov Igor Date: Sun, 29 Oct 2023 21:27:38 +0300 Subject: [PATCH 04/11] Fix loader and install rails panel --- Gemfile | 1 + Gemfile.lock | 6 ++++ case-study.md | 83 ++++++++++++++++++++++++++++++++++++++++---- lib/tasks/utils.rake | 7 +++- 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 0cd0bcde..732d49b5 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,7 @@ group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' + gem 'meta_request' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 25e89a2f..5c4eac8c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -71,6 +71,9 @@ GEM net-pop net-smtp marcel (1.0.2) + meta_request (0.7.4) + rack-contrib (>= 1.1, < 3) + railties (>= 3.0.0, < 7.1) method_source (1.0.0) mini_mime (1.1.5) mini_portile2 (2.8.5) @@ -95,6 +98,8 @@ GEM puma (3.12.6) racc (1.7.1) rack (2.2.8) + rack-contrib (2.4.0) + rack (< 4) rack-test (2.1.0) rack (>= 1.3) rails (5.2.8.1) @@ -172,6 +177,7 @@ DEPENDENCIES bootsnap (>= 1.1.0) byebug listen (>= 3.0.5, < 3.2) + meta_request pg (>= 0.18, < 2.0) pghero puma (~> 3.11) diff --git a/case-study.md b/case-study.md index 9e836fc2..ad6252a8 100644 --- a/case-study.md +++ b/case-study.md @@ -7,17 +7,88 @@ City.insert_all(cities_names.map { |name| { name: name } }) ``` -Готово! большой файл загружается за 6 секунд: +Готово! большой файл загружается за 4 секунд: ``` == Loading data from fixtures/large.json == -Done! it took: 6.605176 sec. +Done! it took: 4.770822 sec. - $ rails c +### + + $ b rails c Loading development environment (Rails 5.2.8.1) irb(main):001:0> Bus.count - (0.7ms) SELECT COUNT(*) FROM "buses" + (0.6ms) SELECT COUNT(*) FROM "buses" => 1000 -irb(main):002:0> Trip.count - (13.2ms) SELECT COUNT(*) FROM "trips" +irb(main):002:0> Service.count + (0.5ms) SELECT COUNT(*) FROM "services" +=> 10 +irb(main):003:0> Trip.count + (7.6ms) SELECT COUNT(*) FROM "trips" => 100000 +irb(main):004:0> City.count + (0.5ms) SELECT COUNT(*) FROM "cities" +=> 10 +irb(main):005:0> Bus.last.services + Bus Load (0.3ms) SELECT "buses".* FROM "buses" ORDER BY "buses"."id" DESC LIMIT $1 [["LIMIT", 1]] + Service Load (1.2ms) SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1 LIMIT $2 [["bus_id", 1000], ["LIMIT", 11]] +=> #, #, #, #, #, #, #, #, #]> + +``` + +# Загрузка файла + +Начинаем с того, что дождаться загрузки страницы не удается: +``` + CACHE Bus Load (0.0ms) SELECT "buses".* FROM "buses" WHERE "buses"."id" = $1 LIMIT $2 [["id", 813], ["LIMIT", 1]] + ↳ app/views/trips/_trip.html.erb:5 + Rendered trips/_trip.html.erb (1.5ms) + CACHE Service Load (0.0ms) SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1 [["bus_id", 813]] + ↳ app/views/trips/index.html.erb:11 + Rendered trips/_service.html.erb (0.0ms) + # ... + Rendered trips/_services.html.erb (173.9ms) + Rendered trips/_delimiter.html.erb (0.0ms) + Bus Load (0.4ms) SELECT "buses".* FROM "buses" WHERE "buses"."id" = $1 LIMIT $2 [["id", 106], ["LIMIT", 1]] + ↳ app/views/trips/_trip.html.erb:5 + Rendered trips/_trip.html.erb (1.6ms) + Service Load (33.1ms) SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1 [["bus_id", 106]] + ↳ app/views/trips/index.html.erb:11 + Rendered trips/_service.html.erb (0.0ms) + # ... +``` + +Нам нужно что-то сделать с повторяющейся: +``` + Rendered collection of services/_service.html.erb [535 times] (3.4ms) +``` + +Заменяем запись на: ``` + <% render services %> +``` + +Убеждаемся, что `rails` помогает нам более эффективно рендерить коллекции: +``` + Rendered collection of services/_service.html.erb [495 times] (3.1ms) +``` + +Ура! страница теперь грузится. Время: 40 секунд. + +Обращаем внимание на повторяющиеся запросы `bus` и `service`: +``` + CACHE Bus Load (0.0ms) SELECT "buses".* FROM "buses" WHERE "buses"."id" = $1 LIMIT $2 [["id", 771], ["LIMIT", 1]] + ↳ app/views/trips/_trip.html.erb:5 + Rendered trips/_trip.html.erb (2.3ms) + CACHE Service Load (0.0ms) SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1 [["bus_id", 771]] + ↳ app/views/trips/index.html.erb:11 + Rendered collection of services/_service.html.erb [279 times] (1.9ms) + Rendered trips/_services.html.erb (3.9ms) + Rendered trips/_delimiter.html.erb (0.0ms) +``` + +Добавим `preload` в `TripsController`: +``` +@trips = Trip.where(from: @from, to: @to).order(:start_time).preload(bus: :services) +``` + +Обратил внимание, что у меня не отображаются услуги по автобусам. Вероятно, какая-то проблема с миграцией. Проверим. diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index 9bfbe0ca..87e920ed 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -33,14 +33,19 @@ task :reload_json, [:file_name] => :environment do |_task, args| # assign services to buses services_ids_by_name = Service.all.map { |s| [s.name, s.id] }.to_h buses_ids_by_number = Bus.all.map { |b| [b.number, b.id] }.to_h + with_assigned_services = [] buses_services = [] json.each do |trip| bus = trip['bus'] - bus_id = buses_ids_by_number[bus['number']] + bus_number = bus['number'] + next if with_assigned_services.include?(bus_number) + + bus_id = buses_ids_by_number[bus_number] bus['services'].each do |service_name| service_id = services_ids_by_name[service_name] buses_services << [bus_id, service_id] end + with_assigned_services << bus_number end sql = <<~HEREDOC INSERT INTO buses_services ( bus_id, service_id ) VALUES From f7438f4c22c02d034db7d4159dd4a5617f490c1b Mon Sep 17 00:00:00 2001 From: Udalov Igor Date: Sun, 29 Oct 2023 21:49:08 +0300 Subject: [PATCH 05/11] Install bullet, solve n+1 & install rak-mini-profiler --- Gemfile | 2 ++ Gemfile.lock | 8 +++++ app/controllers/trips_controller.rb | 2 +- case-study.md | 55 +++++++---------------------- config/environments/development.rb | 9 +++++ config/environments/test.rb | 6 ++++ 6 files changed, 39 insertions(+), 43 deletions(-) diff --git a/Gemfile b/Gemfile index 732d49b5..d48c3474 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,7 @@ group :development do gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' gem 'meta_request' + gem 'bullet' end group :test do @@ -28,3 +29,4 @@ end gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem "pghero" +gem 'rack-mini-profiler' diff --git a/Gemfile.lock b/Gemfile.lock index 5c4eac8c..715c20c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,6 +47,9 @@ GEM bootsnap (1.16.0) msgpack (~> 1.2) builder (3.2.4) + bullet (7.1.2) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11) byebug (11.1.3) concurrent-ruby (1.2.2) crass (1.0.6) @@ -100,6 +103,8 @@ GEM rack (2.2.8) rack-contrib (2.4.0) rack (< 4) + rack-mini-profiler (3.1.1) + rack (>= 1.2.0) rack-test (2.1.0) rack (>= 1.3) rails (5.2.8.1) @@ -161,6 +166,7 @@ GEM timeout (0.4.0) tzinfo (1.2.11) thread_safe (~> 0.1) + uniform_notifier (1.16.0) web-console (3.7.0) actionview (>= 5.0) activemodel (>= 5.0) @@ -175,12 +181,14 @@ PLATFORMS DEPENDENCIES bootsnap (>= 1.1.0) + bullet byebug listen (>= 3.0.5, < 3.2) meta_request pg (>= 0.18, < 2.0) pghero puma (~> 3.11) + rack-mini-profiler rails (~> 5.2.3) rspec-rails tzinfo-data diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb index acb38be2..09176d7f 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).order(:start_time).preload(bus: :services) end end diff --git a/case-study.md b/case-study.md index ad6252a8..c5ea128a 100644 --- a/case-study.md +++ b/case-study.md @@ -35,60 +35,31 @@ irb(main):005:0> Bus.last.services ``` -# Загрузка файла +# Загрузка страницы -Начинаем с того, что дождаться загрузки страницы не удается: +Начальное время - 16 секунд. ``` - CACHE Bus Load (0.0ms) SELECT "buses".* FROM "buses" WHERE "buses"."id" = $1 LIMIT $2 [["id", 813], ["LIMIT", 1]] - ↳ app/views/trips/_trip.html.erb:5 - Rendered trips/_trip.html.erb (1.5ms) - CACHE Service Load (0.0ms) SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1 [["bus_id", 813]] - ↳ app/views/trips/index.html.erb:11 - Rendered trips/_service.html.erb (0.0ms) - # ... - Rendered trips/_services.html.erb (173.9ms) - Rendered trips/_delimiter.html.erb (0.0ms) - Bus Load (0.4ms) SELECT "buses".* FROM "buses" WHERE "buses"."id" = $1 LIMIT $2 [["id", 106], ["LIMIT", 1]] - ↳ app/views/trips/_trip.html.erb:5 - Rendered trips/_trip.html.erb (1.6ms) - Service Load (33.1ms) SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1 [["bus_id", 106]] - ↳ app/views/trips/index.html.erb:11 - Rendered trips/_service.html.erb (0.0ms) - # ... +Completed 200 OK in 16361ms (Views: 15437.2ms | ActiveRecord: 910.2ms) ``` +Судя по логам, есть работа для `bullet`. -Нам нужно что-то сделать с повторяющейся: ``` - Rendered collection of services/_service.html.erb [535 times] (3.4ms) + CACHE Bus Load (0.0ms) SELECT "buses".* FROM "buses" WHERE "buses"."id" = $1 LIMIT $2 [["id", 793], ["LIMIT", 1]] + ↳ app/views/trips/_trip.html.erb:5 + Rendered trips/_trip.html.erb (3.4ms) + CACHE Service Load (0.0ms) SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1 [["bus_id", 793]] ``` -Заменяем запись на: -``` - <% render services %> -``` +Гем говорит о целесообразности добавления `.includes(:bus)`. Правда, что странно - предлагает это сделать во вью. Мне кажется, лучше будет сделать в контроллере - и будем использовать `preload` в вместо `includes`. -Убеждаемся, что `rails` помогает нам более эффективно рендерить коллекции: +Время изменилось, но не существенно: ``` - Rendered collection of services/_service.html.erb [495 times] (3.1ms) +Completed 200 OK in 13197ms (Views: 12532.7ms | ActiveRecord: 649.9ms) ``` -Ура! страница теперь грузится. Время: 40 секунд. +Молодец, `bullet`! Подсказывает не забыть подгрузить `services` тоже. Все, больше для него работы нет. -Обращаем внимание на повторяющиеся запросы `bus` и `service`: -``` - CACHE Bus Load (0.0ms) SELECT "buses".* FROM "buses" WHERE "buses"."id" = $1 LIMIT $2 [["id", 771], ["LIMIT", 1]] - ↳ app/views/trips/_trip.html.erb:5 - Rendered trips/_trip.html.erb (2.3ms) - CACHE Service Load (0.0ms) SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1 [["bus_id", 771]] - ↳ app/views/trips/index.html.erb:11 - Rendered collection of services/_service.html.erb [279 times] (1.9ms) - Rendered trips/_services.html.erb (3.9ms) - Rendered trips/_delimiter.html.erb (0.0ms) -``` -Добавим `preload` в `TripsController`: ``` -@trips = Trip.where(from: @from, to: @to).order(:start_time).preload(bus: :services) +Completed 200 OK in 13902ms (Views: 13840.6ms | ActiveRecord: 47.7ms) ``` - -Обратил внимание, что у меня не отображаются услуги по автобусам. Вероятно, какая-то проблема с миграцией. Проверим. 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 From c32acbb06d9bcfdf5b77b240d66dbd0b25d39c90 Mon Sep 17 00:00:00 2001 From: Udalov Igor Date: Mon, 30 Oct 2023 09:16:54 +0300 Subject: [PATCH 06/11] Add cache --- .../{trips => services}/_service.html.erb | 0 app/views/trips/_services.html.erb | 4 ++- app/views/trips/index.html.erb | 18 ++++++----- case-study.md | 32 +++++++++++++++++++ config/environments/development.rb | 16 +++++----- 5 files changed, 53 insertions(+), 17 deletions(-) rename app/views/{trips => services}/_service.html.erb (100%) diff --git a/app/views/trips/_service.html.erb b/app/views/services/_service.html.erb similarity index 100% rename from app/views/trips/_service.html.erb rename to app/views/services/_service.html.erb diff --git a/app/views/trips/_services.html.erb b/app/views/trips/_services.html.erb index 2de639fc..310bc8e5 100644 --- a/app/views/trips/_services.html.erb +++ b/app/views/trips/_services.html.erb @@ -1,6 +1,8 @@
  • Сервисы в автобусе:
    • <% services.each do |service| %> - <%= render "service", service: service %> + <% cache service do %> + <%= render service %> + <% end %> <% end %>
    diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb index a60bce41..ba2d1636 100644 --- a/app/views/trips/index.html.erb +++ b/app/views/trips/index.html.erb @@ -1,4 +1,4 @@ -

    +

    <%= "Автобусы #{@from.name} – #{@to.name}" %>

    @@ -6,11 +6,13 @@

    <% @trips.each do |trip| %> -
      - <%= render "trip", trip: trip %> - <% if trip.bus.services.present? %> - <%= render "services", services: trip.bus.services %> - <% end %> -
    - <%= render "delimiter" %> + <% cache trip do %> +
      + <%= render "trip", trip: trip %> + <% if trip.bus.services.any? %> + <%= render "services", services: trip.bus.services %> + <% end %> +
    + <%= render "delimiter" %> + <% end %> <% end %> diff --git a/case-study.md b/case-study.md index c5ea128a..e43d0393 100644 --- a/case-study.md +++ b/case-study.md @@ -63,3 +63,35 @@ Completed 200 OK in 13197ms (Views: 12532.7ms | ActiveRecord: 649.9ms) ``` Completed 200 OK in 13902ms (Views: 13840.6ms | ActiveRecord: 47.7ms) ``` + +Установили `rack-mini-profiler`. Обращаем внимание на то, что его включение в `initializer` добавляет добрых 10 секунд! + +``` +Completed 200 OK in 20086ms (Views: 20004.5ms | ActiveRecord: 64.6ms) +``` + +Видим, что много занимает рендеринг `_services`. + +Пробуем воспользоваться рельсовым рендерингом коллекции: +``` + <% render services %> +``` + +Видим результат! + +``` +Completed 200 OK in 14502ms (Views: 14465.4ms | ActiveRecord: 31.2ms) +``` + +Не совсем ясно, что можно улучшить с `services`. Смотрим в соседний по весу `_trip` и видим использование `present?`. Но ведь мы знаем, что `any?` в данном случае лучше! +``` +Completed 200 OK in 12502ms (Views: 12462.2ms | ActiveRecord: 33.8ms) +``` + +Учитывая большое кол-во данных, кажется, нам не обойтись без кэширования `partials`. +Добавили `cache` в темплейты, включили `rails dev:cache`. + +В результате имеем результат: +``` +Completed 200 OK in 979ms (Views: 939.0ms | ActiveRecord: 35.3ms) +``` diff --git a/config/environments/development.rb b/config/environments/development.rb index 903a8661..8468e80a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,12 +1,12 @@ 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 + # 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. From a766f7132fdd64b1c597f2f83f51a3a06f0bbd4a Mon Sep 17 00:00:00 2001 From: Udalov Igor Date: Mon, 30 Oct 2023 09:27:12 +0300 Subject: [PATCH 07/11] Add indexes --- case-study.md | 11 +++++++++++ db/migrate/20231030061817_add_name_index_to_cities.rb | 5 +++++ ...231030061925_add_bus_id_index_to_buses_services.rb | 5 +++++ db/migrate/20231030062020_add_indexes_to_trips.rb | 5 +++++ db/schema.rb | 6 +++++- 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20231030061817_add_name_index_to_cities.rb create mode 100644 db/migrate/20231030061925_add_bus_id_index_to_buses_services.rb create mode 100644 db/migrate/20231030062020_add_indexes_to_trips.rb diff --git a/case-study.md b/case-study.md index e43d0393..5eed040a 100644 --- a/case-study.md +++ b/case-study.md @@ -95,3 +95,14 @@ Completed 200 OK in 12502ms (Views: 12462.2ms | ActiveRecord: 33.8ms) ``` Completed 200 OK in 979ms (Views: 939.0ms | ActiveRecord: 35.3ms) ``` + +До сих пор мы (помимо `preload` и `any?`) занимались улучшением загрузки `partials`. +Что кажется верным, т.к. бд не видится боттлнеком в данном примере. Но все же добавим индексы: +``` +add_index :cities, :name, unique: true +add_index :buses_services, :bus_id +add_index :trips, [:from_id, :to_id] +``` + +Время снова улучшилось: +Completed 200 OK in 860ms (Views: 830.1ms | ActiveRecord: 23.0ms) diff --git a/db/migrate/20231030061817_add_name_index_to_cities.rb b/db/migrate/20231030061817_add_name_index_to_cities.rb new file mode 100644 index 00000000..abf16cc4 --- /dev/null +++ b/db/migrate/20231030061817_add_name_index_to_cities.rb @@ -0,0 +1,5 @@ +class AddNameIndexToCities < ActiveRecord::Migration[5.2] + def change + add_index :cities, :name, unique: true + end +end diff --git a/db/migrate/20231030061925_add_bus_id_index_to_buses_services.rb b/db/migrate/20231030061925_add_bus_id_index_to_buses_services.rb new file mode 100644 index 00000000..e66a0ad5 --- /dev/null +++ b/db/migrate/20231030061925_add_bus_id_index_to_buses_services.rb @@ -0,0 +1,5 @@ +class AddBusIdIndexToBusesServices < ActiveRecord::Migration[5.2] + def change + add_index :buses_services, :bus_id + end +end diff --git a/db/migrate/20231030062020_add_indexes_to_trips.rb b/db/migrate/20231030062020_add_indexes_to_trips.rb new file mode 100644 index 00000000..e85e12d2 --- /dev/null +++ b/db/migrate/20231030062020_add_indexes_to_trips.rb @@ -0,0 +1,5 @@ +class AddIndexesToTrips < ActiveRecord::Migration[5.2] + def change + add_index :trips, [:from_id, :to_id] + end +end diff --git a/db/schema.rb b/db/schema.rb index 96006cf7..68bd56c8 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: 2023_10_28_140206) do +ActiveRecord::Schema.define(version: 2023_10_30_062020) 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,10 +24,12 @@ 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" end create_table "cities", force: :cascade do |t| t.string "name" + t.index ["name"], name: "index_cities_on_name", unique: true end create_table "pghero_query_stats", force: :cascade do |t| @@ -51,6 +54,7 @@ t.integer "duration_minutes" t.integer "price_cents" t.integer "bus_id" + t.index ["from_id", "to_id"], name: "index_trips_on_from_id_and_to_id" end end From a70a371f4797fcd817844cc5a94d1d6a79b31429 Mon Sep 17 00:00:00 2001 From: Udalov Igor Date: Mon, 30 Oct 2023 17:44:47 +0300 Subject: [PATCH 08/11] Get rid of templates --- app/views/services/_service.html.erb | 1 - app/views/trips/_delimiter.html.erb | 1 - app/views/trips/_services.html.erb | 8 -------- app/views/trips/_trip.html.erb | 5 ----- app/views/trips/index.html.erb | 21 +++++++++++++++++---- case-study.md | 24 ++++++++++++++++++++++-- 6 files changed, 39 insertions(+), 21 deletions(-) delete mode 100644 app/views/services/_service.html.erb delete mode 100644 app/views/trips/_delimiter.html.erb delete mode 100644 app/views/trips/_services.html.erb delete mode 100644 app/views/trips/_trip.html.erb diff --git a/app/views/services/_service.html.erb b/app/views/services/_service.html.erb deleted file mode 100644 index 178ea8c0..00000000 --- a/app/views/services/_service.html.erb +++ /dev/null @@ -1 +0,0 @@ -
  • <%= "#{service.name}" %>
  • 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/_services.html.erb b/app/views/trips/_services.html.erb deleted file mode 100644 index 310bc8e5..00000000 --- a/app/views/trips/_services.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -
  • Сервисы в автобусе:
  • -
      - <% services.each do |service| %> - <% cache service do %> - <%= render service %> - <% end %> - <% end %> -
    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 @@ -
  • <%= "Отправление: #{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 ba2d1636..041284dc 100644 --- a/app/views/trips/index.html.erb +++ b/app/views/trips/index.html.erb @@ -8,11 +8,24 @@ <% @trips.each do |trip| %> <% cache trip do %>
      - <%= render "trip", trip: trip %> - <% if trip.bus.services.any? %> - <%= render "services", services: trip.bus.services %> +
    • <%= "Отправление: #{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}" %>
    • + + <% services = trip.bus.services %> + <% if services.any? %> +
    • Сервисы в автобусе:
    • +
        + <% services.each do |service| %> + <% cache service do %> +
      • <%= "#{service.name}" %>
      • + <% end %> + <% end %> +
      <% end %>
    - <%= render "delimiter" %> + ==================================================== <% end %> <% end %> diff --git a/case-study.md b/case-study.md index 5eed040a..52f029c9 100644 --- a/case-study.md +++ b/case-study.md @@ -104,5 +104,25 @@ add_index :buses_services, :bus_id add_index :trips, [:from_id, :to_id] ``` -Время снова улучшилось: -Completed 200 OK in 860ms (Views: 830.1ms | ActiveRecord: 23.0ms) +Так. Давайте все же считать кэширование "нечестным" способом оптимизации здесь, и посмотрим, что сможем "выжать" из страницы без него. + +Снимаем показатели: +``` +Completed 200 OK in 4403ms (Views: 4382.7ms | ActiveRecord: 15.7ms) +``` + +Много времени занимает `delimiter`. Видимо, подхватывание `partial` не бесплатно, и раз уж мы говорим об оптимизации, нам можно обойтись без отдельного темплейта в данном случае. + +``` +Completed 200 OK in 3043ms (Views: 3022.1ms | ActiveRecord: 16.9ms) +``` + +Что ж, пойдем дальше - и весь код `html` соберем в `index.html.erb`. + +Уже неплохой результат! +``` +Completed 200 OK in 455ms (Views: 434.0ms | ActiveRecord: 17.1ms) +Completed 200 OK in 473ms (Views: 449.4ms | ActiveRecord: 15.2ms) +``` + +Стремимся к 50 ms. From 9dfbaa041db18f8dad8af460deaf17eb9d8c458a Mon Sep 17 00:00:00 2001 From: Udalov Igor Date: Mon, 30 Oct 2023 18:55:07 +0300 Subject: [PATCH 09/11] Change start_time date --- app/views/trips/index.html.erb | 42 +++++++++---------- case-study.md | 5 +++ ...1030150533_change_trips_start_time_type.rb | 28 +++++++++++++ db/schema.rb | 5 +-- 4 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 db/migrate/20231030150533_change_trips_start_time_type.rb diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb index 041284dc..076eb391 100644 --- a/app/views/trips/index.html.erb +++ b/app/views/trips/index.html.erb @@ -1,31 +1,27 @@ -

    +

    <%= "Автобусы #{@from.name} – #{@to.name}" %>

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

    <% @trips.each do |trip| %> - <% cache trip do %> -
      -
    • <%= "Отправление: #{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}" %>
      • +
      • <%= "Прибытие: #{(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}" %>
      • - <% services = trip.bus.services %> - <% if services.any? %> -
      • Сервисы в автобусе:
      • -
          - <% services.each do |service| %> - <% cache service do %> -
        • <%= "#{service.name}" %>
        • - <% end %> - <% end %> -
        - <% end %> -
      - ==================================================== - <% end %> + <% services = trip.bus.services %> + <% if services.any? %> +
    • Сервисы в автобусе:
    • +
        + <% services.each do |service| %> +
      • <%= "#{service.name}" %>
      • + <% end %> +
      + <% end %> +
    + ==================================================== <% end %> diff --git a/case-study.md b/case-study.md index 52f029c9..8b8c1ba7 100644 --- a/case-study.md +++ b/case-study.md @@ -126,3 +126,8 @@ Completed 200 OK in 473ms (Views: 449.4ms | ActiveRecord: 15.2ms) ``` Стремимся к 50 ms. + +Заменили тип `start_time` на `time` в БД, стало немного лучше: +``` +Completed 200 OK in 397ms (Views: 381.6ms | ActiveRecord: 11.5ms) +``` diff --git a/db/migrate/20231030150533_change_trips_start_time_type.rb b/db/migrate/20231030150533_change_trips_start_time_type.rb new file mode 100644 index 00000000..1214c212 --- /dev/null +++ b/db/migrate/20231030150533_change_trips_start_time_type.rb @@ -0,0 +1,28 @@ +class ChangeTripsStartTimeType < ActiveRecord::Migration[5.2] + def up + # add a temporary column + add_column :trips, :start_time_time, :time + + # add the the current start_time as datetime to the temporary column for each entry + Trip.all.each do |trip| + trip.update(start_time_time: Time.parse(trip.start_time)) + end + + # drop the old time column + remove_column :trips, :start_time + + # rename the temporary column to start_time + rename_column :trips, :start_time_time, :start_time + end + + def down + add_column :trips, :start_time_time, :time + + Trip.all.each do |trip| + trip.update(start_time_time: Time.parse(trip.start_time).strftime('%H:%M')) + end + + remove_column :trips, :start_time + rename_column :trips, :start_time_time, :start_time + end +end diff --git a/db/schema.rb b/db/schema.rb index 68bd56c8..846bef40 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,9 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_10_30_062020) do +ActiveRecord::Schema.define(version: 2023_10_30_150533) 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| @@ -50,10 +49,10 @@ create_table "trips", force: :cascade do |t| t.integer "from_id" t.integer "to_id" - t.string "start_time" t.integer "duration_minutes" t.integer "price_cents" t.integer "bus_id" + t.time "start_time" t.index ["from_id", "to_id"], name: "index_trips_on_from_id_and_to_id" end From fb8fa19b6697feb47119dbb6b90b2c4cf40d5151 Mon Sep 17 00:00:00 2001 From: Udalov Igor Date: Mon, 30 Oct 2023 20:03:15 +0300 Subject: [PATCH 10/11] Finish --- Gemfile | 3 ++- Gemfile.lock | 17 +++++++++++++++++ app/views/trips/index.html.erb | 2 +- bin/setup | 3 +++ case-study.md | 4 ++-- spec/spec_helper.rb | 3 +++ 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index d48c3474..afbfdfd5 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,7 @@ gem 'bootsnap', '>= 1.1.0', require: false group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + gem 'bullet' end group :development do @@ -18,11 +19,11 @@ group :development do gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' gem 'meta_request' - gem 'bullet' end group :test do gem 'rspec-rails' + gem 'capybara' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem diff --git a/Gemfile.lock b/Gemfile.lock index 715c20c7..98c00628 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -42,6 +42,8 @@ GEM i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) + addressable (2.8.5) + public_suffix (>= 2.0.2, < 6.0) arel (9.0.0) bindex (0.8.1) bootsnap (1.16.0) @@ -51,6 +53,15 @@ GEM activesupport (>= 3.0.0) uniform_notifier (~> 1.11) byebug (11.1.3) + capybara (3.36.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) concurrent-ruby (1.2.2) crass (1.0.6) date (3.3.3) @@ -74,6 +85,7 @@ GEM net-pop net-smtp marcel (1.0.2) + matrix (0.4.2) meta_request (0.7.4) rack-contrib (>= 1.1, < 3) railties (>= 3.0.0, < 7.1) @@ -98,6 +110,7 @@ GEM pg (1.5.4) pghero (2.8.3) activerecord (>= 5) + public_suffix (5.0.3) puma (3.12.6) racc (1.7.1) rack (2.2.8) @@ -136,6 +149,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) + regexp_parser (2.8.2) rspec-core (3.12.2) rspec-support (~> 3.12.0) rspec-expectations (3.12.3) @@ -175,6 +189,8 @@ GEM websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) PLATFORMS ruby @@ -183,6 +199,7 @@ DEPENDENCIES bootsnap (>= 1.1.0) bullet byebug + capybara listen (>= 3.0.5, < 3.2) meta_request pg (>= 0.18, < 2.0) diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb index 076eb391..0fb8d681 100644 --- a/app/views/trips/index.html.erb +++ b/app/views/trips/index.html.erb @@ -7,7 +7,7 @@ <% @trips.each do |trip| %>
      -
    • <%= "Отправление: #{trip.start_time}" %>
    • +
    • <%= "Отправление: #{trip.start_time.strftime('%H:%M')}" %>
    • <%= "Прибытие: #{(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}коп." %>
    • diff --git a/bin/setup b/bin/setup index affa6766..41d4d776 100755 --- a/bin/setup +++ b/bin/setup @@ -34,6 +34,9 @@ chdir APP_ROOT do puts "\n== Loading data from fixtures/large.json ==" system! 'bin/rake reload_json[fixtures/large.json]' + # puts "\n== Loading data from fixtures/example.json ==" + # system! 'bin/rake reload_json[fixtures/example.json]' + puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' diff --git a/case-study.md b/case-study.md index 8b8c1ba7..6e3a4829 100644 --- a/case-study.md +++ b/case-study.md @@ -125,9 +125,9 @@ Completed 200 OK in 455ms (Views: 434.0ms | ActiveRecord: 17.1ms) Completed 200 OK in 473ms (Views: 449.4ms | ActiveRecord: 15.2ms) ``` -Стремимся к 50 ms. - Заменили тип `start_time` на `time` в БД, стало немного лучше: ``` Completed 200 OK in 397ms (Views: 381.6ms | ActiveRecord: 11.5ms) ``` + +Если бы стояла задача улучшать дальше (например, добраться-таки до 50 ms), я бы в первую очередь смотрел в сторону пагинации для этой страницы, наверное - но это в рамках данной задачи нам вряд ли нужно. diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 327b58ea..6f3422b5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,6 +13,9 @@ # it. # # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + +require 'capybara/rspec' + RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest From 0d2eeb2f531c5308e05f66a5f4be619068668c56 Mon Sep 17 00:00:00 2001 From: Udalov Igor Date: Tue, 31 Oct 2023 18:43:48 +0300 Subject: [PATCH 11/11] Restore test --- bin/setup | 8 ++++---- spec/features/trips_spec.rb | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 spec/features/trips_spec.rb diff --git a/bin/setup b/bin/setup index 41d4d776..2ba8ba2d 100755 --- a/bin/setup +++ b/bin/setup @@ -31,11 +31,11 @@ chdir APP_ROOT do # puts "\n== Loading data from fixtures/small.json ==" # system! 'bin/rake reload_json[fixtures/small.json]' - puts "\n== Loading data from fixtures/large.json ==" - system! 'bin/rake reload_json[fixtures/large.json]' + # puts "\n== Loading data from fixtures/large.json ==" + # system! 'bin/rake reload_json[fixtures/large.json]' - # puts "\n== Loading data from fixtures/example.json ==" - # system! 'bin/rake reload_json[fixtures/example.json]' + puts "\n== Loading data from fixtures/example.json ==" + system! 'bin/rake reload_json[fixtures/example.json]' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' diff --git a/spec/features/trips_spec.rb b/spec/features/trips_spec.rb new file mode 100644 index 00000000..7b5f6b15 --- /dev/null +++ b/spec/features/trips_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +RSpec.describe 'displays trips' do + let(:example_path) { 'fixtures/example.json' } + let(:example_data) { JSON.parse File.read(example_path) } + let(:good_trips) { example_data.select { |t| t['from'] == 'Самара' && t['to'] == 'Москва' } } + let(:bad_trips) { bad_trips - good_trips } + let(:link) { URI.encode('/автобусы/Самара/Москва') } + + before { `bin/rake reload_json[#{example_path}]` } + + it 'displays relevant trips with correct details' do + visit link + expect(page).to have_content("В расписании #{good_trips.count} рейсов") + good_trips.each do |trip| + expect(page).to have_content("Отправление: #{trip['start_time']}") + expect(page).to have_content("Прибытие: #{(Time.parse(trip['start_time']) + trip['duration_minutes'].minutes).strftime('%H:%M')}") + expect(page).to have_content("В пути: #{trip['duration_minutes'] / 60}ч. #{trip['duration_minutes'] % 60}мин.") + expect(page).to have_content("Цена: #{trip['price_cents'] / 100}р. #{trip['price_cents'] % 100}коп.") + expect(page).to have_content("Автобус: #{trip['bus']['model']} №#{trip['bus']['number']}") + end + end +end