Skip to content

Commit af85665

Browse files
author
aleksey.ryabchikov
committed
last loop
1 parent 48d69d4 commit af85665

File tree

5 files changed

+111
-30
lines changed

5 files changed

+111
-30
lines changed

Gemfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
source 'https://rubygems.org'
4+
5+
gem 'ruby-prof'
6+
gem 'rspec-benchmark'
7+
gem 'ruby-progressbar'
8+
gem 'stackprof'
9+
gem 'pry'
10+
gem 'minitest'

Gemfile.lock

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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.4)
11+
pry (0.14.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.6.3)
33+
ruby-progressbar (1.13.0)
34+
stackprof (0.2.27)
35+
36+
PLATFORMS
37+
x86_64-linux
38+
39+
DEPENDENCIES
40+
minitest
41+
pry
42+
rspec-benchmark
43+
ruby-prof
44+
ruby-progressbar
45+
stackprof
46+
47+
BUNDLED WITH
48+
2.4.13

case-study.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
- С помощью профилировщика найти главную точку роста (Профилируем с выключенным GC предварительно прогрев кеши)
3030
- Внести оптимизационные правки
3131
- С помощью профилировщика проверить есть ли улучшения
32-
- Запустить тест, проверить если улучшения есть, если да то закоммитить.
32+
- Запустить тест, проверить, если улучшения есть тогда закоммитить.
3333

3434
## Вникаем в детали системы, чтобы найти главные точки роста
3535
Для того, чтобы найти "точки роста" для оптимизации я воспользовался rbspy (удобно, потому что встроен в rubymine), stackprof и ruby-prof в разных режимах отчетов
@@ -46,6 +46,7 @@
4646
- Заменил перебор всех сессий на хэш с группированные данных по user_id. В данном конкретном месте алгоритмическая сложность с O(n) изменилась на O(1)
4747
- Метрика кратно уменьшилась при прогоне теста на перфоманс с средних 5 сек. до 0.4 сек. Это и было самым узким местом программы по всей видимости.
4848
- Повторный запуск профилировщика показал, что вместо 89,15% теперь это место занимает 0.3%.
49+
- В отчетах профилировщика эта точка роста перестала быть главной.
4950

5051
### Ваша находка №2
5152
- Профилировщики указали на следующую точку роста:
@@ -56,6 +57,7 @@
5657
- Заменил `sessions = sessions + [parse_session(line)] if cols[0] == 'session'` на `sessions = sessions << parse_session(line) if cols[0] == 'session'`
5758
Известная проблема в ruby. Оператор << позволяет не создавать новую переменную каждый раз, а писать все в существующую.
5859
- При прогоне теста, среднее значение метрики упало с 0.4 сек. до 0.25 сек.
60+
- В отчетах профилировщика эта точка роста перестала быть главной.
5961

