Skip to content

Commit c536943

Browse files
committed
first part
1 parent 7151f82 commit c536943

File tree

7 files changed

+191
-45
lines changed

7 files changed

+191
-45
lines changed

app/models/bus.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ class Bus < ApplicationRecord
1313
].freeze
1414

1515
has_many :trips
16-
has_and_belongs_to_many :services, join_table: :buses_services
16+
has_many :buses_services
17+
has_many :services, through: :buses_services
1718

1819
validates :number, presence: true, uniqueness: true
1920
validates :model, inclusion: { in: MODELS }

app/models/buses_service.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class BusesService < ApplicationRecord
2+
belongs_to :bus
3+
belongs_to :service
4+
end

app/models/service.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ class Service < ApplicationRecord
1212
'Можно не печатать билет',
1313
].freeze
1414

15-
has_and_belongs_to_many :buses, join_table: :buses_services
15+
has_many :buses_services
16+
has_many :buses, through: :buses_services
1617

1718
validates :name, presence: true
1819
validates :name, inclusion: { in: SERVICES }

app/services/data_loader.rb

Lines changed: 94 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,105 @@
1+
# frozen_string_literal: true
2+
13
class DataLoader
4+
TRIPS_COMMAND =
5+
"copy trips (from_id, to_id, start_time, duration_minutes, price_cents, bus_id) from stdin with csv delimiter ';'"
6+
CITIES_COMMAND = "copy cities (id, name) from stdin with csv delimiter ';'"
7+
BUSES_COMMAND = "copy buses (id, number, model) from stdin with csv delimiter ';'"
8+
SERVICES_COMMAND = "copy services (id, name) from stdin with csv delimiter ';'"
9+
BUSES_SERVICES_COMMAND = "copy buses_services (bus_id, service_id) from stdin with csv delimiter ';'"
10+
11+
def self.load(filename)
12+
new(filename).load
13+
end
14+
15+
def initialize(filename)
16+
@stream = JsonStreamer.stream(filename)
17+
@cities = {}
18+
@services = {}
19+
@buses = {}
20+
@buses_services = []
21+
end
22+
23+
def load
24+
ActiveRecord::Base.transaction do
25+
clean_database
26+
connection = ActiveRecord::Base.connection.raw_connection
27+
28+
ActiveRecord::Base.connection.raw_connection.copy_data TRIPS_COMMAND do
29+
stream.each do |trip|
30+
from = city(trip['from'])
31+
to = city(trip['to'])
32+
33+
if buses[trip['bus']['number']].nil?
34+
bus_services = []
35+
trip['bus']['services'].each do |name|
36+
bus_services << service(name)
37+
end
238

3-
class << self
4-
def load(filename)
5-
ActiveRecord::Base.transaction do
6-
clean_database
7-
8-
JsonStreamer.stream(filename).each do |trip|
9-
from = City.find_or_create_by(name: trip['from'])
10-
to = City.find_or_create_by(name: trip['to'])
11-
services = []
12-
trip['bus']['services'].each do |service|
13-
s = Service.find_or_create_by(name: service)
14-
services << s
39+
create_bus(trip['bus']['number'], trip['bus']['model'])
40+
create_bus_services(buses[trip['bus']['number']], bus_services)
1541
end
16-
bus = Bus.find_or_create_by(number: trip['bus']['number'])
17-
bus.update(model: trip['bus']['model'], services: services)
18-
19-
Trip.create!(
20-
from: from,
21-
to: to,
22-
bus: bus,
23-
start_time: trip['start_time'],
24-
duration_minutes: trip['duration_minutes'],
25-
price_cents: trip['price_cents'],
26-
)
42+
43+
bus = buses[trip['bus']['number']]
44+
45+
connection.put_copy_data("#{from[:id]};#{to[:id]};#{trip['start_time']};#{trip['duration_minutes']};#{trip['price_cents']};#{bus[:id]}\n")
46+
end
47+
end
48+
49+
ActiveRecord::Base.connection.raw_connection.copy_data CITIES_COMMAND do
50+
cities.each do |name, attrs|
51+
connection.put_copy_data("#{attrs[:id]};#{name}\n")
52+
end
53+
end
54+
55+
ActiveRecord::Base.connection.raw_connection.copy_data BUSES_COMMAND do
56+
buses.each do |number, attrs|
57+
connection.put_copy_data("#{attrs[:id]};#{number};#{attrs[:model]}\n")
58+
end
59+
end
60+
61+
ActiveRecord::Base.connection.raw_connection.copy_data SERVICES_COMMAND do
62+
services.each do |name, attrs|
63+
connection.put_copy_data("#{attrs[:id]};#{name}\n")
64+
end
65+
end
66+
67+
ActiveRecord::Base.connection.raw_connection.copy_data BUSES_SERVICES_COMMAND do
68+
buses_services.each do |bus_id, service_id|
69+
connection.put_copy_data("#{bus_id};#{service_id}\n")
2770
end
2871
end
2972
end
73+
end
74+
75+
private
3076

