-
Notifications
You must be signed in to change notification settings - Fork 139
homework 2 #121
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?
homework 2 #121
Changes from all commits
64d0307
9be09c6
8d26943
4d22087
bffe161
ab71cc5
b627974
203449e
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,3 @@ | ||
| source 'https://rubygems.org' | ||
|
|
||
| gem 'stackprof' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| GEM | ||
| remote: https://rubygems.org/ | ||
| specs: | ||
| stackprof (0.2.27) | ||
|
|
||
| PLATFORMS | ||
| ruby | ||
| x86_64-darwin-21 | ||
|
|
||
| DEPENDENCIES | ||
| stackprof | ||
|
|
||
| BUNDLED WITH | ||
| 2.5.16 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| .PHONY: all_reports | ||
| all_reports: | ||
| ruby benchmark.rb | ||
| ruby rubyprof_allocation.rb | ||
| ruby rubyprof_memory.rb | ||
| ruby memory_profiler.rb | ||
|
|
||
|
|
||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| require_relative 'work' | ||
| require 'benchmark' | ||
|
|
||
| puts "SIZE #{ENV['SIZE']}" | ||
| puts Benchmark.realtime { work("data/data#{ENV['SIZE']}.txt", disable_gc: ENV['GB'] || false) } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,234 @@ | ||
| # Case-study оптимизации | ||
|
|
||
| ## Актуальная проблема | ||
| В нашем проекте возникла серьёзная проблема. | ||
|
|
||
| Необходимо было обработать файл с данными, чуть больше ста мегабайт. | ||
|
|
||
| У нас уже была программа на `ruby`, которая умела делать нужную обработку. | ||
|
|
||
| Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. | ||
|
|
||
| Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
|
||
| ## Формирование метрики | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: программа должна потреблять меньше 70Мб памяти при обработке файла data_large.txt | ||
|
|
||
| Предварительно на | ||
| - 10_000 строк из файла используется MEMORY USAGE: без GB 360 MB(c GB 82 MB) скороть работы 1.765 сек | ||
|
Collaborator
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. GC == Garbage Collector |
||
| - 20_000 строк из файла используется MEMORY USAGE: без GB 1292 MB(c GB 92) скороть работы 7.57 сек | ||
|
|
||
| Видно что с увеличеине объема данных в 2 раза, память и время увеличивается без GB (1292 / 360 ≈ 3.59) почти в 4 раза это почти квадратичная сложность O(n^2) | ||
| C GB ситуация лучше, но надо понимать что это большая нагрузка на постоянную очистку. | ||
| ## Гарантия корректности работы оптимизированной программы | ||
| Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
|
||
| Так же добавился тест чтобы проверить используемую память и отслеживать изменения | ||
|
|
||
| ## Feedback-Loop | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* | ||
|
|
||
| Вот как я построил `feedback_loop`: | ||
| 1) поделил входные данные на по 10_000, 20_000 и тд | ||
| 2) - для быстрого сбора всех отчетов `SIZE=10000 make all_reports` | ||
| - подобрал данные по которым видны точки роста | ||
| - провел анализ всех отчтетов и поредедлил точку роста | ||
| - внес правки | ||
| - запустил все отчтеты | ||
| - если результат есть, закомитился | ||
| - зациклил весь 2й пункт | ||
|
|
||
|
|
||
| ## Вникаем в детали системы, чтобы найти главные точки роста | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался в освновном использовались данные из benchmark и memory_profiler, иногда для разнообразия смотрел через qcachegrind. | ||
|
|
||
| Вот какие проблемы удалось найти и решить | ||
|
|
||
| ### Ваша находка №1 | ||
| - по отчету memory_profiler первоночально показала что проблема есть тут: | ||
| ``` | ||
| MEMORY USAGE: БЕЗ GB 360 MB (c GB 38 MB) | ||
|
|
||
| allocated memory by location | ||
| ----------------------------------- | ||
| 287.67 MB rails-optimization-task2/work.rb:56 | ||
|
|
||
| allocated memory by class | ||
| ----------------------------------- | ||
| 416.75 MB Array | ||
| => | ||
| sessions = sessions + [parse_session(line)] if cols[0] == 'session' | ||
| ``` | ||
| подрзреваем что проблема с массивами | ||
| - первым решением заменить сложение массивов на `sessions << parse_session(line)` | ||
| - по результатам изменений видим что потребление памяти уменьшилось в данном месте | ||
| c 287 MB -> 0.4 MB что означает уменьшение в 717.5 раз | ||
|
|
||
| ``` | ||
| MEMORY USAGE: 77 MB | ||
|
|
||
| allocated memory by location | ||
| ----------------------------------- | ||
| 400.00 kB rails-optimization-task2/work.rb:56 | ||
| ``` | ||
|
|
||
| скорость выполнения почти не поменялась 1.765 -> 1.678075 | ||
|
|
||
| - Тк определили что складывать массивы затраратно сразу определяем похожие места | ||
| ``` | ||
| users = users + [parse_user(line)] | ||
| 9.97 MB rails-optimization-task2/work.rb:55 | ||
|
|
||
|
|
||
| users_objects = users_objects + [user_object] | ||
| 9.57 MB rails-optimization-task2/work.rb:104 | ||
| ``` | ||
| заменяем на операцию добавления в конец массива | ||
|
|
||
| - по резульататам всех изменений получаем | ||
| ``` | ||
| MEMORY USAGE: 58 MB | ||
|
|
||
| allocated memory by location | ||
| ----------------------------------- | ||
| 400.00 kB rails-optimization-task2/work.rb:56 | ||
| 400.00 kB rails-optimization-task2/work.rb:55 | ||
| rails-optimization-task2/work.rb:104 # даже не попал в отчет | ||
|
|
||
|
|
||
| allocated memory by class | ||
| ----------------------------------- | ||
| 110.48 MB Array | ||
| 15.22 MB String | ||
| ``` | ||
|
|
||
| ### Ваша находка №2 | ||
| - для большей наглядности повысим файл нагрузки до 20_000 (SIZE=20000 make all_reports) | ||
| - | ||
| замеры через benchmark и сисемный замер памяти | ||
| ``` | ||
| SIZE 20000 | ||
| MEMORY USAGE: без GB 95 MB (c GB 54 MB) | ||
| 6.934339999977965 | ||
| ``` | ||
|
|
||
| замеры через memory_profiler.rb | ||
|
Collaborator
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. через profiler не надо делать замеры. замеры отдельно, профайлинг отдельно |
||
| ``` | ||
| MEMORY USAGE: 207 MB | ||
| Total allocated: 476.27 MB (887788 objects) | ||
| Total retained: 4.14 kB (9 objects) | ||
|
|
||
| 413.26 MB rails-optimization-task2/work.rb:102 | ||
|
|
||
| allocated memory by class | ||
| ----------------------------------- | ||
| 425.92 MB Array | ||
| 30.41 MB String | ||
| ``` | ||
| подозреваем 102 строку `user_sessions = sessions.select { |session| session['user_id'] == user['id'] }`, тут происходит работа с select | ||
| в оффициальной документации | ||
| ``` | ||
| select () - Returns a new array containing all elements of | ||
| ``` | ||
|
|
||
| - как решение вводим HASH и ищем через него. | ||
| ``` | ||
| sessions_by_users = sessions.group_by { |session| session['user_id'] } | ||
|
|
||
| users.each do |user| | ||
| attributes = user | ||
| user_object = User.new(attributes: attributes, sessions: sessions_by_users[user['id']] || []) | ||
| users_objects << user_object | ||
| end | ||
| ``` | ||
| - смотрим на результат, память потребляемя уменьшилась не не сильно с 95 до 84 MB (c GB 54 -> 42) | ||
| ``` | ||
| SIZE 20000 | ||
| MEMORY USAGE: 84 MB (с GB 42 MB) | ||
| 0.3854719999944791 | ||
| ``` | ||
|
|
||
| но потребляемая память в конкретном месте уменьшилась до | ||
|
Collaborator
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. тут лучше быть более точным, что такое потребляемая память не совсем понятно; тут конкретно речь идёт про объём аллоцированной памяти |
||
| ``` | ||
| 700.82 kB rails-optimization-task2/work.rb:100 | ||
| 633.61 kB rails-optimization-task2/work.rb:103 | ||
|
|
||
| allocated memory by class | ||
| ----------------------------------- | ||
| 30.53 MB String | ||
| 14.24 MB Hash | ||
| ``` | ||
|
|
||
| ### Ваша находка №3 | ||
| - исходя из отчетов большую часть памяти занимает цикл, когда мы проходим по всем строкам файла. Посмотрев через qcachegrind, и memory_profiler находим новуб точку роста это split | ||
| ``` | ||
| MEMORY USAGE: 84 MB(c GB 42 MB) | ||
|
|
||
| 9.48 MB rails-optimization-task2/work.rb:54 | ||
| 8.14 MB rails-optimization-task2/work.rb:28 | ||
| ``` | ||
| - заменяб сохранение после split не в массив, а в переннеыю | ||
| `type, user_id, second, third, fourth, fifth = line.split(',')`, так же `parse_user` и `parse_session` убрал split и пользуюсь уже готовыми переменными. | ||
|
Collaborator
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. да-да, отлично, простое решение, которое убирает огромное количество аллокаций в цикле |
||
| - Резульатты улучшения появились, уменьшенеие но не крититчно всег она 14 MB | ||
| ``` | ||
| SIZE 20000 | ||
| MEMORY USAGE: 70 MB (c GB 32 MB) | ||
| ``` | ||
|
|
||
| ### Ваша находка №4 | ||
| - Подняд нагрузку до 200_000 | ||
|
Collaborator
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. это не нагрузка, а скорее объём файла просто (сорри за духоту) |
||
| - Воспользовшись подсказкой о том что надо посмотреть на потоковую реализацию, сделал акцент на то а как память уделяется на по объектам: | ||
| ``` | ||
| SIZE 200000 | ||
| MEMORY USAGE: 515 MB (c GB 274 MB) | ||
| 4.325512000000344 | ||
|
|
||
|
|
||
| allocated memory by class | ||
| ----------------------------------- | ||
| 108.42 MB String | ||
| 71.43 MB Hash | ||
| 44.68 MB Array | ||
| ``` | ||
| + так же вижу что начинает расти размер память на чтение файла, соответвенно при использовании максимального файла будет только ухуодшаться: | ||
| ``` | ||
| 12.94 MB rails-optimization-task2/work.rb:46 | ||
| ``` | ||
| - Было принято решение переписать логику | ||
| 1) читать с файла по строкам и сразу собирать статистику | ||
| 2) записывать в файл результат тоже сразу в новый файл | ||
| 3) хранить уникальные бразуеры через Set | ||
| 4) соотвественно вся статитскика собирается на лету не хранится в памяти | ||
|
|
||
| - Как результат измеенний собераем бенчмарки: | ||
| без GB | ||
| ``` | ||
| SIZE 200000 | ||
| MEMORY USAGE: 198 MB | ||
| 0.8822719999998299 | ||
| ``` | ||
|
|
||
| с GB | ||
| ``` | ||
| SIZE 200000 | ||
| MEMORY USAGE: 20 MB | ||
| 0.5364520000002813 | ||
| ``` | ||
|
|
||
|
|
||
| - Пробуем запустить _large файл, видем что всё удовлетворяет условиям мы уложились в заданый бюджет. | ||
| ``` | ||
| SIZE _large | ||
| MEMORY IN PROCESS USAGE: 21 MB | ||
| MEMORY IN PROCESS USAGE: 21 MB | ||
| MEMORY USAGE RESULT: 22 MB | ||
| MEMORY USAGE: 22 MB | ||
| 8.345943000000261 | ||
| ``` | ||
|
|
||
| ## Результаты | ||
| В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
| Удалось улучшить метрику системы с потребления памяти примерно в 22 Мб и уложиться в заданный бюджет. | ||
|
|
||
| ## Защита от регрессии производительности | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы были написан перформанс тест, который следит за потребляемой памятью | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| require 'memory_profiler' | ||
| require_relative 'work' | ||
|
|
||
|
|
||
| report = MemoryProfiler.report do | ||
| work("data/data#{ENV['SIZE']}.txt", disable_gc: ENV['GB'] || false) | ||
| end | ||
| report.pretty_print(scale_bytes: true) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| require 'rspec' | ||
| require 'rspec-benchmark' | ||
| require_relative 'work' | ||
| require 'byebug' | ||
| require 'benchmark' | ||
|
|
||
|
|
||
| RSpec.configure do |config| | ||
| config.include RSpec::Benchmark::Matchers | ||
| end | ||
|
|
||
| RSpec.describe do | ||
| let(:data_file_path) { 'data.txt' } | ||
|
|
||
| describe '#to_json logic test' do | ||
| subject { File.read(data_file_path) } | ||
|
|
||
| let(:expected_result) do | ||
| JSON.parse('{"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"]}}}') | ||
| end | ||
|
|
||
| it { | ||
| work(data_file_path) | ||
|
|
||
| res = JSON.parse(File.read('result.json')) | ||
| expect(res).to eq(expected_result) | ||
| } | ||
| end | ||
|
|
||
| describe '#performance max' do | ||
| let(:data_file_path) { "data_large.txt" } | ||
|
|
||
| it 'performs success' do | ||
| work(data_file_path) | ||
| expect((`ps -o rss= -p #{Process.pid}`.to_i / 1024)).to be < 70 | ||
|
Collaborator
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 | ||
| end | ||
| 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"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| require 'ruby-prof' | ||
| require_relative 'work' | ||
|
|
||
| profile = RubyProf::Profile.new(measure_mode: RubyProf::ALLOCATIONS) | ||
|
|
||
| result = profile.profile do | ||
| work("data/data#{ENV['SIZE']}.txt", disable_gc: ENV['GB'] || false) | ||
| end | ||
|
|
||
| printer = RubyProf::FlatPrinter.new(result) | ||
| printer.print(File.open('ruby_prof_reports/flat.txt', 'w+')) | ||
|
|
||
| printer = RubyProf::GraphHtmlPrinter.new(result) | ||
| printer.print(File.open('ruby_prof_reports/graph.html', 'w+')) | ||
|
|
||
| printer = RubyProf::CallStackPrinter.new(result) | ||
| printer.print(File.open('ruby_prof_reports/callstack.html', 'w+')) | ||
|
|
||
| printer = RubyProf::CallTreePrinter.new(result) | ||
| printer.print(path: 'ruby_prof_reports', profile: 'profile') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| require 'ruby-prof' | ||
| require_relative 'work' | ||
|
|
||
| profile = RubyProf::Profile.new(measure_mode: RubyProf::MEMORY) | ||
|
|
||
| result = profile.profile do | ||
| work("data/data#{ENV['SIZE']}.txt", disable_gc: ENV['GB'] || false) | ||
| end | ||
|
|
||
| printer = RubyProf::FlatPrinter.new(result) | ||
| printer.print(File.open('ruby_prof_reports/flat_memory.txt', 'w+')) | ||
|
|
||
| printer = RubyProf::GraphHtmlPrinter.new(result) | ||
| printer.print(File.open('ruby_prof_reports/graph_memory.html', 'w+')) | ||
|
|
||
| printer = RubyProf::CallStackPrinter.new(result) | ||
| printer.print(File.open('ruby_prof_reports/callstack_memory.html', 'w+')) | ||
|
|
||
| printer = RubyProf::CallTreePrinter.new(result) | ||
| printer.print(path: 'ruby_prof_reports', profile: 'profile') |
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.
почему GB-то? 😄
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.
Опечатка которая преследовала до конца. :)