diff --git a/case-study.md b/case-study.md new file mode 100644 index 0000000..72ab2a0 --- /dev/null +++ b/case-study.md @@ -0,0 +1,71 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: ruby_prof + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: +1. Сделал копию рабочего файла, которая принимает аргументом файл меньшего размера. +2. Запускал выполнение метода и замерял время выполнения +3. С помощью отчетов ruby-prof.rb находил точку роста, исправлял ее и запускал по новой; пока время не стало укладываться в бюджет. + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался отчетами ruby-prof - flat, graph и callstack + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- Все отчеты показали главную точку роста Array#select (92.75% по ruby-prof flat) +- Вместо перебора сессий в select создал массив sessins_hash c ключом user_id +- Время выполнения уменьшилось с 280 сек до 15.85 сек +- Проблема перестала быть точкой роста + +### Ваша находка №2 +- ruby-prof flat показал главную точку роста Array#each (81.26%) +- увидел, что в парсинге делаются повторяющиеся действия, избавился от этого +- Время выполнения уменьшилось с 15.85 сек до 1.98 сек +- Проблема перестала быть точкой роста + +### Ваша находка №3 +- ruby-prof flat показал главную точку роста Array#all? (36.75%) +- заменил работу с массивом на работу с Set для добавления уникальных браузеров +- Время выполнения уменьшилось с 1.98 сек до 1.4 сек +- Проблема перестала быть точкой роста + +### Ваша находка №4 +- ruby-prof flat показал главную точку роста Array#each, зависящую от Array#map (23.39%) +- заменил работу с промежуточными массивами через map +- Время выполнения уменьшилось с 1.4 сек до 0.68 сек +- Проблема перестала быть точкой роста + +### Ваша находка №X +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- Проблема перестала быть точкой роста + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с очень большого времени до 35 сек, что почти укладывается в заданный бюджет. + +*Какими ещё результами можете поделиться* + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* + diff --git a/task-1.rb b/task-1.rb index 778672d..94e3b9a 100644 --- a/task-1.rb +++ b/task-1.rb @@ -14,9 +14,8 @@ def initialize(attributes:, sessions:) end end -def parse_user(user) - fields = user.split(',') - parsed_result = { +def parse_user(fields) + { 'id' => fields[1], 'first_name' => fields[2], 'last_name' => fields[3], @@ -24,9 +23,8 @@ def parse_user(user) } end -def parse_session(session) - fields = session.split(',') - parsed_result = { +def parse_session(fields) + { 'user_id' => fields[1], 'session_id' => fields[2], 'browser' => fields[3], @@ -44,17 +42,17 @@ def collect_stats_from_users(report, users_objects, &block) end def work - file_lines = File.read('data.txt').split("\n") - users = [] sessions = [] - file_lines.each do |line| + File.read('data_large.txt').split("\n").each do |line| cols = line.split(',') - users = users + [parse_user(line)] if cols[0] == 'user' - sessions = sessions + [parse_session(line)] if cols[0] == 'session' + users = users << parse_user(cols) if cols[0] == 'user' + sessions = sessions << parse_session(cols) if cols[0] == 'session' end + sessions_hash = sessions.group_by { |session| session['user_id'] } + # Отчёт в json # - Сколько всего юзеров + # - Сколько всего уникальных браузеров + @@ -75,11 +73,8 @@ def work report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end + uniqueBrowsers = Set.new + sessions.each { |session| uniqueBrowsers.add(session['browser']) } report['uniqueBrowsersCount'] = uniqueBrowsers.count @@ -94,50 +89,40 @@ def work .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] + users_objects = users.map do |user| + User.new(attributes: user, sessions: sessions_hash[user['id']] || []) end report['usersStats'] = {} - # Собираем количество сессий по пользователям collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } - end - + user_sessions = user.sessions # Собираем количество времени по пользователям - 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 + totalTime = user_sessions.map {|s| s['time'].to_i}.sum.to_s + ' min.' # Выбираем самую длинную сессию пользователя - 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 - + longestSession = user.sessions.map { |s| s['time'].to_i }.max.to_s + ' min.' # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end + browsers = user_sessions.map {|s| s['browser'].upcase}.sort # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end + usedIE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } # Всегда использовал только 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 } } + alwaysUsedChrome = browsers.all? { |b| b =~ /CHROME/ } + + # Даты сессий через запятую в обратном порядке в формате iso8601| + dates = user_sessions.map{|s| s['date']}.sort.reverse + + { + 'sessionsCount' => user_sessions.count, + 'totalTime' => totalTime, + 'longestSession' => longestSession, + 'browsers' => browsers.join(', '), + 'usedIE' => usedIE, + 'alwaysUsedChrome' => alwaysUsedChrome, + 'dates' => dates + } end File.write('result.json', "#{report.to_json}\n") diff --git a/task-1_with_file.rb b/task-1_with_file.rb new file mode 100644 index 0000000..bd80156 --- /dev/null +++ b/task-1_with_file.rb @@ -0,0 +1,165 @@ +# Deoptimized version of homework task + +require 'json' +require 'pry' +require 'date' +require 'minitest/autorun' + +class User + attr_reader :attributes, :sessions + + def initialize(attributes:, sessions:) + @attributes = attributes + @sessions = sessions + end +end + +def parse_user(fields) + { + 'id' => fields[1], + 'first_name' => fields[2], + 'last_name' => fields[3], + 'age' => fields[4], + } +end + +def parse_session(fields) + { + 'user_id' => fields[1], + 'session_id' => fields[2], + 'browser' => fields[3], + 'time' => fields[4], + 'date' => fields[5], + } +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(data_file) + # file_lines = File.read('data.txt').split("\n") + + users = [] + sessions = [] + + File.read(data_file).split("\n").each do |line| + cols = line.split(',') + users = users << parse_user(cols) if cols[0] == 'user' + sessions = sessions << parse_session(cols) if cols[0] == 'session' + end + + sessions_hash = sessions.group_by { |session| session['user_id'] } + + # Отчёт в json + # - Сколько всего юзеров + + # - Сколько всего уникальных браузеров + + # - Сколько всего сессий + + # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + + # + # - По каждому пользователю + # - сколько всего сессий + + # - сколько всего времени + + # - самая длинная сессия + + # - браузеры через запятую + + # - Хоть раз использовал IE? + + # - Всегда использовал только Хром? + + # - даты сессий в порядке убывания через запятую + + + report = {} + + report[:totalUsers] = users.count + + # Подсчёт количества уникальных браузеров + + uniqueBrowsers = Set.new + sessions.each { |session| uniqueBrowsers.add(session['browser']) } + + 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.map do |user| + User.new(attributes: user, sessions: sessions_hash[user['id']] || []) + end + + report['usersStats'] = {} + + collect_stats_from_users(report, users_objects) do |user| + user_sessions = user.sessions + # Собираем количество времени по пользователям + totalTime = user_sessions.map {|s| s['time'].to_i}.sum.to_s + ' min.' + + # Выбираем самую длинную сессию пользователя + longestSession = user.sessions.map { |s| s['time'].to_i }.max.to_s + ' min.' + # Браузеры пользователя через запятую + browsers = user_sessions.map {|s| s['browser'].upcase}.sort + + # Хоть раз использовал IE? + usedIE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } + + # Всегда использовал только Chrome? + alwaysUsedChrome = browsers.all? { |b| b =~ /CHROME/ } + + # Даты сессий через запятую в обратном порядке в формате iso8601| + dates = user_sessions.map{|s| s['date']}.sort.reverse + + { + 'sessionsCount' => user_sessions.count, + 'totalTime' => totalTime, + 'longestSession' => longestSession, + 'browsers' => browsers.join(', '), + 'usedIE' => usedIE, + 'alwaysUsedChrome' => alwaysUsedChrome, + 'dates' => dates + } + end + + File.write('result.json', "#{report.to_json}\n") +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 + work("data_large.txt") + # expected_result = '{"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" + # assert_equal expected_result, File.read('result.json') + end +end