diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c25e27e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +README.md +data_large.txt +data.txt +data-10000.txt +Gemfile +Gemfile.lock +/tests +memory-profiler.rb +.DS_Store +result.json +stackprof.rb +rspec.rb +rspec-perf.rb diff --git a/case-study-template.md b/case-study-template.md deleted file mode 100644 index c3279664..00000000 --- a/case-study-template.md +++ /dev/null @@ -1,55 +0,0 @@ -# Case-study оптимизации - -## Актуальная проблема -В нашем проекте возникла серьёзная проблема. - -Необходимо было обработать файл с данными, чуть больше ста мегабайт. - -У нас уже была программа на `ruby`, которая умела делать нужную обработку. - -Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. - -Я решил исправить эту проблему, оптимизировав эту программу. - -## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* - -## Гарантия корректности работы оптимизированной программы -Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. - -## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* - -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* - -## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* - -Вот какие проблемы удалось найти и решить - -### Ваша находка №1 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -### Ваша находка №2 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -## Результаты -В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. - -*Какими ещё результами можете поделиться* - -## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..09d21020 --- /dev/null +++ b/case-study.md @@ -0,0 +1,91 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: оперативная память + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 15 секунд + +Вот как я построил `feedback_loop`: +- замер потребляемой памяти профилировщиком +- выяснение точки роста +- поиск возможности оптимизации точки роста +- при возможности, оптимизация точки роста + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался +- gem 'memory_profiler' +- gem 'ruby-prof' +- gem 'stackprof' + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- memory_profiler показал точку роста в загрузке всего файла +- переделать программу на построчное считывание +- изначально при чтении файла из 10000 строк потреблялось 441.91 MB, после изменения программы на построчное считывание стало 16.47 MB +- изменилась главна точка роста + +### Ваша находка №2 +- stackprof показал точку роста в split(',') +- убрал лишние split(','), оставил только в одном месте +- кол-во аллокаций уменьшилось с 93104 до 67712 +- кол-во потребляемой памяти уменьшилось с 16.47 MB до 13.09 MB +- пробовал заменить (',') на регулярное выражение, но память увеличиласть на 1 мб +- точка роста осталась там же + +### Ваша находка №3 +- memory_profiler показал что создается много одинаковых срок +- добавил frozen_string_literal: true +- было 179224 в классе String, стало 146149 +- потребление памяти уменьшилось с 13.09 MB до 11.77 MB + +### Ваша находка №4 +- memory_profiler показал что создается много одинаковых срок +- добавил frozen_string_literal: true +- было 179224 в классе String, стало 146149 +- потребление памяти уменьшилось с 13.09 MB до 11.77 MB + +### Ваша находка №5 +- memory_profiler показал что создается много объектов в строке присвоения user_key +- заменил на интерполяцию +- объектов было 6144, стало 1536 +- потребление памяти уменьшилось незначительно с 11.77 MB до 11.52 MB + +### Ваша находка №6 +- memory-profiler показал что создается 4438 объектов при сортировке браузеров +- заменил на sort! +- объектов было 4438, стало 2902 +- потребление памяти уменьшилось незначительно с 11.52 MB до 11.40 MB + +### Ваша находка №7 +- memory-profiler показал что uniq потребляет много памяти +- было решено заменить uniq на Set как в первом задании +- потребление памяти уменьшилось незначительно с 11.40 MB до 11.15 MB + +### Ваша находка №8 +- memory-profiler показал что все еще не укладываемся в метрику, много памяти уходит на запись в файл +- было решено перевести запись в файл на постепенную, по мере поступления результатов +- потребление памяти уменьшилось незначительно с 11.40 MB до 5.83 MB + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с 441.91 MB до 5.83 MB и уложиться в заданный бюджет. Так же удалось довести время выполнения до заданных 30 сек. +xquartz запустить не удалось, может на big sur не идет. Однако, был найден онлайн аналог http://boutglay.com/massifjs/ который почему то не запускается с русского ip, но мы и не таких видали, поэтому скриншоты прилагаю. В итоге, удалось снизить постребление памяти до ~38,6 MB. + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы тесты на rspec. diff --git a/data_large.txt.gz b/data_large.txt.gz deleted file mode 100644 index 91c7e45e..00000000 Binary files a/data_large.txt.gz and /dev/null differ diff --git a/task-2.rb b/task-2.rb index 34e09a3c..1d95de8e 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,177 +1,140 @@ # Deoptimized version of homework task +# frozen_string_literal: true require 'json' require 'pry' require 'date' -require 'minitest/autorun' - -class User - attr_reader :attributes, :sessions - - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions - end +require 'set' +# require 'minitest/autorun' + +def get_user_stats!(users_stats) + users_stats[:longestSession] = "#{users_stats[:longestSession]} min." + users_stats[:totalTime] = "#{users_stats[:totalTime]} min." + users_stats[:browsers] = users_stats[:browsers].sort!.join(', ') + users_stats[:dates] = users_stats[:dates].sort!.reverse! end -def parse_user(user) - fields = user.split(',') - parsed_result = { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], +def work(filepath = 'data_large.txt') + report = { + totalUsers: 0, + uniqueBrowsersCount: Set.new, + totalSessions: 0, + allBrowsers: Set.new, } -end -def parse_session(session) - fields = session.split(',') - parsed_result = { - 'user_id' => fields[1], - 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[5], + users_stats = { + sessionsCount: 0, + totalTime: 0, + longestSession: 0, + browsers: [], + usedIE: false, + alwaysUsedChrome: true, + dates: [] } -end - -def collect_stats_from_users(report, users_objects, &block) - users_objects.each do |user| - user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" - report['usersStats'][user_key] ||= {} - report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) - end -end - -def work - file_lines = File.read('data.txt').split("\n") - - users = [] - sessions = [] - file_lines.each do |line| - cols = line.split(',') - users = users + [parse_user(line)] if cols[0] == 'user' - sessions = sessions + [parse_session(line)] if cols[0] == 'session' + user_key = nil + user_check = nil + session_check = nil + col = 0 + output = File.open('result.json', 'w') + output.write('{"usersStats":{') + + File.foreach(filepath) do |line| + line.split(',') do |item| + col += 1 + if item == 'user' + if session_check + get_user_stats!(users_stats) + output.write("\"#{user_key}\":#{users_stats.to_json},") + end + user_check = true + session_check = false + users_stats = { + sessionsCount: 0, + totalTime: 0, + longestSession: 0, + browsers: [], + usedIE: false, + alwaysUsedChrome: true, + dates: [] + } + user_key = nil + col = 0 + report[:totalUsers] += 1 + next + end + if item == 'session' + user_check = false + session_check = true + col = 0 + report[:totalSessions] += 1 + users_stats[:sessionsCount] += 1 + next + end + + if user_check + case col + when 2 + user_key = item + when 3 + user_key = "#{user_key} #{item}" + end + end + + if session_check + case col + when 3 + report[:allBrowsers].add(item.upcase!) + report[:uniqueBrowsersCount].add(item) + users_stats[:browsers] << item + users_stats[:usedIE] ||= item.match?(/INTERNET EXPLORER/) + users_stats[:alwaysUsedChrome] &&= item.match?(/CHROME/) + when 4 + users_stats[:totalTime] += item.to_i + users_stats[:longestSession] = item.to_i if users_stats[:longestSession] < item.to_i + when 5 + users_stats[:dates] << item + end + end + end end - # Отчёт в json - # - Сколько всего юзеров + - # - Сколько всего уникальных браузеров + - # - Сколько всего сессий + - # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + - # - # - По каждому пользователю - # - сколько всего сессий + - # - сколько всего времени + - # - самая длинная сессия + - # - браузеры через запятую + - # - Хоть раз использовал IE? + - # - Всегда использовал только Хром? + - # - даты сессий в порядке убывания через запятую + - - report = {} - - report[:totalUsers] = users.count - - # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end - - report['uniqueBrowsersCount'] = uniqueBrowsers.count - - report['totalSessions'] = sessions.count - - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') - - # Статистика по пользователям - users_objects = [] - - users.each do |user| - attributes = user - user_sessions = sessions.select { |session| session['user_id'] == user['id'] } - user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects = users_objects + [user_object] - end + report[:uniqueBrowsersCount] = report[:uniqueBrowsersCount].size + report[:allBrowsers] = report[:allBrowsers].sort.join(',') - report['usersStats'] = {} - - # Собираем количество сессий по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } - end - - # Собираем количество времени по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' } - end - - # Выбираем самую длинную сессию пользователя - collect_stats_from_users(report, users_objects) do |user| - { 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' } - end - - # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end - - # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end - - # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } - end - - # Даты сессий через запятую в обратном порядке в формате iso8601 - collect_stats_from_users(report, users_objects) do |user| - { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } - end - - File.write('result.json', "#{report.to_json}\n") - puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + get_user_stats!(users_stats) + output.write("\"#{user_key}\":#{users_stats.to_json}},#{report.to_json[1..-1]}") + output.close 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 - 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 +# 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 diff --git "a/\320\261\321\213\320\273\320\276.png" "b/\320\261\321\213\320\273\320\276.png" new file mode 100644 index 00000000..97c6b4f4 Binary files /dev/null and "b/\320\261\321\213\320\273\320\276.png" differ diff --git "a/\321\201\321\202\320\260\320\273\320\276.png" "b/\321\201\321\202\320\260\320\273\320\276.png" new file mode 100644 index 00000000..63a0bca5 Binary files /dev/null and "b/\321\201\321\202\320\260\320\273\320\276.png" differ