From cd4371d27eb2feb13f96864a7502ec7dfb38122d Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Sun, 3 Mar 2019 20:36:44 +0500 Subject: [PATCH 01/17] Add ruby environment to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45e2ac1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.ruby-version +.ruby-gemset From 318074d64206d0b5c6029ec3b4a008f2fa096f53 Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Sun, 3 Mar 2019 20:47:05 +0500 Subject: [PATCH 02/17] Add data files to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 45e2ac1..741c81c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .ruby-version .ruby-gemset + +data.txt +result.json From 37d3a71bed7fc4a7acc94f9eb1968adea0e3b42d Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Mon, 4 Mar 2019 00:30:12 +0500 Subject: [PATCH 03/17] Add regression tests --- .gitignore | 6 ++++-- task-1.rb | 29 ++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 741c81c..eb915fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .ruby-version .ruby-gemset -data.txt -result.json +*.txt +*.json +*.html +*.callgrind* diff --git a/task-1.rb b/task-1.rb index 778672d..6719e3d 100644 --- a/task-1.rb +++ b/task-1.rb @@ -4,6 +4,8 @@ require 'pry' require 'date' require 'minitest/autorun' +require 'ruby-prof' +require 'benchmark' class User attr_reader :attributes, :sessions @@ -169,8 +171,33 @@ def setup end def test_result - work + profiling_result = RubyProf.profile { work } + 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') + + postfix = RubyProf.measure_mode == RubyProf::MEMORY ? 'memory' : 'time' + printer = RubyProf::CallTreePrinter.new(profiling_result) + printer.print(path: '.', profile: "profile_#{postfix}") + end + + def test_regression + before_mem = after_mem = nil + time = Benchmark.realtime do + before_mem = used_memory + 1000.times { work } + after_mem = used_memory + end + + assert time < 0.8, "Expect execution time to be less than 0.8, got #{time}" + mem_diff = after_mem - before_mem + puts "RSS diff #{mem_diff} KB" + assert mem_diff < 1700, "Expect to not use more than 1700 KB of memory, got #{mem_diff} KB" + end + + private + + def used_memory + `ps -o rss= -p #{Process.pid}`.to_i end end From 2344bd6408ebf7b80ca5306a4e57f03442c65b6d Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Tue, 5 Mar 2019 22:19:50 +0500 Subject: [PATCH 04/17] Improve setup --- .gitignore | 2 ++ task-1.rb | 50 ++++++++++++++++++++++++++------------------------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index eb915fa..64af563 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ *.json *.html *.callgrind* + +.DS_Store diff --git a/task-1.rb b/task-1.rb index 6719e3d..8f61291 100644 --- a/task-1.rb +++ b/task-1.rb @@ -45,8 +45,8 @@ def collect_stats_from_users(report, users_objects, &block) end end -def work - file_lines = File.read('data.txt').split("\n") +def work(file_path) + file_lines = File.read(file_path).split("\n") users = [] sessions = [] @@ -145,6 +145,12 @@ def work File.write('result.json', "#{report.to_json}\n") end +module MemoryMeasure + def self.call + `ps -o rss= -p #{Process.pid}`.to_i + end +end + class TestMe < Minitest::Test def setup File.write('result.json', '') @@ -171,33 +177,29 @@ def setup end def test_result - profiling_result = RubyProf.profile { 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') - - postfix = RubyProf.measure_mode == RubyProf::MEMORY ? 'memory' : 'time' - printer = RubyProf::CallTreePrinter.new(profiling_result) - printer.print(path: '.', profile: "profile_#{postfix}") + assert_equal expected_result, File.read('result.json') end +end - def test_regression - before_mem = after_mem = nil - time = Benchmark.realtime do - before_mem = used_memory - 1000.times { work } - after_mem = used_memory - end - - assert time < 0.8, "Expect execution time to be less than 0.8, got #{time}" - mem_diff = after_mem - before_mem - puts "RSS diff #{mem_diff} KB" - assert mem_diff < 1700, "Expect to not use more than 1700 KB of memory, got #{mem_diff} KB" +if ENV['MEASURE'] + puts '=' * 20 + before_mem = after_mem = profiling_result = nil + time = Benchmark.realtime do + before_mem = MemoryMeasure.call + profiling_result = RubyProf.profile { work(ENV['MEASURE']) } + after_mem = MemoryMeasure.call end - private + puts "Time taken: #{time}" - def used_memory - `ps -o rss= -p #{Process.pid}`.to_i - end + mem_diff = after_mem - before_mem + puts "RSS diff #{mem_diff} KB" + puts '=' * 20 + + postfix = RubyProf.measure_mode == RubyProf::MEMORY ? 'memory' : 'time' + printer = RubyProf::CallTreePrinter.new(profiling_result) + printer.print(path: '.', profile: "profile_#{postfix}") end From a3ce92573405033e6669e6673685409fb21a776f Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Tue, 5 Mar 2019 23:16:57 +0500 Subject: [PATCH 05/17] First iteration --- case-study-template.md | 17 ++++++++++++----- task-1.rb | 21 +++++++++++++++------ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index e0eef00..20a4b6c 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,7 +12,10 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: +- измерение общего времени выполнения с помощью `Benchmark.realtime` +- измерение общей потребляемой памяти с помощью `MemoryMeasure` +- профилирование программы с помощью `ruby-prof` и `qcachegrind` ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. @@ -20,15 +23,19 @@ ## Feedback-Loop Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +Вот как я построил `feedback_loop`: +1. Построение гипотезы +2. Исправление кода +3. Запуск скрипта на тестовом файле небольшого объема (1 мб) +4. Сопоставление новых метрик с предыдущими ## Вникаем в детали системы, чтобы найти 20% точек роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался библиотеками benchmark, ruby-prof и инструментом визуализации qcachegrind Вот какие проблемы удалось найти и решить ### Ваша находка №1 -О вашей находке №1 +Львиную долю времени занимает вложенная итерация сессий при переборе пользователей. ### Ваша находка №2 О вашей находке №2 @@ -38,7 +45,7 @@ ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* +Удалось улучшить метрику системы с 20 сек и 36 МБ RSS. *Какими ещё результами можете поделиться* diff --git a/task-1.rb b/task-1.rb index 8f61291..0ba06ec 100644 --- a/task-1.rb +++ b/task-1.rb @@ -50,11 +50,20 @@ def work(file_path) users = [] sessions = [] + user_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' + case cols[0] + when 'user' + users = users + [parse_user(line)] + when 'session' + session = parse_session(line) + user_id = session['user_id'] + user_sessions[user_id] ||= [] + user_sessions[user_id] << session + sessions << session + end end # Отчёт в json @@ -100,8 +109,8 @@ def work(file_path) 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) + currnet_user_sessions = Array(user_sessions[user['id']]) + user_object = User.new(attributes: attributes, sessions: currnet_user_sessions) users_objects = users_objects + [user_object] end @@ -188,12 +197,12 @@ def test_result puts '=' * 20 before_mem = after_mem = profiling_result = nil time = Benchmark.realtime do - before_mem = MemoryMeasure.call + before_mem = MemoryMeasure.call profiling_result = RubyProf.profile { work(ENV['MEASURE']) } after_mem = MemoryMeasure.call end - puts "Time taken: #{time}" + puts "Time taken: #{time.round(2)}" mem_diff = after_mem - before_mem puts "RSS diff #{mem_diff} KB" From 5876740b895c02940dbd4118894fb31f5fd38fca Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Tue, 5 Mar 2019 23:41:59 +0500 Subject: [PATCH 06/17] Iteration 2 --- case-study-template.md | 4 ++-- task-1.rb | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 20a4b6c..6454237 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -38,14 +38,14 @@ Львиную долю времени занимает вложенная итерация сессий при переборе пользователей. ### Ваша находка №2 -О вашей находке №2 +Оптимизация `all?` (замена `=~` на `match?`, вынос регулярного выражения вне цикла, переписывание на `each`) не дает существенного прироста производительности и экономии памяти. ### Ваша находка №X О вашей находке №X ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с 20 сек и 36 МБ RSS. +Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.81 сек и 39 МБ RSS. *Какими ещё результами можете поделиться* diff --git a/task-1.rb b/task-1.rb index 0ba06ec..2cd43d5 100644 --- a/task-1.rb +++ b/task-1.rb @@ -138,12 +138,14 @@ def work(file_path) # Хоть раз использовал IE? collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } + regexp = /INTERNET EXPLORER/i + { 'usedIE' => user.sessions.any? { |s| s['browser'].match?(regexp) } } end # Всегда использовал только Chrome? collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } + regexp = /CHROME/i + { 'alwaysUsedChrome' => user.sessions.all? { |s| s['browser'].match?(regexp) } } end # Даты сессий через запятую в обратном порядке в формате iso8601 From dbe4d6b3ecac052837b4c592bc960c9a580eb76a Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Wed, 6 Mar 2019 20:58:15 +0500 Subject: [PATCH 07/17] Improve date parsing and sorting --- case-study-template.md | 2 +- task-1.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 6454237..f01e824 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -45,7 +45,7 @@ ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.81 сек и 39 МБ RSS. +Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.75 сек и 41 МБ RSS. *Какими ещё результами можете поделиться* diff --git a/task-1.rb b/task-1.rb index 2cd43d5..131b65e 100644 --- a/task-1.rb +++ b/task-1.rb @@ -150,7 +150,8 @@ def work(file_path) # Даты сессий через запятую в обратном порядке в формате 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 } } + result = user.sessions.map { |s| Date.iso8601(s['date']) }.sort { |d1, d2| d2 <=> d1 } + { 'dates' => result } end File.write('result.json', "#{report.to_json}\n") From 6ba1b9391c773045356253a79e4b362f227b797c Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Wed, 6 Mar 2019 22:05:09 +0500 Subject: [PATCH 08/17] Improve speed by collection all user specifc stat in one cycle --- case-study-template.md | 11 +++-- task-1.rb | 91 +++++++++++++++++++++++++++++------------- 2 files changed, 70 insertions(+), 32 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index f01e824..22aa42a 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,7 +12,7 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: - измерение общего времени выполнения с помощью `Benchmark.realtime` - измерение общей потребляемой памяти с помощью `MemoryMeasure` - профилирование программы с помощью `ruby-prof` и `qcachegrind` @@ -40,12 +40,15 @@ ### Ваша находка №2 Оптимизация `all?` (замена `=~` на `match?`, вынос регулярного выражения вне цикла, переписывание на `each`) не дает существенного прироста производительности и экономии памяти. -### Ваша находка №X -О вашей находке №X +### Ваша находка №3 +Удалось добиться небольшого ускорения, заменив парсинг даты на `Date.iso8601` и реализовав сортировку без `reverse` + +### Ваша находка №4 +Удалось добиться небольшого ускорения, собрав всю статистику пользователя за одну итерацию (вместо нескольких для каждого вида статистики). ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.75 сек и 41 МБ RSS. +Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.7 сек и 43 МБ RSS. *Какими ещё результами можете поделиться* diff --git a/task-1.rb b/task-1.rb index 131b65e..e963aae 100644 --- a/task-1.rb +++ b/task-1.rb @@ -117,41 +117,76 @@ def work(file_path) 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| + # { '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| + # { '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| + # { '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 + # # Браузеры пользователя через запятую + # 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| - regexp = /INTERNET EXPLORER/i - { 'usedIE' => user.sessions.any? { |s| s['browser'].match?(regexp) } } - end + + # collect_stats_from_users(report, users_objects) do |user| + # regexp = /INTERNET EXPLORER/i + # { 'usedIE' => user.sessions.any? { |s| s['browser'].match?(regexp) } } + # end # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - regexp = /CHROME/i - { 'alwaysUsedChrome' => user.sessions.all? { |s| s['browser'].match?(regexp) } } - end + # collect_stats_from_users(report, users_objects) do |user| + # regexp = /CHROME/i + # { 'alwaysUsedChrome' => user.sessions.all? { |s| s['browser'].match?(regexp) } } + # end - # Даты сессий через запятую в обратном порядке в формате iso8601 collect_stats_from_users(report, users_objects) do |user| - result = user.sessions.map { |s| Date.iso8601(s['date']) }.sort { |d1, d2| d2 <=> d1 } - { 'dates' => result } + initial_state = { + 'sessionsCount' => user.sessions.size, # Собираем количество сессий по пользователям + 'totalTime' => 0, # Собираем количество времени по пользователям + 'longestSession' => 0, # Выбираем самую длинную сессию пользователя + 'browsers' => [], # Браузеры пользователя через запятую + 'usedIE' => false, # Хоть раз использовал IE? + 'alwaysUsedChrome' => true, # Всегда использовал только Chrome? + 'dates' => [] # Даты сессий через запятую в обратном порядке в формате iso8601 + } + chrome_regexp = /CHROME/ + ie_regexp = /INTERNET EXPLORER/ + + user.sessions.each.with_object(initial_state) do |session, result| + result['dates'] << Date.iso8601(session['date']) + + browser = session['browser'].upcase + result['alwaysUsedChrome'] &&= browser.match?(chrome_regexp) + result['usedIE'] ||= browser.match?(ie_regexp) + result['browsers'] << browser + + time = session['time'].to_i + result['longestSession'] = [result['longestSession'], time].max + result['totalTime'] += time + end.then do |result| + { + 'sessionsCount' => result['sessionsCount'], + 'totalTime' => "#{result['totalTime']} min.", + 'longestSession' => "#{result['longestSession']} min.", + 'browsers' => result['browsers'].sort!.join(', '), + 'usedIE' => result['usedIE'], + 'alwaysUsedChrome' => result['alwaysUsedChrome'], + 'dates' => result['dates'].sort! { |d1, d2| d2 <=> d1 } + } + # result['dates'].sort! { |d1, d2| d2 <=> d1 } + # result['browsers'] = result['browsers'].sort!.join(', ') + # result['longestSession'] = "#{result['longestSession'].max} min." + # result['totalTime'] = "#{result['longestSession'].sum} min." + end end File.write('result.json', "#{report.to_json}\n") @@ -192,7 +227,7 @@ def test_result 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') + assert_equal expected_result, File.read('result.json') end end @@ -200,7 +235,7 @@ def test_result puts '=' * 20 before_mem = after_mem = profiling_result = nil time = Benchmark.realtime do - before_mem = MemoryMeasure.call + before_mem = MemoryMeasure.call profiling_result = RubyProf.profile { work(ENV['MEASURE']) } after_mem = MemoryMeasure.call end From 372f054e4a481e6d2d38a96a182d03aedef73a75 Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Wed, 6 Mar 2019 22:14:10 +0500 Subject: [PATCH 09/17] Count number of unique browsers using Set --- case-study-template.md | 5 ++++- task-1.rb | 39 +++------------------------------------ 2 files changed, 7 insertions(+), 37 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 22aa42a..441b21b 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -46,9 +46,12 @@ ### Ваша находка №4 Удалось добиться небольшого ускорения, собрав всю статистику пользователя за одну итерацию (вместо нескольких для каждого вида статистики). +### Ваша находка №5 +Удалось добиться прироста производительности ~30% и сокращения объема потребляемой памяти заменой массива с проверкой на уникальность на `Set`. + ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.7 сек и 43 МБ RSS. +Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.46 сек и 40 МБ RSS. *Какими ещё результами можете поделиться* diff --git a/task-1.rb b/task-1.rb index e963aae..8b1816e 100644 --- a/task-1.rb +++ b/task-1.rb @@ -86,13 +86,12 @@ def work(file_path) report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] + uniqueBrowsers = Set.new sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } + uniqueBrowsers << session['browser'] end - report['uniqueBrowsersCount'] = uniqueBrowsers.count + report['uniqueBrowsersCount'] = uniqueBrowsers.size report['totalSessions'] = sessions.count @@ -116,38 +115,6 @@ def work(file_path) 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 - - - # collect_stats_from_users(report, users_objects) do |user| - # regexp = /INTERNET EXPLORER/i - # { 'usedIE' => user.sessions.any? { |s| s['browser'].match?(regexp) } } - # end - - # Всегда использовал только Chrome? - # collect_stats_from_users(report, users_objects) do |user| - # regexp = /CHROME/i - # { 'alwaysUsedChrome' => user.sessions.all? { |s| s['browser'].match?(regexp) } } - # end - collect_stats_from_users(report, users_objects) do |user| initial_state = { 'sessionsCount' => user.sessions.size, # Собираем количество сессий по пользователям From eb14b9a625ce187b6c644a575e41ffc9783d0f83 Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Wed, 6 Mar 2019 22:40:16 +0500 Subject: [PATCH 10/17] Improve performance and RSS usage by removing duplicate `split` --- case-study-template.md | 2 +- task-1.rb | 29 ++++++++--------------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 441b21b..e0ce329 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -51,7 +51,7 @@ ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.46 сек и 40 МБ RSS. +Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.43 сек и 20 МБ RSS. *Какими ещё результами можете поделиться* diff --git a/task-1.rb b/task-1.rb index 8b1816e..f6ec095 100644 --- a/task-1.rb +++ b/task-1.rb @@ -16,25 +16,12 @@ def initialize(attributes:, sessions:) end 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 parse_user(fields) + %w[id first_name last_name age].zip(fields).to_h 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], - } +def parse_session(fields) + %w[user_id session_id browser time date].zip(fields).to_h end def collect_stats_from_users(report, users_objects, &block) @@ -53,12 +40,12 @@ def work(file_path) user_sessions = {} file_lines.each do |line| - cols = line.split(',') - case cols[0] + type, *rest = line.split(',') + case type when 'user' - users = users + [parse_user(line)] + users = users << parse_user(rest) when 'session' - session = parse_session(line) + session = parse_session(rest) user_id = session['user_id'] user_sessions[user_id] ||= [] user_sessions[user_id] << session From a952def33df10139e5ebc6483175e21f3dc67781 Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Wed, 6 Mar 2019 23:00:25 +0500 Subject: [PATCH 11/17] Replace array concatenation with addition to array --- case-study-template.md | 3 +++ task-1.rb | 11 +++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index e0ce329..0806608 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -49,6 +49,9 @@ ### Ваша находка №5 Удалось добиться прироста производительности ~30% и сокращения объема потребляемой памяти заменой массива с проверкой на уникальность на `Set`. +### Ваша находка №6 +В массив лучше записывать так `arr << new_el`. `arr + [new_el]` намного менее эффективен, т.к. создает дополнительный массив, и соединение двух массивов намного сложнее добавления 1 элемента в существующий массив. + ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.43 сек и 20 МБ RSS. diff --git a/task-1.rb b/task-1.rb index f6ec095..b837fa0 100644 --- a/task-1.rb +++ b/task-1.rb @@ -43,7 +43,7 @@ def work(file_path) type, *rest = line.split(',') case type when 'user' - users = users << parse_user(rest) + users << parse_user(rest) when 'session' session = parse_session(rest) user_id = session['user_id'] @@ -93,11 +93,10 @@ def work(file_path) # Статистика по пользователям users_objects = [] - users.each do |user| - attributes = user - currnet_user_sessions = Array(user_sessions[user['id']]) - user_object = User.new(attributes: attributes, sessions: currnet_user_sessions) - users_objects = users_objects + [user_object] + users.each do |user_attrs| + currnet_user_sessions = Array(user_sessions[user_attrs['id']]) + user_object = User.new(attributes: user_attrs, sessions: currnet_user_sessions) + users_objects << user_object end report['usersStats'] = {} From 6dc93e329956b45431ac00f7931c259644d0aceb Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Wed, 6 Mar 2019 23:08:23 +0500 Subject: [PATCH 12/17] Remove `zip` --- task-1.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/task-1.rb b/task-1.rb index b837fa0..9817b77 100644 --- a/task-1.rb +++ b/task-1.rb @@ -17,11 +17,22 @@ def initialize(attributes:, sessions:) end def parse_user(fields) - %w[id first_name last_name age].zip(fields).to_h + { + 'id' => fields[0], + 'first_name' => fields[1], + 'last_name' => fields[2], + 'age' => fields[3] + } end def parse_session(fields) - %w[user_id session_id browser time date].zip(fields).to_h + { + 'user_id' => fields[0], + 'session_id' => fields[1], + 'browser' => fields[2], + 'time' => fields[3], + 'date' => fields[4] + } end def collect_stats_from_users(report, users_objects, &block) From 10b7c8e583b9cc60fd1c10b8cd8954fb30ee9fdd Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Wed, 6 Mar 2019 23:20:12 +0500 Subject: [PATCH 13/17] Remove flat sessions array. Count general report metrics inside user metrics calculation --- case-study-template.md | 2 +- task-1.rb | 30 +++++++++--------------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 0806608..9d88928 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -54,7 +54,7 @@ ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.43 сек и 20 МБ RSS. +Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.4 сек и 20 МБ RSS. *Какими ещё результами можете поделиться* diff --git a/task-1.rb b/task-1.rb index 9817b77..7052bb4 100644 --- a/task-1.rb +++ b/task-1.rb @@ -47,7 +47,6 @@ def work(file_path) file_lines = File.read(file_path).split("\n") users = [] - sessions = [] user_sessions = {} file_lines.each do |line| @@ -60,7 +59,6 @@ def work(file_path) user_id = session['user_id'] user_sessions[user_id] ||= [] user_sessions[user_id] << session - sessions << session end end @@ -83,23 +81,11 @@ def work(file_path) report[:totalUsers] = users.count - # Подсчёт количества уникальных браузеров - uniqueBrowsers = Set.new - sessions.each do |session| - uniqueBrowsers << session['browser'] - end - - report['uniqueBrowsersCount'] = uniqueBrowsers.size + report['uniqueBrowsersCount'] = 0 - report['totalSessions'] = sessions.count + report['totalSessions'] = 0 - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') + report['allBrowsers'] = Set.new # Статистика по пользователям users_objects = [] @@ -136,6 +122,9 @@ def work(file_path) time = session['time'].to_i result['longestSession'] = [result['longestSession'], time].max result['totalTime'] += time + + report['allBrowsers'] << browser + report['totalSessions'] += 1 end.then do |result| { 'sessionsCount' => result['sessionsCount'], @@ -146,13 +135,12 @@ def work(file_path) 'alwaysUsedChrome' => result['alwaysUsedChrome'], 'dates' => result['dates'].sort! { |d1, d2| d2 <=> d1 } } - # result['dates'].sort! { |d1, d2| d2 <=> d1 } - # result['browsers'] = result['browsers'].sort!.join(', ') - # result['longestSession'] = "#{result['longestSession'].max} min." - # result['totalTime'] = "#{result['longestSession'].sum} min." end end + report['uniqueBrowsersCount'] = report['allBrowsers'].size # Подсчёт количества уникальных браузеров + report['allBrowsers'] = report['allBrowsers'].sort.join(',') + File.write('result.json', "#{report.to_json}\n") end From c7677f94cecb3fa07d321e48e334d2cd329596f8 Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Wed, 6 Mar 2019 23:45:23 +0500 Subject: [PATCH 14/17] Prittify code. Replace strings with symbols. Remove `each_with_object` --- task-1.rb | 114 +++++++++++++++++++++++++++--------------------------- 1 file changed, 56 insertions(+), 58 deletions(-) diff --git a/task-1.rb b/task-1.rb index 7052bb4..753a59f 100644 --- a/task-1.rb +++ b/task-1.rb @@ -18,35 +18,36 @@ def initialize(attributes:, sessions:) def parse_user(fields) { - 'id' => fields[0], - 'first_name' => fields[1], - 'last_name' => fields[2], - 'age' => fields[3] + id: fields[0], + first_name: fields[1], + last_name: fields[2], + age: fields[3] } end def parse_session(fields) { - 'user_id' => fields[0], - 'session_id' => fields[1], - 'browser' => fields[2], - 'time' => fields[3], - 'date' => fields[4] + user_id: fields[0], + session_id: fields[1], + browser: fields[2], + time: fields[3], + date: fields[4] } end -def collect_stats_from_users(report, users_objects, &block) +def collect_stats_from_users(report, users_objects) 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)) + user_key = "#{user.attributes[:first_name]} #{user.attributes[:last_name]}" + + report[:usersStats][user_key] ||= {} + report[:usersStats][user_key] = report[:usersStats][user_key].merge(yield(user)) end end def work(file_path) file_lines = File.read(file_path).split("\n") - users = [] + users = [] user_sessions = {} file_lines.each do |line| @@ -55,10 +56,10 @@ def work(file_path) when 'user' users << parse_user(rest) when 'session' - session = parse_session(rest) - user_id = session['user_id'] + session = parse_session(rest) + user_id = session[:user_id] user_sessions[user_id] ||= [] - user_sessions[user_id] << session + user_sessions[user_id] << session end end @@ -81,65 +82,62 @@ def work(file_path) report[:totalUsers] = users.count - report['uniqueBrowsersCount'] = 0 + report[:uniqueBrowsersCount] = 0 - report['totalSessions'] = 0 + report[:totalSessions] = 0 - report['allBrowsers'] = Set.new + report[:allBrowsers] = Set.new # Статистика по пользователям users_objects = [] users.each do |user_attrs| - currnet_user_sessions = Array(user_sessions[user_attrs['id']]) - user_object = User.new(attributes: user_attrs, sessions: currnet_user_sessions) - users_objects << user_object + currnet_user_sessions = Array(user_sessions[user_attrs[:id]]) + user_object = User.new(attributes: user_attrs, sessions: currnet_user_sessions) + users_objects << user_object end - report['usersStats'] = {} + report[:usersStats] = {} collect_stats_from_users(report, users_objects) do |user| - initial_state = { - 'sessionsCount' => user.sessions.size, # Собираем количество сессий по пользователям - 'totalTime' => 0, # Собираем количество времени по пользователям - 'longestSession' => 0, # Выбираем самую длинную сессию пользователя - 'browsers' => [], # Браузеры пользователя через запятую - 'usedIE' => false, # Хоть раз использовал IE? - 'alwaysUsedChrome' => true, # Всегда использовал только Chrome? - 'dates' => [] # Даты сессий через запятую в обратном порядке в формате iso8601 + result = { + sessionsCount: user.sessions.size, # Собираем количество сессий по пользователям + totalTime: 0, # Собираем количество времени по пользователям + longestSession: 0, # Выбираем самую длинную сессию пользователя + browsers: [], # Браузеры пользователя через запятую + usedIE: false, # Хоть раз использовал IE? + alwaysUsedChrome: true, # Всегда использовал только Chrome? + dates: [] # Даты сессий через запятую в обратном порядке в формате iso8601 } chrome_regexp = /CHROME/ ie_regexp = /INTERNET EXPLORER/ - user.sessions.each.with_object(initial_state) do |session, result| - result['dates'] << Date.iso8601(session['date']) - - browser = session['browser'].upcase - result['alwaysUsedChrome'] &&= browser.match?(chrome_regexp) - result['usedIE'] ||= browser.match?(ie_regexp) - result['browsers'] << browser - - time = session['time'].to_i - result['longestSession'] = [result['longestSession'], time].max - result['totalTime'] += time - - report['allBrowsers'] << browser - report['totalSessions'] += 1 - end.then do |result| - { - 'sessionsCount' => result['sessionsCount'], - 'totalTime' => "#{result['totalTime']} min.", - 'longestSession' => "#{result['longestSession']} min.", - 'browsers' => result['browsers'].sort!.join(', '), - 'usedIE' => result['usedIE'], - 'alwaysUsedChrome' => result['alwaysUsedChrome'], - 'dates' => result['dates'].sort! { |d1, d2| d2 <=> d1 } - } + user.sessions.each do |session| + result[:dates] << Date.iso8601(session[:date]) + + browser = session[:browser].upcase + result[:alwaysUsedChrome] &&= browser.match?(chrome_regexp) + result[:usedIE] ||= browser.match?(ie_regexp) + result[:browsers] << browser + + time = session[:time].to_i + result[:longestSession] = [result[:longestSession], time].max + result[:totalTime] += time + + report[:allBrowsers] << browser + report[:totalSessions] += 1 end + + result.merge!( + totalTime: "#{result[:totalTime]} min.", + longestSession: "#{result[:longestSession]} min.", + browsers: result[:browsers].sort!.join(', '), + dates: result[:dates].sort! { |d1, d2| d2 <=> d1 } + ) end - report['uniqueBrowsersCount'] = report['allBrowsers'].size # Подсчёт количества уникальных браузеров - report['allBrowsers'] = report['allBrowsers'].sort.join(',') + report[:uniqueBrowsersCount] = report[:allBrowsers].size # Подсчёт количества уникальных браузеров + report[:allBrowsers] = report[:allBrowsers].sort.join(',') File.write('result.json', "#{report.to_json}\n") end From bad5287832fd7a0423b28e8ade9d8bb70c0906ec Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Thu, 7 Mar 2019 00:00:57 +0500 Subject: [PATCH 15/17] Read file line by line --- case-study-template.md | 3 +++ task-1.rb | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 9d88928..1fe04d8 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -52,6 +52,9 @@ ### Ваша находка №6 В массив лучше записывать так `arr << new_el`. `arr + [new_el]` намного менее эффективен, т.к. создает дополнительный массив, и соединение двух массивов намного сложнее добавления 1 элемента в существующий массив. +### Ваша находка №7 +Чтение файла построчно позволяет сэкономить немного памяти и ускорить работу. + ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.4 сек и 20 МБ RSS. diff --git a/task-1.rb b/task-1.rb index 753a59f..dd18839 100644 --- a/task-1.rb +++ b/task-1.rb @@ -45,12 +45,10 @@ def collect_stats_from_users(report, users_objects) end def work(file_path) - file_lines = File.read(file_path).split("\n") - users = [] user_sessions = {} - file_lines.each do |line| + File.foreach(file_path) do |line| type, *rest = line.split(',') case type when 'user' From 6b1e31057bd98b558d101a42b33b5f9963094c19 Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Thu, 7 Mar 2019 00:18:43 +0500 Subject: [PATCH 16/17] Fill case study --- case-study-template.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 1fe04d8..bb8d30c 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -57,9 +57,7 @@ ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.4 сек и 20 МБ RSS. - -*Какими ещё результами можете поделиться* +Удалось улучшить метрику системы с 20 сек и 36 МБ RSS до 0.35 сек и 19 МБ RSS. ## Защита от регресса производительности Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали* From 36ab99a7cd324a42cfee8a88e98d341209c8e4f5 Mon Sep 17 00:00:00 2001 From: Artem Pyankov Date: Thu, 7 Mar 2019 01:03:32 +0500 Subject: [PATCH 17/17] =?UTF-8?q?=C2=AF\=5F(=E3=83=84)=5F/=C2=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- task-1.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/task-1.rb b/task-1.rb index dd18839..e154989 100644 --- a/task-1.rb +++ b/task-1.rb @@ -126,11 +126,11 @@ def work(file_path) report[:totalSessions] += 1 end + result[:dates].sort! { |d1, d2| d2 <=> d1 } result.merge!( totalTime: "#{result[:totalTime]} min.", longestSession: "#{result[:longestSession]} min.", - browsers: result[:browsers].sort!.join(', '), - dates: result[:dates].sort! { |d1, d2| d2 <=> d1 } + browsers: result[:browsers].sort!.join(', ') ) end