From ef32bd52ad3041010080a78628080caa15fef40f Mon Sep 17 00:00:00 2001 From: Sergey Pustovalov Date: Tue, 5 Mar 2019 08:02:26 +0300 Subject: [PATCH 1/3] Benchmark time --- case-study-template.md | 61 ++++++++++++++++++++++++++++++++-- task-1.rb | 75 +++++++++++++++++++++++++++--------------- 2 files changed, 108 insertions(+), 28 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index e0eef00..1131c19 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,7 +12,8 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: в первую очередь нас интересует время работы программы, пока она отрабатывает за "бесконечное время" оптимизация памяти преждевременна + Хочу не потеряв в удобности поддержки кода оптимизировать код чтобы программа работала хотя бы в пределах минуты ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. @@ -23,7 +24,63 @@ Вот как я построил `feedback_loop`: *как вы построили feedback_loop* ## Вникаем в детали системы, чтобы найти 20% точек роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Первой очевидной точкой раста на первый взгляд кажется чтение файла, но даже в первоначальном состоянии это +занимает около 1,5 секунды что в рамках работы всей программы не так много. + +Для того, чтобы найти "точки роста" для оптимизации я воспользовался +- Посмотрел с помощью benchmark как растет потребление памяти при обработке 100, 1000, 10000, 50000 строк из файла, без построения отчета +получились такие результаты: +100 - 0.0003s +1000 - 0.005s +10000 - 0.5s +50000 - 6.02s программа не отработала +100000 - 21.8945s программа не отработала +Окей, время работы растет очень сильно, но 6 секунд на парсинг строк и в несколько раз больше времени на построение отчета говорят что +основные проблемы не в парсинге строк. Этот момент тоже надо оптимизировать, но позже, пока приоритеты на оптимизацию выглядят так + +1. ??? +2. Парсинг строк +3. Потоковое чтение из файла + +Проверяем каждый этап построения отчета на массиве в 10к строк +Больше всего времени уходит на построение массива из инстанос класса User +парсинг строк - 0.4936 +поиск уникальных браузеров - 0.0604 +Подсчёт количества уникальных браузеров - 0.0604 +Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом - 0.005 +сколько всего сессий + 0.1847 +сколько всего времени + 0.0638 +самая длинная сессия + 0.0067 +браузеры через запятую + 0.0104 +Хоть раз использовал IE? + 0.0075 +Всегда использовал только Хром? + 0.006 +даты сессий в порядке убывания через запятую + 0.0596 + +массив из User - 1.4759 + +Проверяем на 20000 строк +парсинг строк - 0.9174 +поиск уникальных браузеров - 0.1074 +Подсчёт количества уникальных браузеров - 0.0604 +Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом - 0.009 +сколько всего сессий + 0.0047 +сколько всего времени + 0.0131 +самая длинная сессия + 0.0128 +браузеры через запятую + 0.0232 +Хоть раз использовал IE? + 0.018 +Всегда использовал только Хром? + 0.0135 +даты сессий в порядке убывания через запятую + 0.1147 + +массив User = 9.5216 + + +Очевидная проблема в генерации массива инстансов User. +Список кандидатов на оптимизацию выглядит так +1. Построение массива юзеров +2. Парсинг строк +3. Построчное чтение файла +Выносим эту генерацию в отдельную функцию для удобства профилирования и смотрим на потрбление памяти + Вот какие проблемы удалось найти и решить diff --git a/task-1.rb b/task-1.rb index 778672d..7d8554f 100644 --- a/task-1.rb +++ b/task-1.rb @@ -5,6 +5,8 @@ require 'date' require 'minitest/autorun' +require 'benchmark' + class User attr_reader :attributes, :sessions @@ -36,25 +38,32 @@ def parse_session(session) 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)) + stats_time = Benchmark.realtime do + 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 + puts "Stats time #{stats_time.round(4)}" end -def work - file_lines = File.read('data.txt').split("\n") +def work(filename = 'data_large.txt') + file_lines = File.read(filename).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' + lines_handle_time = Benchmark.realtime do + file_lines[0..20000].each do |line| + cols = line.split(',') + users = users + [parse_user(line)] if cols[0] == 'user' + sessions = sessions + [parse_session(line)] if cols[0] == 'session' + end end + puts "Lines handled in #{lines_handle_time.round(4)}" + # Отчёт в json # - Сколько всего юзеров + # - Сколько всего уникальных браузеров + @@ -76,32 +85,42 @@ def work # Подсчёт количества уникальных браузеров uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } + uniq_browsers_count_time = Benchmark.realtime do + sessions.each do |session| + browser = session['browser'] + uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } + end end + puts "uniq_browsers_count_time eq #{uniq_browsers_count_time.round(4)}" + report['uniqueBrowsersCount'] = uniqueBrowsers.count report['totalSessions'] = sessions.count - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') + all_browsers_time = Benchmark.realtime do + report['allBrowsers'] = + sessions + .map { |s| s['browser'] } + .map { |b| b.upcase } + .sort + .uniq + .join(',') + end + puts "all_browsers_time #{all_browsers_time.round(4)}" # Статистика по пользователям 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] + build_users_time = Benchmark.realtime do + 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 end + puts "Users builded in #{build_users_time.round(4)}" report['usersStats'] = {} @@ -143,6 +162,10 @@ def work File.write('result.json', "#{report.to_json}\n") end +puts 'start work' +work +puts 'end work' + class TestMe < Minitest::Test def setup File.write('result.json', '') @@ -169,7 +192,7 @@ 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 From 871122a5ee56d7251511c49a4e4390b87fed1eeb Mon Sep 17 00:00:00 2001 From: Sergey Pustovalov Date: Wed, 6 Mar 2019 00:35:10 +0300 Subject: [PATCH 2/3] First step --- case-study-template.md | 87 +++++++++++++++++++++++++++++++++++++++++- task-1.rb | 40 +++++++++---------- 2 files changed, 104 insertions(+), 23 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 1131c19..87f18e7 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -79,8 +79,93 @@ 1. Построение массива юзеров 2. Парсинг строк 3. Построчное чтение файла -Выносим эту генерацию в отдельную функцию для удобства профилирования и смотрим на потрбление памяти +Очевидная проблема внутри генерации массива это поиск сессий по юзеру, если заранее сделать +группировку по user['id'] то можно избавиться от большого количества поисков +проверяем на 20000 строк все становится намного лучше +массив строится за 0.0181 секунды. + +Можно пробовать 50000 строк +Парсинг строк - 5.9629 сек +Построение массива - 0.2721 + +100000 строк +Парсинг строк - 22.8494 сек +Построение массива - 0.9219 сек + +Убираем лимит строк +3250940 строк все еще не рабоает + +В коде который парсит строки и разбивает их на 2 массива есть очевидная проблема в месте +где складываются массивы `users = users + [parse_user(line)]`, замена на `users.push(parse_user(line))` +явно улучшит ситуацию +пробуем + +100000 строк +Парсинг строк - 0.3815 сек +Построение массива - 1.6567 сек + +200000 строк +Парсинг строк - 1.079 сек +Построение массива - 4.9811 сек + +500000 строк +Парсинг строк - 2.7817 сек +Построение массива - 27.1264 сек +Подсчет количества уникальных браузеров - 2.7305 сек + +1 000 000 строк +Парсинг строк - 6.7363 сек +Построение массива - долго +Подсчет количества уникальных браузеров - 5.1087 сек + +очевидно что надо улучшать построение массива юзеров +тоже меняем на `users_objects.push(user_object)` + +1 000 000 строк +Парсинг строк - 5.4842 сек +Построение массива - 0.6537 +Подсчет количества уникальных браузеров - 5.1087 сек + +2 000 000 строк +Парсинг строк - 14.5464 сек +Построение массива - 1.5422 +Подсчет количества уникальных браузеров - 11.0065 сек +Подсчет количества всех браузеров - 6.2511 + +Смотрим внимательно на подсчет браузеров по сессиям +Для подсчета уникальных браузеров нам не нужен сам массив браузеров, переписываем на array#count +получили уменьшение времени с 11 сек до 1.0845 +Для вывода всех браузеров объединяем 2 подряд map в один и сортировку делаем после uniq +потребляемое время уменьшилось с 6 секунд до 2.9063 +неидеально, но сейчас проблема в другом + +2 000 000 записей +парсинг строк - 14.2793 +поиск уникальных браузеров - 1.0389 +Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом - 2.9063 +сколько всего сессий + 2.9591 +сколько всего времени + 2.3199 +самая длинная сессия + 2.6779 +браузеры через запятую + 7.2522 +Хоть раз использовал IE? + 2.5836 +Всегда использовал только Хром? + 4.4826 +даты сессий в порядке убывания через запятую + 18.4876 + +Очень много времени занимает вывод дат сессий, смотрим подробнее +если сделать допущение что все даты сессий в файле изначально записаны в формате +iso8601 (а если это лог нашей программы то мы можем на это влиять), то можно +сразу сортировать строки. +Сделав так получаем время работы этой статистики - 4.3849 + +Пробуем убрать лимит на количество строк +программа отработала за 1 мин 49 сек 💪, 🤟, 🍾 +Окей, отработала и уже хорошо, но надо найти способы еще ее ускорить +Больше всего времени сейчас занимает разбор считанных строк из файла, пробуем считывать файл построчно +28,4398 лучше не стало +Пробуем убрать дополнительный split внутри parse_session и parse_user +19.6525 очевидно что лучше. +Совсем очевидных проблем кажется что нет, надо запускать профайлеры памяти Вот какие проблемы удалось найти и решить diff --git a/task-1.rb b/task-1.rb index 7d8554f..45e002a 100644 --- a/task-1.rb +++ b/task-1.rb @@ -16,8 +16,8 @@ def initialize(attributes:, sessions:) end end -def parse_user(user) - fields = user.split(',') +def parse_user(fields) + # fields = user.split(',') parsed_result = { 'id' => fields[1], 'first_name' => fields[2], @@ -26,8 +26,8 @@ def parse_user(user) } end -def parse_session(session) - fields = session.split(',') +def parse_session(fields) + # fields = session.split(',') parsed_result = { 'user_id' => fields[1], 'session_id' => fields[2], @@ -50,15 +50,17 @@ def collect_stats_from_users(report, users_objects, &block) def work(filename = 'data_large.txt') file_lines = File.read(filename).split("\n") + puts "Handle file_lines #{file_lines.size} count" users = [] sessions = [] lines_handle_time = Benchmark.realtime do - file_lines[0..20000].each do |line| + # file_lines[0..2000000].each do |line| + 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' + users.push(parse_user(cols)) if cols[0] == 'user' + sessions.push(parse_session(cols)) if cols[0] == 'session' end end @@ -84,40 +86,34 @@ def work(filename = 'data_large.txt') report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] uniq_browsers_count_time = Benchmark.realtime do - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end + report['uniqueBrowsersCount'] = sessions.uniq { |session| session['browser'] }.count end puts "uniq_browsers_count_time eq #{uniq_browsers_count_time.round(4)}" - report['uniqueBrowsersCount'] = uniqueBrowsers.count - report['totalSessions'] = sessions.count all_browsers_time = Benchmark.realtime do report['allBrowsers'] = sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort + .map { |s| s['browser'].upcase } .uniq + .sort .join(',') end puts "all_browsers_time #{all_browsers_time.round(4)}" # Статистика по пользователям users_objects = [] + grouped_sessions = sessions.group_by { |session| session['user_id'] } build_users_time = Benchmark.realtime do users.each do |user| attributes = user - user_sessions = sessions.select { |session| session['user_id'] == user['id'] } + user_sessions = grouped_sessions[user['id']] || [] user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects = users_objects + [user_object] + users_objects.push(user_object) end end puts "Users builded in #{build_users_time.round(4)}" @@ -156,15 +152,15 @@ def work(filename = 'data_large.txt') # Даты сессий через запятую в обратном порядке в формате 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 } } + { 'dates' => user.sessions.map{ |s| s['date'] }.sort.reverse } end File.write('result.json', "#{report.to_json}\n") end -puts 'start work' +puts "start work #{Time.now}" work -puts 'end work' +puts "end work #{Time.now}" class TestMe < Minitest::Test def setup From 57473ab6dd8956ac6f796965727ef545948a04b1 Mon Sep 17 00:00:00 2001 From: Sergey Pustovalov Date: Sun, 10 Mar 2019 00:14:35 +0300 Subject: [PATCH 3/3] Final optimization --- case-study-template.md | 13 +--- task-1.rb | 168 +++++------------------------------------ user.rb | 8 ++ work.rb | 156 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 161 deletions(-) create mode 100644 user.rb create mode 100644 work.rb diff --git a/case-study-template.md b/case-study-template.md index 87f18e7..35626cd 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -167,20 +167,13 @@ iso8601 (а если это лог нашей программы то мы мо 19.6525 очевидно что лучше. Совсем очевидных проблем кажется что нет, надо запускать профайлеры памяти -Вот какие проблемы удалось найти и решить +RubyProf подсказал еще места которые позволили довести время работы программы до 80-90 секунд -### Ваша находка №1 -О вашей находке №1 - -### Ваша находка №2 -О вашей находке №2 - -### Ваша находка №X -О вашей находке №X ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* +Удалось улучшить метрику системы с бесконечного времени, до < 2 минут. Изначальная цель "выйти из 1 минуты" не достигнута, +но отчетом по крайней мере можно пользоваться *Какими ещё результами можете поделиться* diff --git a/task-1.rb b/task-1.rb index 45e002a..4dd872a 100644 --- a/task-1.rb +++ b/task-1.rb @@ -5,166 +5,32 @@ require 'date' require 'minitest/autorun' +require 'ruby-prof' require 'benchmark' -class User - attr_reader :attributes, :sessions +require './user' +require './work' - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions - end -end - -def parse_user(fields) - # fields = user.split(',') - parsed_result = { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], - } -end - -def parse_session(fields) - # fields = session.split(',') - parsed_result = { - '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) - stats_time = Benchmark.realtime do - 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 - puts "Stats time #{stats_time.round(4)}" -end - -def work(filename = 'data_large.txt') - file_lines = File.read(filename).split("\n") - puts "Handle file_lines #{file_lines.size} count" +if ARGV[0] == 'run' + profiling_result = nil - users = [] - sessions = [] - - lines_handle_time = Benchmark.realtime do - # file_lines[0..2000000].each do |line| - file_lines.each do |line| - cols = line.split(',') - users.push(parse_user(cols)) if cols[0] == 'user' - sessions.push(parse_session(cols)) if cols[0] == 'session' - end - end - - puts "Lines handled in #{lines_handle_time.round(4)}" - - # Отчёт в json - # - Сколько всего юзеров + - # - Сколько всего уникальных браузеров + - # - Сколько всего сессий + - # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + - # - # - По каждому пользователю - # - сколько всего сессий + - # - сколько всего времени + - # - самая длинная сессия + - # - браузеры через запятую + - # - Хоть раз использовал IE? + - # - Всегда использовал только Хром? + - # - даты сессий в порядке убывания через запятую + - - report = {} - - report[:totalUsers] = users.count - - # Подсчёт количества уникальных браузеров - uniq_browsers_count_time = Benchmark.realtime do - report['uniqueBrowsersCount'] = sessions.uniq { |session| session['browser'] }.count + before_mem = `ps -o rss= -p #{Process.pid}`.to_i + total_time = Benchmark.realtime do + profiling_result = RubyProf.profile { Work.work('data_large.txt', 'large_result.json') } end + after_mem = `ps -o rss= -p #{Process.pid}`.to_i - puts "uniq_browsers_count_time eq #{uniq_browsers_count_time.round(4)}" - - report['totalSessions'] = sessions.count - - all_browsers_time = Benchmark.realtime do - report['allBrowsers'] = - sessions - .map { |s| s['browser'].upcase } - .uniq - .sort - .join(',') - end - puts "all_browsers_time #{all_browsers_time.round(4)}" + mem_diff = (after_mem - before_mem) / 1024 - # Статистика по пользователям - users_objects = [] - grouped_sessions = sessions.group_by { |session| session['user_id'] } - - build_users_time = Benchmark.realtime do - users.each do |user| - attributes = user - user_sessions = grouped_sessions[user['id']] || [] - user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects.push(user_object) - end - end - puts "Users builded in #{build_users_time.round(4)}" - - 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'] }.sort.reverse } - end - - File.write('result.json', "#{report.to_json}\n") + puts "Total time #{total_time.round(4)}" + puts "Mem diff #{mem_diff}" + printer = RubyProf::CallTreePrinter.new(profiling_result) + printer.print(path: '.', profile: "profile_memory") end -puts "start work #{Time.now}" -work -puts "end work #{Time.now}" - class TestMe < Minitest::Test def setup - File.write('result.json', '') + File.write('test_result.json', '') File.write('data.txt', 'user,0,Leida,Cira,0 session,0,0,Safari 29,87,2016-10-23 @@ -188,8 +54,8 @@ def setup end def test_result - work('data.txt') + Work.work('data.txt', 'test_result.json') 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('test_result.json') end end diff --git a/user.rb b/user.rb new file mode 100644 index 0000000..c0a90d8 --- /dev/null +++ b/user.rb @@ -0,0 +1,8 @@ +class User + attr_reader :attributes, :sessions + + def initialize(attributes:, sessions:) + @attributes = attributes + @sessions = sessions + end +end diff --git a/work.rb b/work.rb new file mode 100644 index 0000000..70126a1 --- /dev/null +++ b/work.rb @@ -0,0 +1,156 @@ +module Work + CHROME_REGEXP = /chrome/i + IE_REGEXP = /internet explorer/i + + module_function + + 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) + # stats_time = Benchmark.realtime do + 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 + # puts "Stats time #{stats_time.round(4)}" + end + + def load_file(filename) + file_lines = File.read(filename).split("\n") + puts "Handle file_lines #{file_lines.size} count" + + users = [] + sessions = [] + + sessions_by_user = {} + uniq_browsers = Set.new + + # lines_handle_time = Benchmark.realtime do + file_lines.each do |line| + cols = line.split(',') + case cols[0] + when 'user' + users.push(parse_user(cols)) + when 'session' + session = parse_session(cols) + sessions_by_user[session['user_id']] ||= [] + sessions_by_user[session['user_id']] << session + uniq_browsers << session['browser'] + sessions << session + end + end + # end + + # puts "Lines handled in #{lines_handle_time.round(4)}" + [users, sessions, sessions_by_user, uniq_browsers] + end + + def build_users(users, sessions_by_user) + users_objects = [] + + # build_users_time = Benchmark.realtime do + users.each do |user| + user_sessions = sessions_by_user[user['id']] || [] + user_object = User.new(attributes: user, sessions: user_sessions) + users_objects.push(user_object) + end + # end + # puts "Users builded in #{build_users_time.round(4)}" + users_objects + end + + def write_report(result_filename, report) + File.write(result_filename, "#{report.to_json}\n") + end + + def work(filename, result_filename) + + users, sessions, sessions_by_user, uniq_browsers = load_file(filename) + # Отчёт в json + # - Сколько всего юзеров + + # - Сколько всего уникальных браузеров + + # - Сколько всего сессий + + # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + + # + # - По каждому пользователю + # - сколько всего сессий + + # - сколько всего времени + + # - самая длинная сессия + + # - браузеры через запятую + + # - Хоть раз использовал IE? + + # - Всегда использовал только Хром? + + # - даты сессий в порядке убывания через запятую + + + report = {} + + report[:totalUsers] = users.count + + puts "total users #{report[:totalUsers]}" + + # Подсчёт количества уникальных браузеров + uniq_browsers_count_time = Benchmark.realtime do + report['uniqueBrowsersCount'] = uniq_browsers.size + end + + puts "uniq_browsers_count_time eq #{uniq_browsers_count_time.round(4)}" + + report['totalSessions'] = sessions.count + + all_browsers_time = Benchmark.realtime do + report['allBrowsers'] = uniq_browsers.sort.map(&:upcase).join(',') + end + puts "all_browsers_time #{all_browsers_time.round(4)}" + + # Статистика по пользователям + users_objects = build_users(users, sessions_by_user) + + report['usersStats'] = {} + collect_stats_from_users(report, users_objects) do |user| + { + sessionsCount: user.sessions.count, + totalTime: 0, + longestSession: 0, + browsers: [], + usedIE: false, + alwaysUsedChrome: true, + dates: [] + }.tap do |result| + + user.sessions.each do |session| + result[:totalTime] += session['time'].to_i + result[:longestSession] = [session['time'].to_i, result[:longestSession]].max + result[:browsers] << session['browser'].upcase + result[:usedIE] ||= session['browser'].match?(IE_REGEXP) + result[:alwaysUsedChrome] &&= session['browser'].match?(CHROME_REGEXP) + result[:dates] << session['date'] + end + + result[:dates].sort!.reverse! + result[:totalTime] = "#{result[:totalTime]} min." + result[:longestSession] = "#{result[:longestSession]} min." + result[:browsers] = result[:browsers].sort.join(', ') + end + end + + write_report(result_filename, report) + end +end