31-
private
77+
attr_reader :stream
78+
attr_accessor :cities, :services, :buses, :buses_services
79+
80+
def clean_database
81+
City.delete_all
82+
Bus.delete_all
83+
Service.delete_all
84+
Trip.delete_all
85+
ActiveRecord::Base.connection.execute('delete from buses_services;')
86+
end
87+
88+
def city(name)
89+
cities[name] ||= { id: cities.size + 1 }
90+
end
91+
92+
def service(name)
93+
services[name] ||= { id: services.size + 1 }
94+
end
95+
96+
def create_bus(number, model)
97+
buses[number] = { id: buses.size + 1, model: }
98+
end
3299

33-
def clean_database
34-
City.delete_all
35-
Bus.delete_all
36-
Service.delete_all
37-
Trip.delete_all
38-
ActiveRecord::Base.connection.execute('delete from buses_services;')
100+
def create_bus_services(bus, services)
101+
services.map do |service|
102+
@buses_services << [bus[:id], service[:id]]
39103
end
40104
end
41105
end

case-study.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,53 @@
1212
1) время выполнения fixtures/small.json - 10.167809
1313
2) воспользуемся rbspy для профилирования скрипта импорта
1414
3) rbspy / rubyprof выдают много лишних данных при вызове таски, поэтому можно вынести импорт в отдельный класс и профилировать его
15-
4) вынесла в класс DataLoader, перенесла тесты
15+
4) вынесла в класс DataLoader, перенесла тесты
16+
17+
Пока выносила обратила внимание что файл считывается в память целиком, также в задании упоминается про файл 1М который весит примерно 3ГБ, перепишем сразу на стриминг
18+
добавила класс jsonStreamer - собирает объекты и возвращает по одному, какой то адекватный гем не нашла для такой обработки файла, теперь используется потоковая обработка файла
19+
20+
21+
1) время выполнения не поменялось
22+
2) время выполнения fixtures/small.json - 10.473295
23+
3) отчет stackfrof - главная точка роста:
24+
в отчете видны 2 главные точки роста - update (41%) и find_or_create_by (25%)
25+
4) что делать с update пока непонятно, а вот такое кол-во find_or_create_by можно заменить
26+
5) Внесем правки в работу с городами, написано что в файле их не больше 100, попробуем собирать их в словать параллельно создавая
27+
6) время выполнения fixtures/small.json - 8.523640
28+
7) изменения в отчете профилировщика: find_or_create_by снизился до 18%
29+
30+
1) также у нас собирается еще 2 стравочника - services, buses
31+
2) заменим find_or_create_by и там
32+
3) services: 6.919434, find_or_create_by 23%
33+
4) buses: 5.319638, find_or_create_by больше нет
34+
35+
1) главная точка роста: update (64%)
36+
2) обновляется автобус сервисами, это обновление в целом выглядит довольно бесполезно, потому что сервисы останутся от последнего trip в файле
37+
3) поэтому обновление сервисами можно в принципе убрать, либо уточнить формат файла, должны ли они добавляться / обновляться или браться пересечение
38+
4) убрали обновление заменив его на создание сервисов при создании автобуса
39+
5) время: 3.025756
40+
6) отчет профилировщика: update больше нет
41+
42+
1) главная точка роста: создание записей
43+
2) обратимся к логам и посмотрим на кол-во обращений к базе
44+
3) на файл example к базе было 10 (создание trip) + 2 (проверка существования автобуса + создание) + 4 (проверка городов + создание) + 2 (создание сревисов) = 18
45+
4) можно воспользоваться стримингом из readme
46+
5) время для small: 0.138599
47+
6) medium: 0.510029
48+
7) large: 3.958625
49+
8) 1M: 38.381950
50+
9) ну тут уже время упирается в стример, без каких либо преобразований он перебирает файл 1M за 34.973238
51+
10) причем потребление памяти на 1М не превышает 8МБ
52+
```
53+
INITIAL MEMORY USAGE: 103 MB
54+
MEMORY USAGE: 103 MB
55+
MEMORY USAGE: 110 MB
56+
... 7 строчек с 110 MB
57+
MEMORY USAGE: 110 MB
58+
MEMORY USAGE: 111 MB
59+
... 26 строчек с 111 MB
60+
MEMORY USAGE: 111 MB
61+
FINAL MEMORY USAGE: 110 MB
62+
```
63+
64+

lib/tasks/profile.rake

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ namespace :profile do
33
task time: :environment do
44
require 'benchmark'
55

6-
puts(Benchmark.measure { DataLoader.load('fixtures/small.json') })
6+
puts(Benchmark.measure { DataLoader.load('fixtures/1M.json') })
77
end
88

