Skip to content

Commit f6010a9

Browse files
committed
optimization #2
1 parent 972fb94 commit f6010a9

File tree

7 files changed

+151
-90
lines changed

7 files changed

+151
-90
lines changed

Gemfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
source "https://rubygems.org"
2+
gem "ruby-prof"
3+
gem "rspec-benchmark"
4+
gem "ruby-progressbar"
5+
gem "minitest"

Gemfile.lock

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
benchmark-malloc (0.2.0)
5+
benchmark-perf (0.6.0)
6+
benchmark-trend (0.4.0)
7+
coderay (1.1.3)
8+
diff-lcs (1.5.1)
9+
method_source (1.1.0)
10+
minitest (5.25.1)
11+
pry (0.15.2)
12+
coderay (~> 1.1)
13+
method_source (~> 1.0)
14+
rspec (3.13.0)
15+
rspec-core (~> 3.13.0)
16+
rspec-expectations (~> 3.13.0)
17+
rspec-mocks (~> 3.13.0)
18+
rspec-benchmark (0.6.0)
19+
benchmark-malloc (~> 0.2)
20+
benchmark-perf (~> 0.6)
21+
benchmark-trend (~> 0.4)
22+
rspec (>= 3.0)
23+
rspec-core (3.13.2)
24+
rspec-support (~> 3.13.0)
25+
rspec-expectations (3.13.3)
26+
diff-lcs (>= 1.2.0, < 2.0)
27+
rspec-support (~> 3.13.0)
28+
rspec-mocks (3.13.2)
29+
diff-lcs (>= 1.2.0, < 2.0)
30+
rspec-support (~> 3.13.0)
31+
rspec-support (3.13.2)
32+
ruby-prof (1.7.1)
33+
ruby-progressbar (1.13.0)
34+
35+
PLATFORMS
36+
arm64-darwin-23
37+
ruby
38+
39+
DEPENDENCIES
40+
minitest
41+
pry
42+
rspec-benchmark
43+
ruby-prof
44+
ruby-progressbar
45+
46+
BUNDLED WITH
47+
2.5.16

case-study-template.md renamed to case-study.md

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,68 +12,57 @@
1212
Я решил исправить эту проблему, оптимизировав эту программу.
1313

1414
## Формирование метрики
15-
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика*
15+
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику:
16+
Время выполнения программы.
1617

1718
## Гарантия корректности работы оптимизированной программы
1819
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации.
1920

2021
## Feedback-Loop
2122
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось*
2223

23-
Вот как я построил `feedback_loop`: *как вы построили feedback_loop*
24+
Вот как я построил `feedback_loop`: создал файлы с различным количеством строк, чтобы программа могла выполняться за 10–20 секунд.
25+
После каждого изменения я запускал программу на файлах с разным количеством строк и смотрел на результаты.
2426

2527
## Вникаем в детали системы, чтобы найти главные точки роста
26-
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались*
28+
Для того, чтобы найти "точки роста" для оптимизации я воспользовался:
29+
- gem ruby-prof и отчеты callstack & qcachegrind
2730

28-
Вот какие проблемы удалось найти и решить
31+
Вот какие проблемы удалось найти и решить:
2932

30-
### Ваша находка №1
31-
- какой отчёт показал главную точку роста
32-
- как вы решили её оптимизировать
33-
- как изменилась метрика
34-
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
35-
36-
### Ваша находка №2
37-
- какой отчёт показал главную точку роста
38-
- как вы решили её оптимизировать
39-
- как изменилась метрика
40-
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
41-
42-
### Ваша находка №X
43-
- какой отчёт показал главную точку роста
44-
- как вы решили её оптимизировать
45-
- как изменилась метрика
46-
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
47-
48-
## Результаты
49-
В результате проделанной оптимизации наконец удалось обработать файл с данными.
50-
Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет.
51-
52-
*Какими ещё результами можете поделиться*
53-
54-
## Защита от регрессии производительности
55-
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали*
56-
57-
58-
59-
Добавил прогресс бары
60-
61-
### находка №1 Многократная итерация объекта sessions для создания users_objects
33+
### находка №1 Многократная итерация объекта sessions для создания users_objects
6234
- callstack из ruby-prof
63-
- одной итерацией собрать необходимые данные в объекте sessions_by_user
64-
- время выполнения приложения на 15т строк сократилась 7.5 секунд до 0.9 секунд
65-
- исправленная проблема перестала быть главной точкой роста
66-
35+
- Одной итерацией собрать необходимые данные в объекте sessions_by_user
36+
- Время выполнения приложения на 15т строк сократилась 7.5 секунд до 0.9 секунд
37+
- Исправленная проблема перестала быть главной точкой роста
6738