6062
### Ваша находка №3
6163
- Ради интереса прогнал тесты с разным объемом данных, 20к, 40к, 80к и так далее. В том числе и на полном файле.
@@ -67,15 +69,47 @@
6769
```
6870
- Оптимизировал блок each, заменил '+' на Set.
6971
- При прогоне теста, среднее значение метрики упало с 0.25 сек. до 0.18 сек.
72+
- В отчетах профилировщика эта точка роста перестала быть главной.
7073

7174
### Ваша находка №4
72-
- Попробовал увеличить кол-во данных, чтобы проще было увидеть проблематику до 100_000 строк.
73-
- По отчетам нашел новую точку роста, это метод `collect_stats_from_users`, он аффектит на два проблемных места сразу
75+
- По отчетам профилировщиков нашел новую точку роста, это метод `def collect_stats_from_users`
7476
```
7577
%Total %Self Total Self Wait Child Calls Name
7678
89.28% 23.89% 1.43 0.38 0.00 1.05 10 Array#each
7779
```
78-
- Оптимизировал, а именно избавился от collect_stats_from_users и начал подготавливать данные за один проход.
80+
- Оптимизировал, а именно избавился от collect_stats_from_users и начал подготавливать данные для отчета за один проход.
7981
- При прогоне теста, среднее значение метрики упало с 0.18 сек. до 0.14 сек. на 20_000 строк.
82+
- В отчетах профилировщика эта точка роста перестала быть главной.
8083

84+
### Ваша находка №5
85+
- stackprof показал на точку роста на строке с парсингом даты `dates = sessions.map { |s| Date.parse(s['date']) }`
8186

87+
```
88+
%Total %Self Total Self Wait Child Calls Name
89+
28.57% 16.02% 0.09 0.05 0.00 0.04 16954 <Class::Date>#parse
90+
```
91+
- Поправил, Date.parse лишняя обработка.
92+
- Среднее значение метрики упало с 0.14 до 0.2 сек.
93+
- В отчетах профилировщика эта точка роста перестала быть главной.
94+
95+
### Ваша находка №6
96+
- Уже правктически выполняется бюджет прогона файла data_large.txt, но профилировщик подсветил еще одну точку роста:
97+
```
98+
%Total %Self Total Self Wait Child Calls Name
99+
30.73% 30.73% 0.06 0.06 0.00 0.00 40001 String#split
100+
```
101+
- Выпилил лишние String.split
102+
- Так как выполнение теста на 20_000 строк очень мало, увеличил файл до 60_000 строк.
103+
Среднее значение прогода 60_000 строк = 0.3 сек.
104+
- Среднее значение прогона всего файла с ~30 сек. упало до 27 сек.
105+
- В отчетах профилировщика эта точка роста перестала быть главной.
106+
- Вижу еще несколько точек роста, но решил на этом остановится =)
107+
108+
## Результаты
109+
В результате проделанной оптимизации наконец удалось обработать файл с данными.
110+
Удалось улучшить метрику системы на 20_000 строк с 5 сек. до 0.1 сек., на 60_000 строк с до 0.3 сек и уложиться в заданный бюджет.
111+
Так же был переписан тест с minitest на rspec и вынесен в отдельный файл.
112+
113+
## Защита от регрессии производительности
114+
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы было написано два performance теста, один для проверки на 60_000 строках,
115+
второй на полном объеме файла data_large.txt

spec/task-1_spec.rb

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,12 @@
33

44
describe 'task-1' do
55
let(:result) { './spec/fixtures/files/result.json' }
6+
67
describe 'Result' do
78
let(:file) { './spec/fixtures/files/data.txt' }
89

910
let(:expected_result) do
10-
'{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,
11-
CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,
12-
SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.",
13-
"longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35,
14-
SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23",
15-
"2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.",
16-
"browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,
17-
"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.",
18-
"longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,
19-
"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}' + "\n"
11+
'{"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"
2012
end
2113

2214
it 'returns equal' do
@@ -30,14 +22,14 @@
3022

3123
describe 'with partial file' do
3224
it 'works under 0.4 sec' do
33-
expect { work(file) }.to perform_under(0.14).sec.warmup(2).times.sample(10).times
25+
expect { work(file) }.to perform_under(0.3).sec.warmup(2).times.sample(10).times
3426
end
3527
end
3628

37-
# describe 'with full file' do
38-
# it 'works under 30 sec' do
39-
# expect { work('./data_large.txt') }.to perform_under(30).sec
40-
# end
41-
# end
29+
describe 'with full file' do
30+
it 'works under 30 sec' do
31+
expect { work('./data_large.txt') }.to perform_under(27).sec
32+
end
33+
end
4234
end
4335
end

task-1.rb

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,17 @@ def initialize(attributes:, sessions:)
1515
end
1616
end
1717

18-
def parse_user(user)
19-
fields = user.split(',')
20-
parsed_result = {
18+
def parse_user(fields)
19+
{
2120
'id' => fields[1],
2221
'first_name' => fields[2],
2322
'last_name' => fields[3],
2423
'age' => fields[4],
2524
}
2625
end
2726

28-
def parse_session(session)
29-
fields = session.split(',')
30-
parsed_result = {
27+
def parse_session(fields)
28+
{
3129
'user_id' => fields[1],
3230
'session_id' => fields[2],
3331
'browser' => fields[3],
@@ -52,8 +50,8 @@ def work(file_path, result_path = 'spec/fixtures/files/result.json')
5250

5351
file_lines.each do |line|
5452
cols = line.split(',')
55-
users = users << parse_user(line) if cols[0] == 'user'
56-
sessions = sessions << parse_session(line) if cols[0] == 'session'
53+
users = users << parse_user(cols) if cols[0] == 'user'
54+
sessions = sessions << parse_session(cols) if cols[0] == 'session'
5755
end
5856

5957
# Отчёт в json
@@ -108,7 +106,6 @@ def work(file_path, result_path = 'spec/fixtures/files/result.json')
108106
sessions = user.sessions
109107
times = sessions.map { |s| s['time'].to_i }
110108
browsers = sessions.map { |s| s['browser'].upcase }
111-
dates = sessions.map { |s| Date.parse(s['date']) }
112109

113110
report['usersStats'][user_key] = {
114111
# Количество сессий
@@ -124,7 +121,7 @@ def work(file_path, result_path = 'spec/fixtures/files/result.json')
124121
# Всегда использовал только Chrome?
125122
'alwaysUsedChrome' => browsers.all? { |b| b.include?('CHROME') },
126123
# Даты сессий через запятую в обратном порядке в формате iso8601
127-
'dates' => dates.sort.reverse.map(&:iso8601)
124+
'dates' => user.sessions.map { |s| s['date'] }.sort.reverse
128125
}
129126
end
130127

0 commit comments

Comments
 (0)