diff --git a/.gitignore b/.gitignore
index 18b43c9..be342df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@
# Ignore master key for decrypting credentials and more.
/config/master.key
+.env
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..2626e15
--- /dev/null
+++ b/.rspec
@@ -0,0 +1,3 @@
+--color
+--require spec_helper
+--require rails_helper
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
index 33017fd..c62f8b4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,10 +7,19 @@ gem 'rails', '~> 5.2.3'
gem 'pg', '>= 0.18', '< 2.0'
gem 'puma', '~> 3.11'
gem 'bootsnap', '>= 1.1.0', require: false
+gem 'oj'
+gem 'activerecord-import'
+gem 'newrelic_rpm'
+gem 'pghero'
+gem 'pg_query', '>= 0.9.0'
+gem 'ruby-prof'
+gem 'strong_migrations'
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 'meta_request'
+ gem 'dotenv-rails'
end
group :development do
@@ -20,6 +29,8 @@ group :development do
end
group :test do
+ gem 'rspec-rails', '~> 3.8'
+ gem 'rails-controller-testing'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
diff --git a/Gemfile.lock b/Gemfile.lock
index eb22e16..5187268 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -33,6 +33,8 @@ GEM
activemodel (= 5.2.3)
activesupport (= 5.2.3)
arel (>= 9.0)
+ activerecord-import (1.0.1)
+ activerecord (>= 3.2)
activestorage (5.2.3)
actionpack (= 5.2.3)
activerecord (= 5.2.3)
@@ -43,13 +45,19 @@ GEM
minitest (~> 5.1)
tzinfo (~> 1.1)
arel (9.0.0)
- bindex (0.6.0)
- bootsnap (1.4.2)
+ bindex (0.7.0)
+ bootsnap (1.4.4)
msgpack (~> 1.0)
builder (3.2.3)
byebug (11.0.1)
+ callsite (0.0.11)
concurrent-ruby (1.1.5)
crass (1.0.4)
+ diff-lcs (1.3)
+ dotenv (2.7.2)
+ dotenv-rails (2.7.2)
+ dotenv (= 2.7.2)
+ railties (>= 3.2, < 6.1)
erubi (1.8.0)
ffi (1.10.0)
globalid (0.4.2)
@@ -67,18 +75,29 @@ GEM
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
+ meta_request (0.7.0)
+ callsite (~> 0.0, >= 0.0.11)
+ rack-contrib (>= 1.1, < 3)
+ railties (>= 3.0.0, < 7)
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)
+ msgpack (1.2.10)
+ newrelic_rpm (6.3.0.355)
nio4r (2.3.1)
- nokogiri (1.10.2)
+ nokogiri (1.10.3)
mini_portile2 (~> 2.4.0)
+ oj (3.7.12)
pg (1.1.4)
+ pg_query (1.1.0)
+ pghero (2.2.0)
+ activerecord
puma (3.12.1)
- rack (2.0.6)
+ rack (2.0.7)
+ rack-contrib (2.1.0)
+ rack (~> 2.0)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.3)
@@ -94,6 +113,10 @@ GEM
bundler (>= 1.3.0)
railties (= 5.2.3)
sprockets-rails (>= 2.0.0)
+ rails-controller-testing (1.0.4)
+ actionpack (>= 5.0.1.x)
+ actionview (>= 5.0.1.x)
+ activesupport (>= 5.0.1.x)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
@@ -109,6 +132,24 @@ GEM
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
+ rspec-core (3.8.0)
+ rspec-support (~> 3.8.0)
+ rspec-expectations (3.8.3)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.8.0)
+ rspec-mocks (3.8.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.8.0)
+ rspec-rails (3.8.2)
+ actionpack (>= 3.0)
+ activesupport (>= 3.0)
+ railties (>= 3.0)
+ rspec-core (~> 3.8.0)
+ rspec-expectations (~> 3.8.0)
+ rspec-mocks (~> 3.8.0)
+ rspec-support (~> 3.8.0)
+ rspec-support (3.8.0)
+ ruby-prof (0.17.0)
ruby_dep (1.5.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
@@ -117,6 +158,8 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
+ strong_migrations (0.3.1)
+ activerecord (>= 3.2.0)
thor (0.20.3)
thread_safe (0.3.6)
tzinfo (1.2.5)
@@ -134,12 +177,23 @@ PLATFORMS
ruby
DEPENDENCIES
+ activerecord-import
bootsnap (>= 1.1.0)
byebug
+ dotenv-rails
listen (>= 3.0.5, < 3.2)
+ meta_request
+ newrelic_rpm
+ oj
pg (>= 0.18, < 2.0)
+ pg_query (>= 0.9.0)
+ pghero
puma (~> 3.11)
rails (~> 5.2.3)
+ rails-controller-testing
+ rspec-rails (~> 3.8)
+ ruby-prof
+ strong_migrations
tzinfo-data
web-console (>= 3.3.0)
diff --git a/app/models/bus.rb b/app/models/bus.rb
index 1dcc54c..1ee02a7 100644
--- a/app/models/bus.rb
+++ b/app/models/bus.rb
@@ -12,8 +12,10 @@ class Bus < ApplicationRecord
'Газель',
].freeze
- has_many :trips
- has_and_belongs_to_many :services, join_table: :buses_services
+ has_many :trips, dependent: :destroy
+ # has_and_belongs_to_many :services, join_table: :buses_services
+ has_many :buses_services, class_name: 'BusesService'
+ 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 0000000..6ec494e
--- /dev/null
+++ b/app/models/buses_service.rb
@@ -0,0 +1,4 @@
+class BusesService < ApplicationRecord
+ belongs_to :bus
+ belongs_to :service
+end
\ No newline at end of file
diff --git a/app/models/service.rb b/app/models/service.rb
index 9cbb2a3..d1da573 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -12,7 +12,9 @@ class Service < ApplicationRecord
'Можно не печатать билет',
].freeze
- has_and_belongs_to_many :buses, join_table: :buses_services
+ # has_and_belongs_to_many :buses, join_table: :buses_services
+ has_many :buses_services, class_name: 'BusesService'
+ has_many :buses, through: :buses_services
validates :name, presence: true
validates :name, inclusion: { in: SERVICES }
diff --git a/app/models/trip.rb b/app/models/trip.rb
index 9d63dff..66b45fb 100644
--- a/app/models/trip.rb
+++ b/app/models/trip.rb
@@ -1,9 +1,9 @@
class Trip < ApplicationRecord
HHMM_REGEXP = /([0-1][0-9]|[2][0-3]):[0-5][0-9]/
- belongs_to :from, class_name: 'City'
- belongs_to :to, class_name: 'City'
- belongs_to :bus
+ belongs_to :from, class_name: 'City', foreign_key: :from_id, optional: true
+ belongs_to :to, class_name: 'City', foreign_key: :to_id, optional: true
+ belongs_to :bus, foreign_key: :bus_id, optional: true
validates :from, presence: true
validates :to, presence: true
diff --git a/app/scripts/ruby_prof.rb b/app/scripts/ruby_prof.rb
new file mode 100644
index 0000000..ab77cb2
--- /dev/null
+++ b/app/scripts/ruby_prof.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'ruby-prof'
+require_relative '../services/json_importer'
+
+RubyProf.measure_mode = RubyProf::WALL_TIME
+
+INITIAL_DATA_FILE = 'fixtures/small.json'
+OUTPUT_DIR = 'tmp/data'
+
+
+def flat_profile
+ run_profiler do |result|
+ printer = RubyProf::FlatPrinterWithLineNumbers.new(result)
+ printer.print(File.open("#{OUTPUT_DIR}/ruby_prof_flat_demo.txt", 'w+'))
+ end
+end
+
+def graph_profile
+ run_profiler do |result|
+ printer = RubyProf::GraphHtmlPrinter.new(result)
+ printer.print(File.open("#{OUTPUT_DIR}/ruby_prof_graph_demo.html", "w+"))
+ end
+end
+
+def callstack_profile
+ run_profiler do |result|
+ printer = RubyProf::CallStackPrinter.new(result)
+ printer.print(File.open("#{OUTPUT_DIR}/ruby_prof_callstack_demo.html", "w+"))
+ end
+end
+
+def calltree_profile
+ run_profiler do |result|
+ printer = RubyProf::CallTreePrinter.new(result)
+ printer.print(path: OUTPUT_DIR, profile: 'profile')
+ end
+end
+
+
+def run_profiler
+ RubyProf.measure_mode = RubyProf::WALL_TIME
+ result = RubyProf.profile { JsonImporter.new.import_json_to_db(file_path: INITIAL_DATA_FILE) }
+ yield result
+end
+
+# flat_profile
+# graph_profile
+# callstack_profile
+# calltree_profile
\ No newline at end of file
diff --git a/app/services/json_importer.rb b/app/services/json_importer.rb
new file mode 100644
index 0000000..557b50d
--- /dev/null
+++ b/app/services/json_importer.rb
@@ -0,0 +1,79 @@
+ require 'oj'
+
+class JsonImporter
+ attr_accessor :services_names_hash, :cities_names_hash, :buses_hash, :trips
+ def initialize
+ @services_names_hash = {}
+ @cities_names_hash = {}
+ @buses_hash = {}
+ @trips = []
+ end
+
+ def import_json_to_db(file_path:)
+ # byebug
+ json = Oj.load_file(file_path)
+
+ ActiveRecord::Base.transaction do
+ delete_existing_records
+ create_cities_and_services(json)
+ create_buses_with_services(json)
+ create_trips(json)
+ end
+ end
+
+ def create_cities_and_services(json)
+ json.each do |trip|
+ cities_names_hash[trip['from']] ||= City.new(name: trip['from'])
+ cities_names_hash[trip['to']] ||= City.new(name: trip['to'])
+ trip['bus']['services'].each do |service|
+ services_names_hash[service] = Service.new(name: service)
+ end
+ end
+ # byebug
+ City.import cities_names_hash.values, syncronize: true, raise_error: true
+ # byebug
+ Service.import services_names_hash.values, syncronize: true, raise_error: true
+ end
+
+ def create_buses_with_services(json)
+ buses_numbers = []
+ json.each do |trip|
+ next if buses_numbers.include?(trip['bus']['number'])
+ bus = Bus.new(
+ number: trip['bus']['number'],
+ model: trip['bus']['model']
+ )
+ bus.services = services_names_hash.values_at(*trip['bus']['services'])
+
+ buses_hash[trip['bus']['number']] = bus
+ buses_numbers << trip['bus']['number']
+ end
+ # byebug
+ Bus.import buses_hash.values, recursive: true, syncronize: true,raise_error: true
+ end
+
+ def create_trips(json)
+ json.each do |trip|
+ from = cities_names_hash[trip['from']]
+ to = cities_names_hash[trip['to']]
+ bus = buses_hash[trip['bus']['number']]
+ trips << Trip.new(
+ from: from,
+ to: to,
+ bus: bus,
+ start_time: trip['start_time'],
+ duration_minutes: trip['duration_minutes'],
+ price_cents: trip['price_cents']
+ )
+ end
+ Trip.import trips, raise_error: true
+ end
+
+ def delete_existing_records
+ City.delete_all
+ Bus.delete_all
+ Service.delete_all
+ Trip.delete_all
+ ActiveRecord::Base.connection.execute('delete from buses_services;')
+ end
+end
diff --git a/app/views/trips/_delimiter.html.erb b/app/views/trips/_delimiter.html.erb
deleted file mode 100644
index 3f845ad..0000000
--- a/app/views/trips/_delimiter.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-====================================================
diff --git a/app/views/trips/_service.html.erb b/app/views/trips/_service.html.erb
deleted file mode 100644
index 178ea8c..0000000
--- a/app/views/trips/_service.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-
<%= "#{service.name}" %>
diff --git a/app/views/trips/_services.html.erb b/app/views/trips/_services.html.erb
deleted file mode 100644
index 2de639f..0000000
--- a/app/views/trips/_services.html.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-Сервисы в автобусе:
-
- <% services.each do |service| %>
- <%= render "service", service: service %>
- <% end %>
-
diff --git a/app/views/trips/_trip.html.erb b/app/views/trips/_trip.html.erb
index fa1de9a..c04dcf0 100644
--- a/app/views/trips/_trip.html.erb
+++ b/app/views/trips/_trip.html.erb
@@ -1,5 +1,16 @@
-<%= "Отправление: #{trip.start_time}" %>
-<%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %>
-<%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %>
-<%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %>
-<%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %>
+
+ - <%= "Отправление: #{trip.start_time}" %>
+ - <%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %>
+ - <%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %>
+ - <%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %>
+ - <%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %>
+ <% if trip.bus.services.present? %>
+ - Сервисы в автобусе:
+
+ <% trip.bus.services.each do |service| %>
+ - <%= "#{service.name}" %>
+ <% end %>
+
+ <% end %>
+
+====================================================
diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb
index a60bce4..0df45dc 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 "trip", trip: trip %>
- <% if trip.bus.services.present? %>
- <%= render "services", services: trip.bus.services %>
- <% end %>
-
- <%= render "delimiter" %>
-<% end %>
+<%= render partial: 'trip', collection: @trips %>
diff --git a/config/application.rb b/config/application.rb
index 9c33109..e094ea9 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -15,5 +15,10 @@ class Application < Rails::Application
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
+ config.autoload_paths << Rails.root.join('services')
+
+ config.after_initialize do |app|
+ app.config.paths.add 'app/services', :eager_load => true
+ end
end
end
diff --git a/config/newrelic.yml b/config/newrelic.yml
new file mode 100644
index 0000000..f724f14
--- /dev/null
+++ b/config/newrelic.yml
@@ -0,0 +1,47 @@
+#
+# This file configures the New Relic Agent. New Relic monitors Ruby, Java,
+# .NET, PHP, Python, Node, and Go applications with deep visibility and low
+# overhead. For more information, visit www.newrelic.com.
+#
+# Generated May 06, 2019
+#
+# This configuration file is custom generated for Selfemployed_57
+#
+# For full documentation of agent configuration options, please refer to
+# https://docs.newrelic.com/docs/agents/ruby-agent/installation-configuration/ruby-agent-configuration
+
+common: &default_settings
+ # Required license key associated with your New Relic account.
+ license_key: ENV["NEWRELIC_LICENSE_KEY"]
+ # c4e0921d5f0dc7a300da9c9c5d58f6e94f26b8f1
+
+ # Your application name. Renaming here affects where data displays in New
+ # Relic. For more details, see https://docs.newrelic.com/docs/apm/new-relic-apm/maintenance/renaming-applications
+ app_name: Bus Tours
+
+ # To disable the agent regardless of other settings, uncomment the following:
+ # agent_enabled: false
+
+ # Logging level for log/newrelic_agent.log
+ log_level: info
+
+
+# Environment-specific settings are in this section.
+# RAILS_ENV or RACK_ENV (as appropriate) is used to determine the environment.
+# If your application has other named environments, configure them here.
+development:
+ <<: *default_settings
+ app_name: Bus Tours (Development)
+ monitor_mode: true
+
+test:
+ <<: *default_settings
+ # It doesn't make sense to report to New Relic from automated test runs.
+ monitor_mode: false
+
+staging:
+ <<: *default_settings
+ app_name: Bus Tours (Staging)
+
+production:
+ <<: *default_settings
diff --git a/config/routes.rb b/config/routes.rb
index a2da6a7..4cc70a9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,6 @@
Rails.application.routes.draw do
+ mount PgHero::Engine, at: "pghero"
+
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
get "/" => "statistics#index"
get "автобусы/:from/:to" => "trips#index"
diff --git a/db/migrate/20190506035213_add_indicies_to_trips.rb b/db/migrate/20190506035213_add_indicies_to_trips.rb
new file mode 100644
index 0000000..e91caeb
--- /dev/null
+++ b/db/migrate/20190506035213_add_indicies_to_trips.rb
@@ -0,0 +1,7 @@
+class AddIndiciesToTrips < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+ def change
+ add_index :trips, %i[from_id to_id], algorithm: :concurrently
+ add_index :trips, :start_time, algorithm: :concurrently
+ end
+end
diff --git a/db/migrate/20190506044704_add_foreign_key_to_trips.rb b/db/migrate/20190506044704_add_foreign_key_to_trips.rb
new file mode 100644
index 0000000..6fc40b8
--- /dev/null
+++ b/db/migrate/20190506044704_add_foreign_key_to_trips.rb
@@ -0,0 +1,6 @@
+class AddForeignKeyToTrips < ActiveRecord::Migration[5.2]
+ def change
+ add_foreign_key :trips, :cities, column: :from_id, on_delete: :cascade
+ add_foreign_key :trips, :cities, column: :to_id, on_delete: :cascade
+ end
+end
diff --git a/db/migrate/20190506051147_add_more_indicies.rb b/db/migrate/20190506051147_add_more_indicies.rb
new file mode 100644
index 0000000..f6514d4
--- /dev/null
+++ b/db/migrate/20190506051147_add_more_indicies.rb
@@ -0,0 +1,8 @@
+class AddMoreIndicies < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+ def change
+ add_index :cities, :name, unique: true, algorithm: :concurrently
+ add_index :buses, :number, unique: true, algorithm: :concurrently
+ add_index :services, :name, unique: true, algorithm: :concurrently
+ end
+end
diff --git a/db/migrate/20190512223139_add_foreign_key_to_buses_services.rb b/db/migrate/20190512223139_add_foreign_key_to_buses_services.rb
new file mode 100644
index 0000000..57225a1
--- /dev/null
+++ b/db/migrate/20190512223139_add_foreign_key_to_buses_services.rb
@@ -0,0 +1,6 @@
+class AddForeignKeyToBusesServices < ActiveRecord::Migration[5.2]
+ def change
+ add_foreign_key :buses_services, :buses, column: :bus_id, on_delete: :cascade
+ add_foreign_key :buses_services, :services, column: :service_id, on_delete: :cascade
+ end
+end
diff --git a/db/migrate/20190513023411_remove_fk_from_buses_services.rb b/db/migrate/20190513023411_remove_fk_from_buses_services.rb
new file mode 100644
index 0000000..78927ce
--- /dev/null
+++ b/db/migrate/20190513023411_remove_fk_from_buses_services.rb
@@ -0,0 +1,6 @@
+class RemoveFkFromBusesServices < ActiveRecord::Migration[5.2]
+ def change
+ remove_foreign_key :buses_services, column: :bus_id
+ remove_foreign_key :buses_services, column: :service_id
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f6921e4..dbf98ae 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,14 +10,17 @@
#
# 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: 2019_05_13_023411) do
# These are extensions that must be enabled in order to support this database
+ enable_extension "pg_stat_statements"
+ enable_extension "pgcrypto"
enable_extension "plpgsql"
create_table "buses", force: :cascade do |t|
t.string "number"
t.string "model"
+ t.index ["number"], name: "index_buses_on_number", unique: true
end
create_table "buses_services", force: :cascade do |t|
@@ -27,10 +30,23 @@
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|
+ 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"
+ t.index ["name"], name: "index_services_on_name", unique: true
end
create_table "trips", force: :cascade do |t|
@@ -40,6 +56,10 @@
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"
+ t.index ["start_time"], name: "index_trips_on_start_time"
end
+ add_foreign_key "trips", "cities", column: "from_id", on_delete: :cascade
+ add_foreign_key "trips", "cities", column: "to_id", on_delete: :cascade
end
diff --git a/lib/tasks/metric_measurement.rake b/lib/tasks/metric_measurement.rake
new file mode 100644
index 0000000..b612532
--- /dev/null
+++ b/lib/tasks/metric_measurement.rake
@@ -0,0 +1,36 @@
+require 'benchmark'
+
+desc 'measure performance for optimization rake import_json_benchmark[fixtures/small.json]'
+task :import_json_benchmark, [:file_name] => :environment do |_task, args|
+ result = Benchmark.measure do
+ puts "\n== Loading data from #{args.file_name} =="
+ Benchmark.measure { Rake::Task['reload_json'].invoke(*args) }
+ end
+
+ puts result
+end
+
+# CPU time, system CPU time, the sum of the user and system CPU times, and the elapsed real time. The unit of time is seconds.
+
+# initial results
+# 2.654352 0.956313 13.612620 ( 14.781368)
+
+# add indicies to trips
+# 12.471791 0.888674 13.360465 ( 14.467896)
+
+# add indicies to cities, services and buses
+# 11.874181 0.867256 12.743072 ( 13.819131)
+
+# == Loading data from fixtures/small.json ==
+# Reload complete!
+# 14.654087 0.681433 15.337232 ( 16.270607)
+
+# add Oj and activerecord-import
+# == Loading data from fixtures/small.json ==
+# Reload complete!
+# 1.585816 0.067304 1.655319 ( 1.651922)
+
+
+# Loading data from fixtures/large.json ==
+# Reload complete!
+# 33.660827 0.582401 34.245360 (40.213537)
\ No newline at end of file
diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake
index 540fe87..e8616b5 100644
--- a/lib/tasks/utils.rake
+++ b/lib/tasks/utils.rake
@@ -1,34 +1,8 @@
+
# Наивная загрузка данных из json-файла в БД
# rake reload_json[fixtures/small.json]
+desc 'Import data from json file into database rake reload_json[fixtures/small.json]'
task :reload_json, [:file_name] => :environment do |_task, args|
- json = JSON.parse(File.read(args.file_name))
-
- ActiveRecord::Base.transaction do
- City.delete_all
- Bus.delete_all
- Service.delete_all
- Trip.delete_all
- ActiveRecord::Base.connection.execute('delete from buses_services;')
-
- json.each do |trip|
- from = City.find_or_create_by(name: trip['from'])
- to = City.find_or_create_by(name: trip['to'])
- services = []
- trip['bus']['services'].each do |service|
- s = Service.find_or_create_by(name: service)
- services << s
- end
- bus = Bus.find_or_create_by(number: trip['bus']['number'])
- bus.update(model: trip['bus']['model'], services: services)
-
- Trip.create!(
- from: from,
- to: to,
- bus: bus,
- start_time: trip['start_time'],
- duration_minutes: trip['duration_minutes'],
- price_cents: trip['price_cents'],
- )
- end
- end
-end
+ JsonImporter.new.import_json_to_db(file_path: args.file_name)
+ puts 'Reload complete!'
+end
\ No newline at end of file
diff --git a/metrics/after/slow_queries_after.png b/metrics/after/slow_queries_after.png
new file mode 100644
index 0000000..5c3a0ef
Binary files /dev/null and b/metrics/after/slow_queries_after.png differ
diff --git a/metrics/after/slowest_queries_after.png b/metrics/after/slowest_queries_after.png
new file mode 100644
index 0000000..10263cf
Binary files /dev/null and b/metrics/after/slowest_queries_after.png differ
diff --git a/metrics/after/transactions_after.png b/metrics/after/transactions_after.png
new file mode 100644
index 0000000..f4827d5
Binary files /dev/null and b/metrics/after/transactions_after.png differ
diff --git a/metrics/before/Screen Shot 2019-05-05 at 10.07.13 PM.png b/metrics/before/Screen Shot 2019-05-05 at 10.07.13 PM.png
new file mode 100644
index 0000000..046122f
Binary files /dev/null and b/metrics/before/Screen Shot 2019-05-05 at 10.07.13 PM.png differ
diff --git a/metrics/before/Screen Shot 2019-05-05 at 10.07.22 PM.png b/metrics/before/Screen Shot 2019-05-05 at 10.07.22 PM.png
new file mode 100644
index 0000000..e75827d
Binary files /dev/null and b/metrics/before/Screen Shot 2019-05-05 at 10.07.22 PM.png differ
diff --git a/metrics/before/front_end_services_before.png b/metrics/before/front_end_services_before.png
new file mode 100644
index 0000000..8c97933
Binary files /dev/null and b/metrics/before/front_end_services_before.png differ
diff --git a/metrics/before/pg_hero_advice.png b/metrics/before/pg_hero_advice.png
new file mode 100644
index 0000000..eda590c
Binary files /dev/null and b/metrics/before/pg_hero_advice.png differ
diff --git a/metrics/before/slow_queries.png b/metrics/before/slow_queries.png
new file mode 100644
index 0000000..8f048df
Binary files /dev/null and b/metrics/before/slow_queries.png differ
diff --git a/metrics/before/slowest_queries.png b/metrics/before/slowest_queries.png
new file mode 100644
index 0000000..7789dbb
Binary files /dev/null and b/metrics/before/slowest_queries.png differ
diff --git a/metrics/case_study_english.md b/metrics/case_study_english.md
new file mode 100644
index 0000000..ffe035f
--- /dev/null
+++ b/metrics/case_study_english.md
@@ -0,0 +1,149 @@
+# We had a problem of loading large json files as well as rendering web pages fast with the given amount of data.
+
+## Main goal is:
+ - Optimize program to load large.json file within 1(one) minute.
+ - Find out how we ca n render schedule pages faster
+
+## Tools I used to solve the problem:
+ - gem pghero
+ - newrelic
+ - gem activerecord-import
+ - gem strong_migrations
+ - benchmark
+ - apache benchmark testing
+ - siege
+
+
+
+## For given feedback loop on database level I did the following:
+ - desided to start with `small.json` in order to increase feedback loop
+ - extracted logic from rake tasks in it's own class so that it's easier to test.
+ - wrote test for loadeing `.json` files
+ - by implementing any changes on database level I made sure tests are passing and my metrics from new relic and benchmark metrics show better results
+
+## Let\'s look at `Benchmark.measure` numbers showing the following:
+- CPU time
+- system CPU time
+- the sum of the user and system CPU time
+- the elapsed real time. The unit of time is seconds.
+
+#### Initial results of running `be rake 'import_json_benchmark[fixtures/small.json]'`:
+```
+ 2.654352 0.956313 13.612620 (14.781368)
+```
+
+
+
+
+
+#### Results of running `be rake 'import_json_benchmark[fixtures/small.json]'` after optimization and using `gem oj`, `gem activerecord-import`, adding necessary indicies:
+```
+1.585816 0.067304 1.655319 (1.651922)
+```
+
+#### This improvement allowed me to load `fixtures/large.json` file with the following metrics:
+```
+ 33.660827 0.582401 34.245360 (40.213537)
+```
+
+
+### Page load metrics
+### Newrelic
+
+#### Apache benchmark
+1. With indicies but without front end optimization
+
+```
+ab -n 10 -c 1 http://localhost:3000/автобусы/Самара/Москва
+
+
+Server Software:
+Server Hostname: localhost
+Server Port: 3000
+
+Document Path: /%D0%B0%D0%B2%D1%82%D0%BE%D0%B1%D1%83%D1%81%D1%8B/%D0%A1%D0%B0%D0%BC%D0%B0%D1%80%D0%B0/%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0
+Document Length: 538780 bytes
+
+Concurrency Level: 1
+Time taken for tests: 247.233 seconds
+Complete requests: 10
+Failed requests: 0
+Total transferred: 5395342 bytes
+HTML transferred: 5387800 bytes
+Requests per second: 0.04 [#/sec] (mean)
+Time per request: 24723.295 [ms] (mean)
+Time per request: 24723.295 [ms] (mean, across all concurrent requests)
+Transfer rate: 21.31 [Kbytes/sec] received
+
+Connection Times (ms)
+ min mean[+/-sd] median max
+Connect: 0 0 0.0 0 0
+Processing: 22301 24723 948.8 25043 25566
+Waiting: 22300 24723 948.9 25043 25566
+Total: 22301 24723 948.8 25043 25566
+
+Percentage of the requests served within a certain time (ms)
+ 50% 25043
+ 66% 25186
+ 75% 25199
+ 80% 25265
+ 90% 25566
+ 95% 25566
+ 98% 25566
+ 99% 25566
+ 100% 25566 (longest request)
+ ````
+
+
+2. After removing partials and adding collections
+```
+ab -n 10 -c 1 http://localhost:3000/автобусы/Самара/Москва
+Server Software:
+Server Hostname: localhost
+Server Port: 3000
+
+Document Path: /%D0%B0%D0%B2%D1%82%D0%BE%D0%B1%D1%83%D1%81%D1%8B/%D0%A1%D0%B0%D0%BC%D0%B0%D1%80%D0%B0/%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0
+Document Length: 569667 bytes
+
+Concurrency Level: 1
+Time taken for tests: 45.169 seconds
+Complete requests: 10
+Failed requests: 0
+Total transferred: 5704204 bytes
+HTML transferred: 5696670 bytes
+Requests per second: 0.22 [#/sec] (mean)
+Time per request: 4516.931 [ms] (mean)
+Time per request: 4516.931 [ms] (mean, across all concurrent requests)
+Transfer rate: 123.33 [Kbytes/sec] received
+
+Connection Times (ms)
+ min mean[+/-sd] median max
+Connect: 0 0 0.0 0 0
+Processing: 3838 4517 448.1 4523 5575
+Waiting: 3838 4516 448.1 4522 5575
+Total: 3838 4517 448.1 4523 5575
+
+Percentage of the requests served within a certain time (ms)
+ 50% 4523
+ 66% 4573
+ 75% 4583
+ 80% 4725
+ 90% 5575
+ 95% 5575
+ 98% 5575
+ 99% 5575
+ 100% 5575 (longest request)
+ ````
+
+As we can see from the results above after getting rid of partials and rendering partial with collection
+- Time taken for tests reduced from 247.233 seconds to 45.169 seconds
+- Time per request reducded from 24723.295 [ms] (mean) to 4516.931 [ms] (mean)
+- Requests per second increased from 0.04 [#/sec] to 0.22 [#/sec] (mean)
+
+
+
+On the screenshots below we can observe that slowest queries and transactions metrics were improved as well:
+
+
+
+
\ No newline at end of file
diff --git a/spec/controllers/trips_controller_spec.rb b/spec/controllers/trips_controller_spec.rb
new file mode 100644
index 0000000..2f89d36
--- /dev/null
+++ b/spec/controllers/trips_controller_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+RSpec.describe TripsController, type: :controller do
+ describe 'GET #index' do
+ before do
+ JsonImporter.new().import_json_to_db(file_path: 'spec/fixtures/data.json')
+ get :index, params: { from: 'Сочи', to: 'Тула'}
+ end
+
+ it 'populates correct data' do
+ Trip.where(from: 'Сочи', to: 'Тула').find_each do |trip|
+ expect(response.body).to include?("Отправление: #{trip.start_time}")
+ expect(response.body).to include?("Автобус: #{trip.bus.model} №#{trip.bus.number}")
+ end
+ end
+
+ it 'renders index view' do
+ expect(response).to render_template :index
+ end
+ end
+end
diff --git a/spec/fixtures/data.json b/spec/fixtures/data.json
new file mode 100644
index 0000000..ac21537
--- /dev/null
+++ b/spec/fixtures/data.json
@@ -0,0 +1,89 @@
+[{
+ "from": "Сочи",
+ "to": "Тула",
+ "start_time": "16:11",
+ "duration_minutes": 83,
+ "price_cents": 23354,
+ "bus": {
+ "number": "229",
+ "model": "Икарус",
+ "services": ["Ремни безопасности", "Кондиционер общий", "Кондиционер Индивидуальный", "Телевизор индивидуальный", "Стюардесса", "Можно не печатать билет"]
+ }
+}, {
+ "from": "Самара",
+ "to": "Самара",
+ "start_time": "13:13",
+ "duration_minutes": 572,
+ "price_cents": 83861,
+ "bus": {
+ "number": "912",
+ "model": "Вольво",
+ "services": ["WiFi", "Работающий туалет", "Телевизор индивидуальный", "Стюардесса", "Можно не печатать билет"]
+ }
+}, {
+ "from": "Красноярск",
+ "to": "Волгоград",
+ "start_time": "13:48",
+ "duration_minutes": 186,
+ "price_cents": 80288,
+ "bus": {
+ "number": "584",
+ "model": "ГАЗ",
+ "services": ["Кондиционер общий"]
+ }
+}, {
+ "from": "Рыбинск",
+ "to": "Саратов",
+ "start_time": "15:13",
+ "duration_minutes": 271,
+ "price_cents": 6803,
+ "bus": {
+ "number": "739",
+ "model": "УАЗ",
+ "services": ["WiFi", "Работающий туалет", "Ремни безопасности", "Кондиционер общий", "Кондиционер Индивидуальный", "Телевизор общий", "Телевизор индивидуальный", "Стюардесса", "Можно не печатать билет"]
+ }
+}, {
+ "from": "Тула",
+ "to": "Саратов",
+ "start_time": "18:24",
+ "duration_minutes": 67,
+ "price_cents": 87845,
+ "bus": {
+ "number": "811",
+ "model": "Вольво",
+ "services": ["Работающий туалет"]
+ }
+}, {
+ "from": "Москва",
+ "to": "Самара",
+ "start_time": "16:15",
+ "duration_minutes": 214,
+ "price_cents": 62185,
+ "bus": {
+ "number": "387",
+ "model": "Буханка",
+ "services": ["Туалет", "Ремни безопасности"]
+ }
+}, {
+ "from": "Тула",
+ "to": "Саратов",
+ "start_time": "18:27",
+ "duration_minutes": 269,
+ "price_cents": 95089,
+ "bus": {
+ "number": "974",
+ "model": "ГАЗ",
+ "services": ["WiFi"]
+ }
+}, {
+ "from": "Сочи",
+ "to": "Самара",
+ "start_time": "21:10",
+ "duration_minutes": 479,
+ "price_cents": 26945,
+ "bus": {
+ "number": "379",
+ "model": "Газель",
+ "services": ["WiFi", "Туалет", "Работающий туалет", "Кондиционер общий", "Кондиционер Индивидуальный", "Телевизор индивидуальный", "Стюардесса", "Можно не печатать билет"]
+ }
+}]
\ No newline at end of file
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
new file mode 100644
index 0000000..b5fed83
--- /dev/null
+++ b/spec/rails_helper.rb
@@ -0,0 +1,61 @@
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+require 'rails_helper'
+ENV['RAILS_ENV'] ||= 'test'
+require File.expand_path('../../config/environment', __FILE__)
+# 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')].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
+
+ # 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/services/json_importer_spec.rb b/spec/services/json_importer_spec.rb
new file mode 100644
index 0000000..a01ea25
--- /dev/null
+++ b/spec/services/json_importer_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+describe 'Services::JsonImporter' do
+ describe '#import_json_to_db', focus: true do
+ it 'imports eight records of Trip into db', focus: true do
+ expect { import_file }.to change(Trip, :count).by(8)
+ end
+
+ it 'has correct data' do
+ import_file
+ actual_result = Trip.first.to_h
+ expected_result = {
+ from: "Сочи",
+ to: "Тула",
+ start_time: "16:11",
+ duration_minutes: 83,
+ price_cents: 23354,
+ bus: {
+ number: "229",
+ model: "Икарус",
+ services: ["Ремни безопасности", "Кондиционер общий", "Кондиционер Индивидуальный", "Телевизор индивидуальный", "Стюардесса", "Можно не печатать билет"]
+ }
+ }
+ end
+
+ # expect(actual_result).to eq(expected_result)
+ end
+
+ def import_file
+ JsonImporter.new().import_json_to_db(file_path: 'spec/fixtures/data.json')
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..7954854
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,97 @@
+# 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 http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ config.warnings = false
+ # 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:
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#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