6839
### находка №2 Неэффективный алгоритм с многократными проверками для сбора unique_browsers.
6940
- callstack из ruby-prof
7041
- Было принято решение заменить неэффективный алгоритм с многократными проверками на более оптимизированное решение, использующее встроенные методы Ruby
71-
- время выполнения приложения на 15т строк сократилась 0.9 секунд до 0.6 секунд
72-
- исправленная проблема перестала быть главной точкой роста
42+
- Время выполнения приложения на 15т строк сократилась 0.9 секунд до 0.6 секунд
43+
- Исправленная проблема перестала быть главной точкой роста
7344

7445
### Находка №3: Неэффективное добавление элементов в массивы с использованием оператора конкатенации
7546
- callstack из ruby-prof
7647
- Было принято решение заменить неэффективное добавление элементов в массивы с помощью оператора + на использование метода << (shovel operator). Дополнительно была применена конструкция case для улучшения читаемости и производительности.
7748
Время выполнения: точные измерения не предоставлены, но ожидается значительное улучшение производительности, особенно для больших наборов данных. Операция << имеет сложность O(1), тогда как + создает новый массив при каждой итерации, что имеет сложность O(n).
78-
- время выполнения приложения на 15т строк сократилась 0.6 секунд до 0.5 секунд
79-
- исправленная проблема перестала быть главной точкой роста
49+
- Время выполнения приложения на 15т строк сократилась 0.6 секунд до 0.5 секунд
50+
- Исправленная проблема перестала быть главной точкой роста
51+
52+
### Находка №4: Избыточный парсинг дат и преобразование в формат iso8601
53+
- callstack из ruby-prof
54+
- Так как мы уже имеем данные в нужном формате, то было принято решение не тратить время на преобразование даты в формат iso8601
55+
- Исправленная проблема перестала быть главной точкой роста
56+
57+
### Находка №5: Избыточное использование хешей для представления сессий
58+
- callstack из ruby-prof
59+
- Анализ профилировщика показал, что создание и обработка хешей в методе parse_session занимают значительное время, особенно при большом количестве сессий. Это связано с накладными расходами на создание объектов хешей и доступ к их ключам.
60+
- Было принято решение заменить использование хешей на Struct для представления сессий. Struct предоставляет более легковесную и быструю структуру для хранения данных с фиксированными ключами, что уменьшает время создания объектов и ускоряет доступ к их атрибутам.
61+
- Исправленная проблема перестала быть главной точкой роста. Данные начали обрабатываться быстрее и укладываться в бюджет времени.
62+
63+
## Результаты
64+
В результате проделанной оптимизации наконец удалось обработать файл с данными.
65+
Удалось улучшить метрику системы с более чем 15 минут обработки файла до 30 секунд и уложиться в заданный бюджет.
66+
67+
## Защита от регрессии производительности
68+
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы я написал performance_test.rb.

performance_test.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
require 'rspec-benchmark'
2+
require_relative 'task-1'
3+
4+
RSpec.configure do |config|
5+
config.include RSpec::Benchmark::Matchers
6+
end
7+
8+
describe 'Performance' do
9+
describe 'task-1#work' do
10+
it 'works with large data under 30 sec' do
11+
expect { work(file_name: "data_large.txt", progress_bar: false) }.to perform_under(30).sec
12+
end
13+
end
14+
end

run_profiling.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
require_relative 'task-1.rb'
2+
require 'ruby-prof'
3+
4+
RubyProf.measure_mode = RubyProf::WALL_TIME
5+
6+
result = RubyProf.profile { work(file_name: '100000.txt', disable_gc: true, progress_bar: false) }
7+
8+
RubyProf::FlatPrinter.new(result).print(File.open("ruby_prof_reports/flat.txt", "w+"))
9+
RubyProf::GraphHtmlPrinter.new(result).print(File.open("ruby_prof_reports/graph.html", "w+"))
10+
RubyProf::CallStackPrinter.new(result).print(File.open('ruby_prof_reports/callstack.html', 'w+'))
11+
RubyProf::CallTreePrinter.new(result).print(:path => "ruby_prof_reports", :profile => 'callgrind')

task-1.rb

Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
# Deoptimized version of homework task
2-
31
require 'json'
4-
require 'pry'
5-
require 'date'
6-
require 'minitest/autorun'
2+
73
require 'ruby-progressbar'
84

