-
Notifications
You must be signed in to change notification settings - Fork 136
Task2 homework #85
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?
Task2 homework #85
Changes from all commits
b4c171b
0b4d11f
466d40c
3759095
5e88f33
e4782ea
65864a6
13f8593
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,67 @@ | ||
| # Case-study оптимизации | ||
|
|
||
| ## Актуальная проблема | ||
| В нашем проекте возникла серьёзная проблема. | ||
|
|
||
| Необходимо было обработать файл с данными, чуть больше ста мегабайт. | ||
|
|
||
| У нас уже была программа на `ruby`, которая умела делать нужную обработку. | ||
|
|
||
| Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. | ||
|
|
||
| Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
|
||
| ## Формирование метрики | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я решил использовать профилировщик rubyprof в режиме memory, а так же выводить потребление памяти системным процессом в нескольких местах кода | ||
|
|
||
| ## Гарантия корректности работы оптимизированной программы | ||
| Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
|
||
| ## Feedback-Loop | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений примерно за 10 секунд | ||
|
|
||
| Вот как я построил `feedback_loop`: | ||
| Я разбил файл data_large.txt на несколько файлов с 1%, 5%, 10% и 50% от общего объема данных | ||
| Замеры начинались на 1% данных, что позволяло уложиться в 10 сек. | ||
| При видимом улучшении показателя потребляемой памяти изменения коммитились | ||
| При значительном снижении потребляемой памяти и времени работы программы постеменно увеличивался объем данных | ||
|
|
||
| ## Вникаем в детали системы, чтобы найти главные точки роста | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался отчетом ruby-prof в формате cachegrind | ||
|
|
||
| Первые замеры (1% данных): | ||
| - время выполнения программы: 10.38 сек | ||
| - потребление памяти: 420мб | ||
|
|
||
| Вот какие проблемы удалось найти и решить | ||
|
|
||
| ### Ваша находка №1 | ||
| - Из отчета стало очевидно, что считывать файл в память, фомировать отчеты и уже после записывать в файл - слишком "дорого" | ||
| - Было решено переделать программу на формирование отчета по каждому пользователю, записи этого отчета в файл, обнуления пользователя в памяти и переход к следующему | ||
| Так же для отчета по статистике были использованы переменные-счетчики, которые обновлялись по мере чтения информации по каждому пользователю, а для хранения уникальных названий браузеров был выбран `SortedSet` | ||
| Отчет по пользователям писался в отдельный файл, после чего в финальный отчет записывался отчет по статистике, считывался файл с отчетами по каждому пользователю и последовательно дописывался в финальный отчет | ||
| - В результате время выполнения программы и потребление памяти значительно сократились, но до попадания в бюджет было еще далеко | ||
|
|
||
| Замеры во итогу (полный объем данных): | ||
| - время выполнения программы: 6.65 сек | ||
| - потребление памяти: 169 мб | ||
|
|
||
| ### Ваша находка №2 | ||
| - отчет rubyprof показал, что основное потребление памяти связано с повторным чтением из файла (`each_line`), в который писались отчеты по пользователям | ||
| стало очевидно, что использование временного вспомогательного файла хоть и дало результаты, но их явно недостаточно | ||
| - было решено писать отчеты по пользователям сразу в итоговый отчет, так же использовать счетчики для отчета по статистике и записывать статистику уже в конец файла. Это позволило бы избавиться от лишних операций с дополнительным файлом | ||
| - после некоторых "мучений" с формированием обоих отчетов в один синтаксически корректный .json файл результат был достигнут | ||
| - повторные запуски теста с выводом потребления памяти системным процессом показывали разные результаты, но каждый раз укладывались в бюджет | ||
|
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 работает не всегда предсказуемо |
||
|
|
||
| Замеры по итогу (полный объем данных) | ||
| - время работы программы: 6.4 сек | ||
| - потребление памяти от 19 до 36 мб | ||
|
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. Судя по картинке из Valgrind там программа на самом деле не работает Может быть файл не пробрасывается или что-то в таком роде "Правильная картинка" - примерно ровная линия в районе 30-40MB Главное, что потребление памяти практически не зависит от объёма входных данных и не растёт по мере работы программы
Author
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. Сейчас перепроверил еще раз, отключил тест, чтобы файл отчета не перезатирался. На выходе получил json на 151 мб. Это может как-то быть связано с тем, что у меня М1? может он как-то хитро с памятью работает через своп и профилировщик это не отслеживает? При этом пик по памяти показывает 4мб |
||
|
|
||
| ## Результаты | ||
| В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
| Удалось улучшить метрику системы с 420 мб на 1% данных, до ~30 мб на полном объеме и уложиться в заданный бюджет. | ||
|
|
||
| Время выполнения программы так же значительно сократилось. | ||
|
|
||
| ## Защита от регрессии производительности | ||
| Так как отчеты по потребляемой памяти разнились от запуска к запуску, для защиты от потери достигнутого прогресса при дальнейших изменениях программы использовались максимальные из полученных значения по памяти, а так же простой тест на корректность формирования самого отчета | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # Optimized version of homework task | ||
|
|
||
| require 'json' | ||
| require 'pry' | ||
| require 'set' | ||
| require 'minitest/autorun' | ||
|
|
||
| DELIMITER = ',' | ||
| WITH_SPACE = ', ' | ||
| IE = 'I' | ||
| CR = 'C' | ||
|
|
||
| def work(path) | ||
| file = File.open(path) | ||
| @report_file = File.open('result.json', 'w') | ||
| @report_file.write('{"usersStats":{') | ||
| @user_report = {} | ||
| @users_count = 0 | ||
| @sessions_count = 0 | ||
| @all_browsers = SortedSet.new | ||
|
|
||
| file.each_line(chomp: true) do |line| | ||
| cols = line.split(DELIMITER) | ||
| if line.start_with?('u') | ||
| @users_count += 1 | ||
| write_user_report(false) unless @user_report == {} | ||
| @user_name = "#{cols[2]} #{cols[3]}" | ||
| @user_report = {} | ||
| @user_report[@user_name] = initialize_user_report | ||
| elsif line.start_with?('s') | ||
| @sessions_count += 1 | ||
| @user_report[@user_name]['sessionsCount'] += 1 | ||
| @user_report[@user_name]['totalTime'] += cols[4].to_i | ||
| @user_report[@user_name]['longestSession'] = longest_session(cols[4].to_i) | ||
| @user_report[@user_name]['browsers'] << cols[3].upcase! | ||
| @user_report[@user_name]['usedIE'] = true if used_ie?(cols[3]) | ||
| @user_report[@user_name]['alwaysUsedChrome'] = only_chrome?(@user_report[@user_name]['browsers']) | ||
| @user_report[@user_name]['dates'] << cols[5] | ||
| @all_browsers << cols[3] | ||
| end | ||
| end | ||
| write_user_report(true) | ||
| @report_file.write(DELIMITER, summary_report.to_json[1..-1]) | ||
| @report_file.close | ||
|
|
||
| puts 'MEMORY USAGE: %d MB' % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) | ||
| end | ||
|
|
||
| def longest_session(time) | ||
| @user_report[@user_name]['longestSession'] < time ? time : @user_report[@user_name]['longestSession'] | ||
| end | ||
|
|
||
| def used_ie?(browser) | ||
| browser.start_with?(IE) | ||
| end | ||
|
|
||
| def only_chrome?(browsers) | ||
| return false if @user_report[@user_name]['usedIE'] | ||
|
|
||
| browsers.all? { |b| b.start_with?(CR) } | ||
| end | ||
|
|
||
| def initialize_user_report | ||
| { | ||
| 'sessionsCount' => 0, | ||
| 'totalTime' => 0, | ||
| 'longestSession' => 0, | ||
| 'browsers' => [], | ||
| 'usedIE' => false, | ||
| 'alwaysUsedChrome' => false, | ||
| 'dates' => [] | ||
| } | ||
| end | ||
|
|
||
| def write_user_report(last) | ||
| prepare_report | ||
| if last | ||
| @report_file.write(@user_report.to_json[1..-2], '}') | ||
| else | ||
| @report_file.write(@user_report.to_json[1..-2], DELIMITER) | ||
| end | ||
| end | ||
|
|
||
| def summary_report | ||
| { | ||
| 'totalUsers' => @users_count, | ||
| 'uniqueBrowsersCount' => @all_browsers.size, | ||
| 'totalSessions' => @sessions_count, | ||
| 'allBrowsers' => @all_browsers.to_a.join(DELIMITER) | ||
| } | ||
| end | ||
|
|
||
| def prepare_report | ||
| @user_report[@user_name]['totalTime'] = "#{@user_report[@user_name]['totalTime']} min." | ||
| @user_report[@user_name]['longestSession'] = "#{@user_report[@user_name]['longestSession']} min." | ||
| @user_report[@user_name]['browsers'] = @user_report[@user_name]['browsers'].sort!.join(WITH_SPACE) | ||
| @user_report[@user_name]['dates'] = @user_report[@user_name]['dates'].sort!.reverse! | ||
| end | ||
|
|
||
| class TestMe < Minitest::Test | ||
| def setup | ||
| File.write('result.json', '') | ||
| File.write('data.txt', | ||
| 'user,0,Leida,Cira,0 | ||
| session,0,0,Safari 29,87,2016-10-23 | ||
| session,0,1,Firefox 12,118,2017-02-27 | ||
| session,0,2,Internet Explorer 28,31,2017-03-28 | ||
| session,0,3,Internet Explorer 28,109,2016-09-15 | ||
| session,0,4,Safari 39,104,2017-09-27 | ||
| session,0,5,Internet Explorer 35,6,2016-09-01 | ||
| user,1,Palmer,Katrina,65 | ||
| session,1,0,Safari 17,12,2016-10-21 | ||
| session,1,1,Firefox 32,3,2016-12-20 | ||
| session,1,2,Chrome 6,59,2016-11-11 | ||
| session,1,3,Internet Explorer 10,28,2017-04-29 | ||
| session,1,4,Chrome 13,116,2016-12-28 | ||
| user,2,Gregory,Santos,86 | ||
| session,2,0,Chrome 35,6,2018-09-21 | ||
| session,2,1,Safari 49,85,2017-05-22 | ||
| session,2,2,Firefox 47,17,2018-02-02 | ||
| session,2,3,Chrome 20,84,2016-11-25 | ||
| ') | ||
| end | ||
|
|
||
| def test_result | ||
| work('data.txt') | ||
| expected_result = 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"]}}}') | ||
| assert_equal expected_result, JSON.parse(File.read('result.json')) | ||
| end | ||
| end |
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.
👍