From 3f5374bfc3c2e7145777827da0396396e9bd550a Mon Sep 17 00:00:00 2001 From: Aleksei Iaroslavtsev Date: Sat, 11 May 2024 13:40:53 +0200 Subject: [PATCH 1/3] adapt utils --- Gemfile | 17 ++-- Gemfile.lock | 150 ------------------------------------ app/models/buses_service.rb | 6 ++ db/schema.rb | 52 ++++++------- lib/tasks/utils.rake | 70 ++++++++++++----- 5 files changed, 94 insertions(+), 201 deletions(-) delete mode 100644 Gemfile.lock create mode 100644 app/models/buses_service.rb diff --git a/Gemfile b/Gemfile index e20b1260..52076400 100644 --- a/Gemfile +++ b/Gemfile @@ -1,26 +1,31 @@ +# frozen_string_literal: true + source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '2.6.3' +ruby '3.2.4' -gem 'rails', '~> 5.2.3' +gem 'activerecord-import' +gem 'bootsnap', '>= 1.1.0', require: false gem 'pg', '>= 0.18', '< 2.0' gem 'puma', '~> 3.11' -gem 'bootsnap', '>= 1.1.0', require: false +gem 'rails', '~> 6.1' +gem 'stackprof' +gem 'vernier', '~> 1.0' 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 'byebug', platforms: %i[mri mingw x64_mingw] end 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 'web-console', '>= 3.3.0' end group :test do end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index fccf6f5f..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,150 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - actioncable (5.2.3) - actionpack (= 5.2.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) - 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) - 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) - 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) - 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) - 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) - 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) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.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) - 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) - 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) - ffi (~> 1.0) - ruby_dep (1.5.0) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) - 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) - bindex (>= 0.4.0) - railties (>= 5.0) - websocket-driver (0.7.0) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) - -PLATFORMS - ruby - -DEPENDENCIES - bootsnap (>= 1.1.0) - byebug - listen (>= 3.0.5, < 3.2) - pg (>= 0.18, < 2.0) - puma (~> 3.11) - rails (~> 5.2.3) - tzinfo-data - web-console (>= 3.3.0) - -RUBY VERSION - ruby 2.6.3p62 - -BUNDLED WITH - 2.0.2 diff --git a/app/models/buses_service.rb b/app/models/buses_service.rb new file mode 100644 index 00000000..976a0c10 --- /dev/null +++ b/app/models/buses_service.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class BusesService < ApplicationRecord + belongs_to :bus + belongs_to :service +end diff --git a/db/schema.rb b/db/schema.rb index f6921e45..d63a75e1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,45 +1,45 @@ +# frozen_string_literal: true + # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # 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: 20_190_330_193_044) do # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" + enable_extension 'plpgsql' - create_table "buses", force: :cascade do |t| - t.string "number" - t.string "model" + create_table 'buses', force: :cascade do |t| + t.string 'number' + t.string 'model' end - create_table "buses_services", force: :cascade do |t| - t.integer "bus_id" - t.integer "service_id" + create_table 'buses_services', force: :cascade do |t| + t.integer 'bus_id' + t.integer 'service_id' end - create_table "cities", force: :cascade do |t| - t.string "name" + create_table 'cities', force: :cascade do |t| + t.string 'name' end - create_table "services", force: :cascade do |t| - t.string "name" + create_table 'services', force: :cascade do |t| + t.string 'name' end - 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" + 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' end - end diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index 540fe871..1c1fa0b2 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -1,34 +1,66 @@ +# frozen_string_literal: true + # Наивная загрузка данных из json-файла в БД # rake reload_json[fixtures/small.json] + task :reload_json, [:file_name] => :environment do |_task, args| - json = JSON.parse(File.read(args.file_name)) + # Vernier.run(out: "time_profile.json") do + profile = StackProf.run(mode: :wall, raw: true) do + # Rails.logger.level = Logger::DEBUG + # ActiveRecord::Base.logger = Logger.new STDOUT - 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']) + json = JSON.parse(File.read(args.file_name)) + + ActiveRecord::Base.connection.execute <<-SQL + ALTER TABLE buses_services DROP CONSTRAINT IF EXISTS for_upsert; + ALTER TABLE buses_services + ADD CONSTRAINT for_upsert UNIQUE (bus_id, service_id); + SQL + + ActiveRecord::Base.transaction do + cities = {} + ac_services = {} + ac_buses = {} + trips = [] services = [] - trip['bus']['services'].each do |service| - s = Service.find_or_create_by(name: service) - services << s + + json.each do |trip| + cities[trip['from']] ||= City.create!(name: trip['from']) + cities[trip['to']] ||= City.create!(name: trip['to']) + ac_buses[trip['bus']['number']] ||= Bus.create(number: trip['bus']['number'], model: trip['bus']['model']) + + trip['bus']['services'].each do |service| + ac_services[service] ||= Service.create!(name: service) + s = [ac_buses[trip['bus']['number']].id, ac_services[service].id] + services << s + end + + trips << [cities[trip['from']].id, cities[trip['to']].id, trip['start_time'], trip['duration_minutes'], + trip['price_cents'], ac_buses[trip['bus']['number']].id] + + next unless trips.length == 1000 + + BusesService.import %i[bus_id service_id], services, validate: false, on_duplicate_key_ignore: true + Trip.import %i[from_id to_id start_time duration_minutes price_cents bus_id], trips, validate: false + trips = [] + services = [] 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'], - ) + + BusesService.import %i[bus_id service_id], services, validate: false, on_duplicate_key_ignore: true + Trip.import %i[from_id to_id start_time duration_minutes price_cents bus_id], trips, validate: false + puts format('MEMORY USAGE: %d MB', (`ps -o rss= -p #{Process.pid}`.to_i / 1024)) end end + + ActiveRecord::Base.connection.execute <<-SQL + ALTER TABLE buses_services DROP CONSTRAINT IF EXISTS for_upsert; + SQL + File.write('stackprof.json', JSON.generate(profile)) + # end end From 2b7e3bfde7dc4b0f464fbee31164af12a5b5b816 Mon Sep 17 00:00:00 2001 From: Aleksei Iaroslavtsev Date: Sat, 11 May 2024 14:21:26 +0200 Subject: [PATCH 2/3] remove n+1 and adapt view --- .ruby-version | 2 +- Gemfile | 3 +++ app/controllers/trips_controller.rb | 2 +- app/views/trips/index.html.erb | 17 +++++++++--- config/environments/development.rb | 9 +++++++ config/routes.rb | 2 +- db/schema.rb | 42 ++++++++++++++--------------- lib/tasks/utils.rake | 6 ++--- 8 files changed, 52 insertions(+), 31 deletions(-) diff --git a/.ruby-version b/.ruby-version index ec1cf33c..351227fc 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.3 +3.2.4 diff --git a/Gemfile b/Gemfile index 52076400..d6f663bc 100644 --- a/Gemfile +++ b/Gemfile @@ -12,6 +12,9 @@ gem 'puma', '~> 3.11' gem 'rails', '~> 6.1' gem 'stackprof' gem 'vernier', '~> 1.0' +gem 'rack-mini-profiler' +gem 'bullet' +gem 'memory_profiler' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb index acb38be2..1431925c 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.includes(bus: :services).where(from: @from, to: @to).order(:start_time) end end diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb index a60bce41..c941e529 100644 --- a/app/views/trips/index.html.erb +++ b/app/views/trips/index.html.erb @@ -7,10 +7,19 @@ <% @trips.each do |trip| %> - <%= render "delimiter" %> + ==================================================== <% end %> diff --git a/config/environments/development.rb b/config/environments/development.rb index 1311e3e4..8468e80a 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/routes.rb b/config/routes.rb index a2da6a7b..a44d0a82 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,5 @@ Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html - get "/" => "statistics#index" + # get "/" => "statistics#index" get "автобусы/:from/:to" => "trips#index" end diff --git a/db/schema.rb b/db/schema.rb index d63a75e1..09e263b6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -12,34 +10,36 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20_190_330_193_044) do +ActiveRecord::Schema.define(version: 2019_03_30_193044) do + # These are extensions that must be enabled in order to support this database - enable_extension 'plpgsql' + enable_extension "plpgsql" - create_table 'buses', force: :cascade do |t| - t.string 'number' - t.string 'model' + create_table "buses", force: :cascade do |t| + t.string "number" + t.string "model" end - create_table 'buses_services', force: :cascade do |t| - t.integer 'bus_id' - t.integer 'service_id' + create_table "buses_services", force: :cascade do |t| + t.integer "bus_id" + t.integer "service_id" end - create_table 'cities', force: :cascade do |t| - t.string 'name' + create_table "cities", force: :cascade do |t| + t.string "name" end - create_table 'services', force: :cascade do |t| - t.string 'name' + create_table "services", force: :cascade do |t| + t.string "name" end - 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' + 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" end + end diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index 1c1fa0b2..623a97e9 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -5,7 +5,7 @@ task :reload_json, [:file_name] => :environment do |_task, args| # Vernier.run(out: "time_profile.json") do - profile = StackProf.run(mode: :wall, raw: true) do + # profile = StackProf.run(mode: :wall, raw: true) do # Rails.logger.level = Logger::DEBUG # ActiveRecord::Base.logger = Logger.new STDOUT @@ -54,9 +54,9 @@ task :reload_json, [:file_name] => :environment do |_task, args| BusesService.import %i[bus_id service_id], services, validate: false, on_duplicate_key_ignore: true Trip.import %i[from_id to_id start_time duration_minutes price_cents bus_id], trips, validate: false - puts format('MEMORY USAGE: %d MB', (`ps -o rss= -p #{Process.pid}`.to_i / 1024)) + # puts format('MEMORY USAGE: %d MB', (`ps -o rss= -p #{Process.pid}`.to_i / 1024)) end - end + # end ActiveRecord::Base.connection.execute <<-SQL ALTER TABLE buses_services DROP CONSTRAINT IF EXISTS for_upsert; From 6ac1542c0d385dd5ed416979cf8f7ec60928aea8 Mon Sep 17 00:00:00 2001 From: Aleksei Iaroslavtsev Date: Sat, 11 May 2024 21:25:29 +0200 Subject: [PATCH 3/3] add report --- Gemfile | 4 +- Gemfile.lock | 206 +++++++++++++++++++++++++++++++++++++++++++++++ case-study.md | 33 ++++++++ config/routes.rb | 1 + db/schema.rb | 5 +- img.png | Bin 0 -> 32287 bytes 6 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 Gemfile.lock create mode 100644 case-study.md create mode 100644 img.png diff --git a/Gemfile b/Gemfile index d6f663bc..61ebaeaf 100644 --- a/Gemfile +++ b/Gemfile @@ -12,9 +12,11 @@ gem 'puma', '~> 3.11' gem 'rails', '~> 6.1' gem 'stackprof' gem 'vernier', '~> 1.0' -gem 'rack-mini-profiler' +# gem 'rack-mini-profiler' gem 'bullet' gem 'memory_profiler' +gem "pghero" +gem "pg_query", ">= 2" group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..7fc33310 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,206 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (6.1.7.7) + actionpack (= 6.1.7.7) + activesupport (= 6.1.7.7) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (6.1.7.7) + actionpack (= 6.1.7.7) + activejob (= 6.1.7.7) + activerecord (= 6.1.7.7) + activestorage (= 6.1.7.7) + activesupport (= 6.1.7.7) + mail (>= 2.7.1) + actionmailer (6.1.7.7) + actionpack (= 6.1.7.7) + actionview (= 6.1.7.7) + activejob (= 6.1.7.7) + activesupport (= 6.1.7.7) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (6.1.7.7) + actionview (= 6.1.7.7) + activesupport (= 6.1.7.7) + rack (~> 2.0, >= 2.0.9) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.1.7.7) + actionpack (= 6.1.7.7) + activerecord (= 6.1.7.7) + activestorage (= 6.1.7.7) + activesupport (= 6.1.7.7) + nokogiri (>= 1.8.5) + actionview (6.1.7.7) + activesupport (= 6.1.7.7) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (6.1.7.7) + activesupport (= 6.1.7.7) + globalid (>= 0.3.6) + activemodel (6.1.7.7) + activesupport (= 6.1.7.7) + activerecord (6.1.7.7) + activemodel (= 6.1.7.7) + activesupport (= 6.1.7.7) + activerecord-import (1.6.0) + activerecord (>= 4.2) + activestorage (6.1.7.7) + actionpack (= 6.1.7.7) + activejob (= 6.1.7.7) + activerecord (= 6.1.7.7) + activesupport (= 6.1.7.7) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (6.1.7.7) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + bindex (0.8.1) + bootsnap (1.18.3) + msgpack (~> 1.2) + builder (3.2.4) + bullet (7.1.6) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11) + byebug (11.1.3) + concurrent-ruby (1.2.3) + crass (1.0.6) + date (3.3.4) + erubi (1.12.0) + ffi (1.16.3) + globalid (1.2.1) + activesupport (>= 6.1) + google-protobuf (4.26.1-arm64-darwin) + rake (>= 13) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + memory_profiler (1.0.1) + method_source (1.1.0) + mini_mime (1.1.5) + minitest (5.22.3) + msgpack (1.7.2) + net-imap (0.4.11) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol + nio4r (2.7.3) + nokogiri (1.16.4-arm64-darwin) + racc (~> 1.4) + pg (1.5.6) + pg_query (5.1.0) + google-protobuf (>= 3.22.3) + pghero (3.4.1) + activerecord (>= 6) + puma (3.12.6) + racc (1.7.3) + rack (2.2.9) + rack-mini-profiler (3.3.1) + rack (>= 1.2.0) + rack-test (2.1.0) + rack (>= 1.3) + rails (6.1.7.7) + actioncable (= 6.1.7.7) + actionmailbox (= 6.1.7.7) + actionmailer (= 6.1.7.7) + actionpack (= 6.1.7.7) + actiontext (= 6.1.7.7) + actionview (= 6.1.7.7) + activejob (= 6.1.7.7) + activemodel (= 6.1.7.7) + activerecord (= 6.1.7.7) + activestorage (= 6.1.7.7) + activesupport (= 6.1.7.7) + bundler (>= 1.15.0) + railties (= 6.1.7.7) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (6.1.7.7) + actionpack (= 6.1.7.7) + activesupport (= 6.1.7.7) + method_source + rake (>= 12.2) + thor (~> 1.0) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + sprockets (4.2.1) + concurrent-ruby (~> 1.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + stackprof (0.2.26) + thor (1.3.1) + timeout (0.4.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uniform_notifier (1.16.0) + vernier (1.0.1) + web-console (3.7.0) + actionview (>= 5.0) + activemodel (>= 5.0) + bindex (>= 0.4.0) + railties (>= 5.0) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.6.13) + +PLATFORMS + arm64-darwin-23 + +DEPENDENCIES + activerecord-import + bootsnap (>= 1.1.0) + bullet + byebug + listen (>= 3.0.5, < 3.2) + memory_profiler + pg (>= 0.18, < 2.0) + pg_query (>= 2) + pghero + puma (~> 3.11) + rack-mini-profiler + rails (~> 6.1) + stackprof + tzinfo-data + vernier (~> 1.0) + web-console (>= 3.3.0) + +RUBY VERSION + ruby 3.2.4p170 + +BUNDLED WITH + 2.4.19 diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..c22828e4 --- /dev/null +++ b/case-study.md @@ -0,0 +1,33 @@ +# Case-study Report + +## Актуальная проблема + +Нужно оптимизировать импорт данных и отображение рассписаний: +- `rake reload_json[fixtures/large.json]` должнен выполняться в пределах минуты; +- загрузка страницы `автобусы/Самара/Москва` должна укалывать в 200 мс; + + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы измеряется время выполнения импорта данных для файла large и скорость закрузки стратрицы. + + +## Главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался инструментами rack-mini-profiler, memory-profiler, pghero и bullet. + + +### Hаходки +- Использовал gem activerecord-import и следовал рекомендациям по импорту данных: создал вспомогательные справочники для `cities`, `buses` и использовал `on_duplicate_key_ignore` в связке с `CONSTRAINT for_upsert UNIQUE (bus_id, service_id)` + * Время выполнения импорта файла `fixtures/large.json` составило 24 секунды +- Профилирование показало, что значительное время уходит на `ActiveModel::AttributeMethods#method_missing` при формировании данных для импорта. Для ускорения данные для импорта формировались с id объекта из справочтика, а не с объектом из справочника. + * ![img.png](img.png) + * Время выполнения импорта файла `fixtures/large.json` составило 18 секунд вместо 24 секунд + +- Гем bullet показал наличие n+1 при загрузки страницы `автобусы/Самара/Москва` + * Первым шагом было добалвение загрузки связанных объектов Bus с помощью `includes` для Trip. Время загрузки значительно не изменилось: было 2095.1ms стало 2290ms + * Вторым шагом было добавлние загрузки связанных объектов Services с помощью `includes` для Trip и Bus. Время загрузки уменьшилось до 548ms +- Логи показали много обращений к шаблон для рендера + * Создание общего шаблона для рендера без вложенных шаблонов уменьшило время загрузки страницы `автобусы/Самара/Москва` до 178ms +- Добавление индексов согласно рекомендациям PG Hero сократило время загрузки до 156ms + +## Результаты +В результате проделанной оптимизации удалось обработать большой файл с данными за целевое время и значительно ускорить загрузку страницы diff --git a/config/routes.rb b/config/routes.rb index a44d0a82..4526e018 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/db/schema.rb b/db/schema.rb index 09e263b6..d96f7b5c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_03_30_193044) do +ActiveRecord::Schema.define(version: 2024_05_11_124734) do # These are extensions that must be enabled in order to support this database + enable_extension "pg_stat_statements" enable_extension "plpgsql" create_table "buses", force: :cascade do |t| @@ -23,6 +24,7 @@ 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| @@ -40,6 +42,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 diff --git a/img.png b/img.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b7a9c95d42c7e2c9ff1b73c7d52f19828af25f GIT binary patch literal 32287 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 L$19Fqy7hkm#9Hpj literal 0 HcmV?d00001