95
class User
@@ -25,15 +21,11 @@ def parse_user(user)
2521
}
2622
end
2723

24+
Session = Struct.new(:user_id, :session_id, :browser, :time, :date)
25+
2826
def parse_session(session)
29-
fields = session.split(',')
30-
parsed_result = {
31-
'user_id' => fields[1],
32-
'session_id' => fields[2],
33-
'browser' => fields[3],
34-
'time' => fields[4],
35-
'date' => fields[5],
36-
}
27+
_, user_id, session_id, browser, time, date = session.split(',', 6)
28+
Session.new(user_id, session_id, browser, time, date)
3729
end
3830

3931
def collect_stats_from_users(report, users_objects, &block)
@@ -149,7 +141,7 @@ def work(file_name: "data.txt", disable_gc: false, progress_bar: true)
149141

150142
# Даты сессий через запятую в обратном порядке в формате iso8601
151143
collect_stats_from_users(report, users_objects) do |user|
152-
{ 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } }
144+
{ 'dates' => user.sessions.map{|s| s['date']}.sort.reverse }
153145
end
154146

155147
File.write('result.json', "#{report.to_json}\n")
@@ -158,35 +150,3 @@ def work(file_name: "data.txt", disable_gc: false, progress_bar: true)
158150
def increment_progressbar(progressbar)
159151
progressbar&.increment
160152
end
161-
162-
class TestMe < Minitest::Test
163-
def setup
164-
File.write('result.json', '')
165-
File.write('data.txt',
166-
'user,0,Leida,Cira,0
167-
session,0,0,Safari 29,87,2016-10-23
168-
session,0,1,Firefox 12,118,2017-02-27
169-
session,0,2,Internet Explorer 28,31,2017-03-28
170-
session,0,3,Internet Explorer 28,109,2016-09-15
171-
session,0,4,Safari 39,104,2017-09-27
172-
session,0,5,Internet Explorer 35,6,2016-09-01
173-
user,1,Palmer,Katrina,65
174-
session,1,0,Safari 17,12,2016-10-21
175-
session,1,1,Firefox 32,3,2016-12-20
176-
session,1,2,Chrome 6,59,2016-11-11
177-
session,1,3,Internet Explorer 10,28,2017-04-29
178-
session,1,4,Chrome 13,116,2016-12-28
179-
user,2,Gregory,Santos,86
180-
session,2,0,Chrome 35,6,2018-09-21
181-
session,2,1,Safari 49,85,2017-05-22
182-
session,2,2,Firefox 47,17,2018-02-02
183-
session,2,3,Chrome 20,84,2016-11-25
184-
')
185-
end
186-
187-
def test_result
188-
work
189-
expected_result = '{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}' + "\n"
190-
assert_equal expected_result, File.read('result.json')
191-
end
192-
end

test.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require 'minitest/autorun'
2+
require_relative 'task-1.rb'
3+
4+
class TestMe < Minitest::Test
5+
def setup
6+
File.write('result.json', '')
7+
File.write('data.txt',
8+
'user,0,Leida,Cira,0
9+
session,0,0,Safari 29,87,2016-10-23
10+
session,0,1,Firefox 12,118,2017-02-27
11+
session,0,2,Internet Explorer 28,31,2017-03-28
12+
session,0,3,Internet Explorer 28,109,2016-09-15
13+
session,0,4,Safari 39,104,2017-09-27
14+
session,0,5,Internet Explorer 35,6,2016-09-01
15+
user,1,Palmer,Katrina,65
16+
session,1,0,Safari 17,12,2016-10-21
17+
session,1,1,Firefox 32,3,2016-12-20
18+
session,1,2,Chrome 6,59,2016-11-11
19+
session,1,3,Internet Explorer 10,28,2017-04-29
20+
session,1,4,Chrome 13,116,2016-12-28
21+
user,2,Gregory,Santos,86
22+
session,2,0,Chrome 35,6,2018-09-21
23+
session,2,1,Safari 49,85,2017-05-22
24+
session,2,2,Firefox 47,17,2018-02-02
25+
session,2,3,Chrome 20,84,2016-11-25
26+
')
27+
end
28+
29+
def test_result
30+
work(progress_bar: false)
31+
expected_result = '{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}' + "\n"
32+
assert_equal expected_result, File.read('result.json')
33+
end
34+
end
35+

0 commit comments

Comments
 (0)