-
Notifications
You must be signed in to change notification settings - Fork 135
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
Task2 #96
base: master
Are you sure you want to change the base?
Task2 #96
Changes from all commits
cac454f
3928f13
f124b2c
a0d5f7c
f1852bb
1618920
64b2b32
11a6bd8
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 |
---|---|---|
@@ -0,0 +1,13 @@ | ||
data/ | ||
|
||
ruby_prof_reports/ | ||
stackprof_reports/ | ||
|
||
ruby_prof.rb | ||
ruby_prof_grind.rb | ||
stackprof.rb | ||
stackprof_speedscope.rb | ||
memory_profiler.rb | ||
|
||
docker-valgrind-massif/ | ||
massif.out.1 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
test: | ||
ruby test_me.rb | ||
|
||
bm: | ||
ruby benchmarking.rb | ||
|
||
memory: | ||
ruby memory_profiler.rb | ||
|
||
prof: | ||
ruby ruby_prof.rb | ||
|
||
prof-graph_read: | ||
open ruby_prof_reports/graph.html | ||
|
||
prof-call_stack_read: | ||
open ruby_prof_reports/call_stack.html | ||
|
||
prof-call_grind: | ||
ruby ruby_prof_grind.rb | ||
|
||
prof-call_grind_read: | ||
qcachegrind ruby_prof_reports/callgrind.out.${P} | ||
|
||
.PHONY: test |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
require 'benchmark' | ||
require_relative 'task-2' | ||
|
||
# 100_000 | ||
# | ||
# Start | ||
# MEMORY USAGE: 634 MB | ||
# Finish in 125.11s | ||
|
||
# File.foreach | ||
# MEMORY USAGE: 596 MB | ||
# Finish in 119.95 | ||
|
||
# 40_000 | ||
# | ||
# File.foreach | ||
# MEMORY USAGE: 316 MB | ||
# Finish in 15.67 | ||
|
||
# 1. | ||
# MEMORY USAGE: 179 MB | ||
# Finish in 16.95 | ||
|
||
# 2. | ||
# MEMORY USAGE: 65 MB | ||
# Finish in 0.39 | ||
|
||
# 3. | ||
# MEMORY USAGE: 53 MB | ||
# Finish in 0.25 | ||
|
||
# 3. | ||
# MEMORY USAGE: 53 MB | ||
# Finish in 0.25 | ||
|
||
# 4. | ||
# MEMORY USAGE: 51 MB | ||
# Finish in 0.1 | ||
|
||
# 5. | ||
# MEMORY USAGE: 29 MB | ||
# Finish in 0.2 | ||
|
||
# 100_000 | ||
# END | ||
# MEMORY USAGE: 30 MB | ||
# Finish in 0.5 | ||
|
||
time = Benchmark.realtime do |x| | ||
work('data/data_large.txt') | ||
end | ||
puts "Finish in #{time.round(2)}" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,44 +12,190 @@ | |
Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
||
## Формирование метрики | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *объем потребления оперативной памяти при обработке файла `data_large` в течение работы программы. Использовались файлы 100_000 строк - начальный и конечный замперы, 40_000 - промежуточные. | ||
|
||
## Гарантия корректности работы оптимизированной программы | ||
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
||
## Feedback-Loop | ||
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* | ||
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 1 min. | ||
|
||
Вот как я построил `feedback_loop`: *как вы построили feedback_loop* | ||
benchmarking.rb | ||
memory-profiler | ||
ruby-prof_call_grind + ruby-prof_call_stack | ||
Refactoring | ||
Test | ||
benchmarking.rb | ||
ruby-prof_call_stack | ||
|
||
## Вникаем в детали системы, чтобы найти главные точки роста | ||
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* | ||
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *memory-profiler ruby-prof_call_grind + ruby-prof_call_stack* | ||
|
||
Вот какие проблемы удалось найти и решить | ||
|
||
### Ваша находка №0 | ||
Перевод программы на потоковый подход (File.foreach вместо File.read) значимо на объем памяти и время выполнения не повлиял | ||
|
||
Стартовое значение метрики (40_000) | ||
# MEMORY USAGE: 316 MB | ||
# Finish in 16.95 | ||
|
||
### Ваша находка №1 | ||
- какой отчёт показал главную точку роста | ||
memory-profiler: | ||
MEMORY USAGE: 5425 MB | ||
Total allocated: 6.70 GB (2039801 objects) | ||
allocated memory by location: | ||
4.59 GB: `sessions = sessions + [parse_session(line)] if cols[0] == 'session'` | ||
1.55 GB: user_sessions = sessions.select { |session| session['user_id'] == user['id'] } | ||
|
||
allocated objects by location: | ||
611814 - { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } | ||
|
||
Allocated String Report: | ||
144564 " " - 154 | ||
107718 "session" - 61 | ||
|
||
stackprof - менее информативен, далее не пользуемся | ||
ruby-prof flat - менее информативен, далее не пользуемся | ||
ruby-prof graph - неплохой, пользуемся как вспомогательным | ||
ruby-prof call_stack - отличный в плане визуала, пользуемся как вторым основным, graph не нужен | ||
2 точки роста: | ||
54% - Object#collect_stats_from_users | ||
-> 26% - Date#parse | ||
42% - Foreach#each | ||
-> 14% - #parse_session | ||
ruby-prof call_grind - просто лучший, хотя дольше вызывать, пользуем только его и memory-profiler | ||
|
||
- как вы решили её оптимизировать | ||
- как изменилась метрика | ||
Array#<< вместо Array#+ в `sessions = sessions + [parse_session(line)] if cols[0] == 'session'` | ||
|
||
- как изменилась метрика: уменьшилась в 2 раза | ||
# MEMORY USAGE: 179 MB | ||
|
||
- как изменился отчёт профилировщика | ||
memory-profiler: | ||
1.6MB: `sessions = sessions + [parse_session(line)] if cols[0] == 'session'` | ||
|
||
### Ваша находка №2 | ||
- какой отчёт показал главную точку роста | ||
memory-profiler: | ||
Total allocated: 1.96 GB (1959801 objects) | ||
1.66 GB: user_sessions = sessions.select { |session| session['user_id'] == user['id'] } | ||
allocated memory by class | ||
----------------------------------- | ||
1.69 GB Array | ||
66.92 MB String | ||
|
||
call_stack: | ||
56% - Object#collect_stats_from_users | ||
-> Array#each | ||
-> Array#map | ||
-> Data#parse | ||
|
||
- как вы решили её оптимизировать | ||
Снова идем за memory-profiler: в Array#each с #select меняем Array#+ на Array#<<. Эффект однако небоьшой, проблема - | ||
в неприлично разбухающем количестве select-массивов при итерации юзеров. Поэтому заменил их промежуточным хэшем с одним проходом по sessions, из которого потом удобно формировать хэш юзеров. | ||
|
||
- как изменилась метрика | ||
# MEMORY USAGE: 65 MB: в 3 раза | ||
# Finish in 0.38 | ||
|
||
- как изменился отчёт профилировщика | ||
1.18 MB:) | ||
allocated memory by class | ||
----------------------------------- | ||
67.16 MB String | ||
28.59 MB Hash | ||
26.59 MB Array | ||
|
||
### Ваша находка №3 | ||
- какой отчёт показал главную точку роста | ||
Возвращаемся к отчетам ruby-prof, который теперь более показательные чем memory-profiler | ||
memory-profiler: | ||
48.05 MB - #map | ||
call_stack: | ||
56% - Object#collect_stats_from_users | ||
-> Array#each | ||
-> Array#map | ||
-> Date#parse | ||
Array#map [67551 calls, 67553 total] | ||
|
||
Слишком много map | ||
- как вы решили её оптимизировать | ||
Убираем лишние maps при вызовах Object#collect_stats_from_users, также уберем Date.parse | ||
|
||
- как изменилась метрика: меньше, чем хотелось бы | ||
MEMORY USAGE: 53 MB | ||
Finish in 0.25 | ||
|
||
### Ваша находка №X | ||
- как изменился отчёт профилировщика | ||
18% - Object#collect_stats_from_users | ||
Array#map [6141 calls, 12284 total] | ||
|
||
### Ваша находка №4 | ||
- какой отчёт показал главную точку роста | ||
memory-profiler: | ||
19MB: fields = session.split(',') | ||
16MB: cols = line.split(',') | ||
|
||
ruby-call-stack: | ||
31.37% (41.21%) Object#parse_session [33859 calls, 33859 total] | ||
21.96% (70.00%) String#split [33859 calls, 80000 total] | ||
|
||
Проблема - в String#split в parse_session и parse_user | ||
Убрал лишние #split, но лучше не стало | ||
- как изменился отчёт профилировщика | ||
28.85% (100.00%) Array#each [1 calls, 6144 total] | ||
29.96 MB String | ||
14.36 MB Array | ||
9.24 MB Hash | ||
Попробуем убрать лишние хранилища данные - хэши и массивы: в частности - промежуточный объект sessions и плохую сборку уникальных массивов | ||
|
||
- как изменилась метрика: никак, но скорость уменьшилась более, чем двукратно (за счет удаления лишних переборов), однако на для памяти по сравнению с тем, чем она уже забита, это - ничто. И это первый вывод - смотри что оптимизируешь! | ||
|
||
data_40k.txt | ||
MEMORY USAGE: 51 MB | ||
Finish in 0.1 | ||
|
||
data_large.txt | ||
MEMORY USAGE: 1889 MB | ||
Finish in 17.85 | ||
|
||
### Ваша находка №6 | ||
И тут я вспомнил, что забыл поменять запись файла на потоковую:( | ||
- как вы решили её оптимизировать | ||
- как изменилась метрика | ||
Поменял запись | ||
- как изменилась метрика: с одной стороны не сильно, но с другой - очевидно, что поменялась асимптотика | ||
data_40k.txt | ||
MEMORY USAGE: 29 MB | ||
Finish in 0.2 | ||
|
||
data_large.txt | ||
MEMORY USAGE: 31 MB | ||
Finish in 15.81 | ||
|
||
- как изменился отчёт профилировщика | ||
66.8MB - File.write | ||
19MB - String.split | ||
В целом значимых точек роста нет. Можно еще поработать со строками: #start_with?, символы в хэшах и проч., но во-первых все это уже делал, во-вторых - очевидно, что значимого влияния не будет и временные затраты себя не оправдают, и в-главных - бюджет достигнут с 31MB при необходимых 70, важно во-время остановится) | ||
|
||
## Результаты | ||
В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. | ||
Удалось улучшить метрику системы с *634 MB до 30MB для 100_000 строк, data_large.txt: 31MB* и уложиться в заданный бюджет. | ||
|
||
*Какими ещё результами можете поделиться* | ||
String#split - тяжелый, Array#<< - эффективный, потоки хороши (как обычно), всегда смотри что оптимизруешь) | ||
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. split не то чтобы тяжёлый, он просто создаёт объекты в нашем случае, на каждую строчку по штук 5, потом они удаляются GC В принципе можно воспользосаться split with block, там изнутри блока можно записывать значения в заранее созданные 5 переменных и наверно избежать создания лишних объектов Но создание этих объектов не критично. Для нас тут самое главное, что мы не накапливаем в памяти большого объёма данных, и GC может удалять ненужные объекты и стабильно держать потребление памяти на 40Мб при обработке сколь угодно большого файла 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. Array << не то чтобы эффективный, скорее a = a + [b] дико не эффективно, я бы так поставил акцент |
||
|
||
Massif | ||
❯ ./profile.sh | ||
==1== Massif, a heap profiler | ||
==1== Copyright (C) 2003-2015, and GNU GPL'd, by Nicholas Nethercote | ||
==1== Using Valgrind-3.12.0.SVN and LibVEX; rerun with -h for copyright info | ||
==1== Command: ruby main.rb | ||
==1== | ||
MEMORY USAGE: 52 MB (37 MB on Visualizer) | ||
==1== | ||
|
||
## Защита от регрессии производительности | ||
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* | ||
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали*: perormance.rb |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
require_relative 'task-2.rb' | ||
|
||
work('data/data_large.txt') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
require 'rspec/core' | ||
require 'rspec-benchmark' | ||
require_relative 'task-2' | ||
|
||
RSpec.configure do |config| | ||
config.include RSpec::Benchmark::Matchers | ||
end | ||
|
||
describe 'basic work' do | ||
let(:filepath) { 'data/data_test.txt' } | ||
|
||
it 'eats less than 30MB' do | ||
expect { work(filepath) } | ||
.to perform_allocation(30_000_000).bytes | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#!/bin/bash | ||
|
||
docker run -it \ | ||
-v $(pwd):/home/massif/test \ | ||
-e DATA_FILE=data/data_large.txt \ | ||
spajic/docker-valgrind-massif \ | ||
valgrind --tool=massif ruby main.rb |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"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"]}},"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"} |
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.
👍