diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79f2c21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.ruby-version + +*.html +*.txt +*.json + +.byebug_history diff --git a/case-study-template.md b/case-study-template.md index e0eef00..dce5cbd 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,35 +12,68 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: +- измерение общего времени выполнения с помощью `Benchmark.realtime` +- профилирование программы с помощью `ruby-prof` + +Время выполнения исходного кода на тестовом файле 50000 строк(2Мb) составляет: 59-61 секунд. ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за время меньшее 1 минуты. -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +Вот как я построил `feedback_loop`: +- тестовый файл 50000 строк(2Мb), исходное время выполнения 59-61 секунд +- поиск самого узкого места +- улучшение кода +- замеры метрик +- запуск тестов +- анализ результатов ## Вникаем в детали системы, чтобы найти 20% точек роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался библиотеками benchmark, ruby-prof. Вот какие проблемы удалось найти и решить ### Ваша находка №1 -О вашей находке №1 +Больше всего потребление памяти занимает формирование массивов пользователей и сессий. +Решение, замена конкатенации на модификацию массивов `users` и `sessions` 'in place' с помощью `Array#push` ### Ваша находка №2 -О вашей находке №2 +Удаляем при парсинге не успользуемые в дальнейшем данные. + +### Ваша находка №3 +Улучшаем алгоритм работы программы - начинаем хранить сессии в хэше, вместо массивов. + +### Ваша находка №4 +Удалось добиться небольшого ускорения, собрав всю статистику пользователя за одну итерацию (вместо нескольких для каждого вида статистики). -### Ваша находка №X -О вашей находке №X +### Ваша находка №5 +Оптимизация регулярок (замена `=~` на `match?`, вынос регулярного выражения вне цикла) + +### Ваша находка №6 +Чтение файла построчно позволяет сэкономить немного памяти и ускорить работу. ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* - -*Какими ещё результами можете поделиться* +Удалось улучшить метрику системы: + +Для тестового файла 50000 строк(2Мb), исходное время выполнения 59-61 секунд, до 0,54 секунд +``` +Start +rss before: 17 MB +rss after: 61 MB +Finish in 0.54 s +``` +Для основного файла до 55 секунд. +``` +Start +rss before: 16 MB +rss after: 2231 MB +Finish in 55.53 s +``` ## Защита от регресса производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавлен regress тест. diff --git a/task-1.rb b/task-1.rb index 778672d..23830e3 100644 --- a/task-1.rb +++ b/task-1.rb @@ -1,9 +1,9 @@ -# Deoptimized version of homework task - require 'json' -require 'pry' require 'date' require 'minitest/autorun' +require 'byebug' +require 'ruby-prof' +require 'benchmark' class User attr_reader :attributes, :sessions @@ -15,129 +15,76 @@ def initialize(attributes:, sessions:) end def parse_user(user) - fields = user.split(',') - parsed_result = { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], + { + id: user[1], + full_name: "#{user[2]} #{user[3]}" } 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], + { + session_id: session[2], + browser: session[3].upcase!, + time: session[4].to_i, + date: session[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)) +def collect_stats_from_users(report, users_objects) + while users_objects.size.positive? + user = users_objects.shift + report[:usersStats][user.attributes[:full_name]] ||= {} + report[:usersStats][user.attributes[:full_name]].merge!(yield(user)) end end -def work - file_lines = File.read('data.txt').split("\n") - +def work(file_path) 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' + sessions = Hash.new { |hash, key| hash[key] = [] } + sessions_count = 0 + + File.foreach(file_path) do |line| + line = line.split(',') + users << parse_user(line) if line[0] == 'user' + if line[0] == 'session' + sessions[line[1]] << parse_session(line) + sessions_count += 1 + 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['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.' } + unique_browsers = sessions.values.flatten.map! { |session| session[:browser] }.uniq! || [] + report[:uniqueBrowsersCount] = unique_browsers.count + report[:totalSessions] = sessions_count + report[:allBrowsers] = unique_browsers.sort!.join(',') + + users.map! do |user| + user_id = user[:id] + user_sessions = sessions[user_id] + User.new(attributes: user, sessions: user_sessions) 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 } } + report[:usersStats] = {} + + collect_stats_from_users(report, users) do |user| + users_times = user.sessions.map { |s| s[:time] } + users_browsers = user.sessions.map { |s| s[:browser] } + ie_count = 0 + chrome_count = 0 + users_browsers.each do |b| + ie_count += 1 if /INTERNET EXPLORER/.match?(b) + chrome_count += 1 if /CHROME/.match?(b) + end + + { + 'sessionsCount' => user.sessions.count, + 'totalTime' => "#{users_times.sum} min.", + 'longestSession' => "#{users_times.max} min.", + 'browsers' => users_browsers.sort!.join(', '), + 'usedIE' => ie_count.positive?, + 'alwaysUsedChrome' => chrome_count == users_browsers.size, + 'dates' => user.sessions.map { |s| Date.parse(s[:date]).iso8601 }.sort!.reverse! + } end File.write('result.json', "#{report.to_json}\n") @@ -169,8 +116,35 @@ def setup end def test_result - work + work('data.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 + + def test_regress + time = Benchmark.realtime { work('data.txt') } + assert time < 0.001, 'Test regress' + end +end + +def print_memory_usage + '%d MB' % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end + +# RubyProf.measure_mode = RubyProf::MEMORY + +puts 'Start' +result_profil = nil + +time = Benchmark.realtime do + puts "rss before: #{print_memory_usage}" + # result_profil = RubyProf.profile do + work('data_large.txt') + # end + puts "rss after: #{print_memory_usage}" +end + +puts "Finish in #{time.round(2)}" + +# printer = RubyProf::GraphHtmlPrinter.new(result_profil) +# printer.print(File.open('ruby_prof_graph_allocations_profile.html', 'w+'))