-
Notifications
You must be signed in to change notification settings - Fork 12
optimizations #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
optimizations #14
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| class TripsController < ApplicationController | ||
| def index | ||
| @from = City.find_by_name!(params[:from]) | ||
| @to = City.find_by_name!(params[:to]) | ||
| @trips = Trip.where(from: @from, to: @to).order(:start_time) | ||
| from = City.find_by_name!(params[:from]) | ||
| to = City.find_by_name!(params[:to]) | ||
| @trips = Trip.includes(bus: :services).where(from: from, to: to).order(:start_time) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| class BusService < ApplicationRecord | ||
| self.table_name = "buses_services" | ||
|
|
||
| belongs_to :bus | ||
| belongs_to :service | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| class Importer | ||
| attr_reader :json | ||
|
|
||
| def initialize(json) | ||
| @json = json | ||
| end | ||
|
|
||
| def call | ||
| ActiveRecord::Base.transaction do | ||
| reset_db! | ||
| import_cities | ||
| import_services | ||
| import_busses | ||
|
|
||
| buses = Bus.pluck(:number, :id).to_h | ||
| cities = City.pluck(:name, :id).to_h | ||
| link_services(buses) | ||
| import_trips(buses, cities) | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def reset_db! | ||
| City.delete_all | ||
| Bus.delete_all | ||
| Service.delete_all | ||
| Trip.delete_all | ||
| BusService.delete_all | ||
| end | ||
|
|
||
| def import_cities | ||
| cities = Set.new | ||
| json.each do |trip| | ||
| cities << [trip['from']] << [trip['to']] | ||
| end | ||
| City.import! [:name], cities.to_a | ||
| end | ||
|
|
||
| def import_services | ||
| services = Set.new | ||
| json.each do |trip| | ||
| services.merge trip['bus']['services'].map { |el| [el] } | ||
| end | ||
| Service.import! [:name], services.to_a | ||
| end | ||
|
|
||
| def import_busses | ||
| buses = Set.new | ||
| json.each do |trip| | ||
| buses << [trip['bus']['number'], trip['bus']['model']] | ||
| end | ||
| Bus.import! [:number, :model], buses.to_a | ||
| end | ||
|
|
||
| def link_services(buses) | ||
| services = Service.pluck(:name, :id).to_h | ||
| joins = Set.new | ||
| json.each do |trip| | ||
| bid = buses[trip['bus']['number']] | ||
| joins.merge(trip['bus']['services'].map { |service| [bid, services[service]] }) | ||
| end | ||
| BusService.import! [:bus_id, :service_id], joins.to_a | ||
| end | ||
|
|
||
| def import_trips(buses, cities) | ||
| trips = Set.new | ||
| columns = [:from_id, :to_id, :bus_id, :start_time, :duration_minutes, :price_cents] | ||
| json.each do |trip| | ||
| from_id = cities[trip['from']] | ||
| to_id = cities[trip['to']] | ||
| bus_id = buses[trip['bus']['number']] | ||
| trips.add [ | ||
| from_id, | ||
| to_id, | ||
| bus_id, | ||
| trip['start_time'], | ||
| trip['duration_minutes'], | ||
| trip['price_cents'], | ||
| ] | ||
| end | ||
| Trip.import! columns, trips.to_a | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,10 @@ | ||
| <li><%= "Отправление: #{trip.start_time}" %></li> | ||
| <li><%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %></li> | ||
| <li><%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %></li> | ||
| <li><%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %></li> | ||
| <li><%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %></li> | ||
| <ul> | ||
| <li><%= "Отправление: #{trip.start_time}" %></li> | ||
| <li><%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %></li> | ||
| <li><%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %></li> | ||
| <li><%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %></li> | ||
| <li><%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %></li> | ||
| <% if trip.bus.services.any? %> | ||
| <%= render "services", services: trip.bus.services %> | ||
| <% end %> | ||
| </ul> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,7 @@ | ||
| <h1> | ||
| <%= "Автобусы #{@from.name} – #{@to.name}" %> | ||
| <%= "Автобусы #{params[:from]} – #{params[:to]}" %> | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ловко 👍 |
||
| </h1> | ||
| <h2> | ||
| <%= "В расписании #{@trips.count} рейсов" %> | ||
| <%= "В расписании #{@trips.size} рейсов" %> | ||
| </h2> | ||
|
|
||
| <% @trips.each do |trip| %> | ||
| <ul> | ||
| <%= render "trip", trip: trip %> | ||
| <% if trip.bus.services.present? %> | ||
| <%= render "services", services: trip.bus.services %> | ||
| <% end %> | ||
| </ul> | ||
| <%= render "delimiter" %> | ||
| <% end %> | ||
| <%= render partial: 'trip', collection: @trips, spacer_template: "delimiter", cached: true %> | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Плюсик за |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # Case-study оптимизации | ||
|
|
||
| ## Актуальная проблема | ||
| 1. Медленная загрузка данных в базу | ||
| 2. Медленная загрузка страницы с расписанием | ||
|
|
||
| ## Формирование метрики | ||
| Для расчета времени загрузки данных в базу использовался ```Benchmark.realtime``` | ||
| Для измерения скорости загрузки страницы использовался ```gem rack-mini-profiler``` | ||
|
|
||
| ## Feedback-Loop | ||
| В качестве референсного файла использовался fixtures/small.json, по результатам обработки которого уже принималось решение относительно эффективности того или иного метода оптимизации. | ||
| Эффективность оптимизаций по скорости загрузки страницы оценивалась на основе метрик ```gem rack-mini-profiler``` | ||
|
|
||
| ### Загрузка данных в базу | ||
| ## Находка №1 | ||
| Для обработки каждой json записи инициализируется несколько объектов и каждый из них в отдельной транзакции вставляется в базу данных, что крайне неэффективно на больших объемах. Было принято решение использовать ```gem activerecord-import```, для массовой вставки данных | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
|
||
| ## Находка №2 | ||
| Используется устаревший метод ```has_and_belongs_to_many```. Использовав связную модель через ```has many through:```, появляется возможность более эффективно использовать ```gem activerecord import``` | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
|
||
| ## Находка №3 | ||
| Используется не самая эффективная библиотека для работы с json. Было принято решение использовать ```gem oj```, для более эффективной обработки | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Тут бы хорошо добавить инф-ю про эффект замены |
||
|
|
||
| ### Загрузка стрaницы | ||
| Замеры проводились после загрузки файла ```fixtures/large.json``` | ||
| Исходя из результатов ```gem rack-mini-profiler```  можно сделать следующие выводы: | ||
|
|
||
| ## Находка №1 | ||
| Огромное количество запросов к БД и большое количество N+1 запросов. Принято решение использовать includes для подгрузки ассоциаций и ликвидации лишних запросов. | ||
|
|
||
| ## Находка №2 | ||
| Большое количество времени занимает отрисовка partial. Было принято решение кешировать результаты | ||
|
|
||
| ## Находка №3 | ||
| Проанализировав sql, стало понятно, что не используются индексы на некоторых таблицах | ||
|  | ||
|  | ||
| Добавив соответствующие индексы удалось существенно ускорить выполнение запросов | ||
|  | ||
|  | ||
|
|
||
| ## Результаты | ||
|  | ||
|  | ||
|
|
||
| Благодаря оптимизациям удалось снизить скорость загрузки страницы до 420ms и загрузить требуемый файл за 17сек | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| if Rails.env.development? | ||
| require "rack-mini-profiler" | ||
|
|
||
| # initialization is skipped so trigger it | ||
| Rack::MiniProfilerRails.initialize!(Rails.application) | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| class AddIndexToTrips < ActiveRecord::Migration[5.2] | ||
| disable_ddl_transaction! | ||
|
|
||
| def change | ||
| add_index :trips, [:from_id, :to_id], algorithm: :concurrently | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 плюсик за |
||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| class AddIndexToBusesServices < ActiveRecord::Migration[5.2] | ||
| disable_ddl_transaction! | ||
|
|
||
| def change | ||
| add_index :buses_services, :bus_id, algorithm: :concurrently | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,14 @@ | ||
| # Наивная загрузка данных из json-файла в БД | ||
| # rake reload_json[fixtures/small.json] | ||
| task :reload_json, [:file_name] => :environment do |_task, args| | ||
| json = JSON.parse(File.read(args.file_name)) | ||
|
|
||
| ActiveRecord::Base.transaction do | ||
| City.delete_all | ||
| Bus.delete_all | ||
| Service.delete_all | ||
| Trip.delete_all | ||
| ActiveRecord::Base.connection.execute('delete from buses_services;') | ||
| require 'benchmark' | ||
| require 'oj' | ||
|
|
||
| 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 | ||
| task :reload_json, [:file_name] => :environment do |_task, args| | ||
| json = Oj.load(File.read(args.file_name)) | ||
| p 'Start' | ||
| time = Benchmark.realtime do | ||
| Importer.new(json).call | ||
| end | ||
| p 'Done' | ||
| p time | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Интересная идея с
Set!По-моему первый раз встречается среди домашних работ этого задания.