99
desc 'Ruby prof'
@@ -39,7 +39,23 @@ namespace :profile do
3939
io.close
4040
end
4141

42-
DataLoader.load('fixtures/small.json')
42+
DataLoader.load('fixtures/1M.json')
4343
monitor_thread.kill
4444
end
45+
46+
desc 'Stackprof cli'
47+
task stackprof_cli: :environment do
48+
StackProf.run(mode: :wall, out: Rails.root.join('profile', 'stackprof_reports/stackprof.dump'), interval: 1000) do
49+
DataLoader.load('fixtures/small.json')
50+
end
51+
end
52+
53+
desc 'Stackprof speedscope'
54+
task stackprof_speedscope: :environment do
55+
profile = StackProf.run(mode: :wall, raw: true) do
56+
DataLoader.load('fixtures/small.json')
57+
end
58+
59+
File.write(Rails.root.join('profile', 'stackprof_reports/stackprof.json'), JSON.generate(profile))
60+
end
4561
end

spec/services/data_loader_spec.rb

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,33 @@
2323
)
2424
end
2525

26+
it 'creates buses_services' do
27+
bus = Bus.find_by(number: '123')
28+
wifi = Service.find_by(name: 'WiFi')
29+
toilet = Service.find_by(name: 'Туалет')
30+
31+
expect(BusesService.all).to contain_exactly(
32+
have_attributes(bus_id: bus.id, service_id: wifi.id),
33+
have_attributes(bus_id: bus.id, service_id: toilet.id)
34+
)
35+
end
36+
2637
it 'creates trips' do
2738
samara = City.find_by(name: 'Самара')
2839
moscow = City.find_by(name: 'Москва')
2940
bus = Bus.find_by(number: '123')
3041

3142
expect(Trip.all).to contain_exactly(
32-
have_attributes(start_time: '11:00', duration_minutes: 168, price_cents: 474, from: moscow, to: samara, bus:),
33-
have_attributes(start_time: '17:30', duration_minutes: 37, price_cents: 173, from: samara, to: moscow, bus:),
34-
have_attributes(start_time: '12:00', duration_minutes: 323, price_cents: 672, from: moscow, to: samara, bus:),
35-
have_attributes(start_time: '18:30', duration_minutes: 315, price_cents: 969, from: samara, to: moscow, bus:),
36-
have_attributes(start_time: '13:00', duration_minutes: 304, price_cents: 641, from: moscow, to: samara, bus:),
37-
have_attributes(start_time: '19:30', duration_minutes: 21, price_cents: 663, from: samara, to: moscow, bus:),
38-
have_attributes(start_time: '14:00', duration_minutes: 598, price_cents: 629, from: moscow, to: samara, bus:),
39-
have_attributes(start_time: '20:30', duration_minutes: 292, price_cents: 22, from: samara, to: moscow, bus:),
40-
have_attributes(start_time: '15:00', duration_minutes: 127, price_cents: 795, from: moscow, to: samara, bus:),
41-
have_attributes(start_time: '21:30', duration_minutes: 183, price_cents: 846, from: samara, to: moscow, bus:)
43+
have_attributes(start_time: '11:00', duration_minutes: 168, price_cents: 474, from_id: moscow.id, to_id: samara.id, bus_id: bus.id),
44+
have_attributes(start_time: '17:30', duration_minutes: 37, price_cents: 173, from_id: samara.id, to_id: moscow.id, bus_id: bus.id),
45+
have_attributes(start_time: '12:00', duration_minutes: 323, price_cents: 672, from_id: moscow.id, to_id: samara.id, bus_id: bus.id),
46+
have_attributes(start_time: '18:30', duration_minutes: 315, price_cents: 969, from_id: samara.id, to_id: moscow.id, bus_id: bus.id),
47+
have_attributes(start_time: '13:00', duration_minutes: 304, price_cents: 641, from_id: moscow.id, to_id: samara.id, bus_id: bus.id),
48+
have_attributes(start_time: '19:30', duration_minutes: 21, price_cents: 663, from_id: samara.id, to_id: moscow.id, bus_id: bus.id),
49+
have_attributes(start_time: '14:00', duration_minutes: 598, price_cents: 629, from_id: moscow.id, to_id: samara.id, bus_id: bus.id),
50+
have_attributes(start_time: '20:30', duration_minutes: 292, price_cents: 22, from_id: samara.id, to_id: moscow.id, bus_id: bus.id),
51+
have_attributes(start_time: '15:00', duration_minutes: 127, price_cents: 795, from_id: moscow.id, to_id: samara.id, bus_id: bus.id),
52+
have_attributes(start_time: '21:30', duration_minutes: 183, price_cents: 846, from_id: samara.id, to_id: moscow.id, bus_id: bus.id)
4253
)
4354
end
4455
end

0 commit comments

Comments
 (0)