From 784fa213c734e33de277d184d3f5348d4be5eb5f Mon Sep 17 00:00:00 2001 From: Andrey Paderin Date: Tue, 5 Mar 2019 23:08:30 +0300 Subject: [PATCH 1/8] #1 --- case-study.md | 59 +++++++++++++++ task-1.rb | 197 +++++++++++++++++++++++++++++++------------------- 2 files changed, 183 insertions(+), 73 deletions(-) create mode 100644 case-study.md diff --git a/case-study.md b/case-study.md new file mode 100644 index 0000000..255792d --- /dev/null +++ b/case-study.md @@ -0,0 +1,59 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я сформировал метрику: *Объем памяти выделенной процессу* + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *36 сек.* + +Вот как я построил `feedback_loop`: +Выдернул из файла первые 30K строк и сохранил их в отдельный файл. +Программа стала обрабатывать этот файл за 36 сек. +Объем занимаемой памяти процессом составил ~ 3,7 ГБ + +## Вникаем в детали системы, чтобы найти 20% точек роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался *memory_profiler, ruby-prof* + +Вот какие проблемы удалось найти и решить + +### 1. Добавление элемента в массив +Данная операция занимала ~ 2,5 ГБ +`sessions = sessions + [parse_session(line)]` +Заменил на строку на `sessions << parse_session(line)` +Размер памяти сокрадился до 933МБ + +### 2. Выборка user_sessions c использованием метода select +Занимала порядка 933 МБ +После изменений файл выполняется ~ 8 сек +размер алоцируемой памяти памяти 85 МБ + +### 3. Аналогичная проблема п.1 только с users +После изменения алоцируемая память уменьшиласть до 27 МБ +Время работы программы сократилось до ~ 5.5 сек + + +### Не качественный код + + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* + +*Какими ещё результами можете поделиться* + +## Защита от регресса производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали* diff --git a/task-1.rb b/task-1.rb index 778672d..42b2fd3 100644 --- a/task-1.rb +++ b/task-1.rb @@ -4,55 +4,104 @@ require 'pry' require 'date' require 'minitest/autorun' +require 'benchmark' +require 'memory_profiler' +require 'ruby-prof' + +RubyProf.measure_mode = RubyProf::MEMORY + +def print_memory_usage + format('%d MB', (`ps -o rss= -p #{Process.pid}`.to_i / 1024)) +end + +def test + puts 'START' + time = Benchmark.realtime do + puts "rss before concatenation: #{print_memory_usage}" + report = RubyProf.profile do + # report = MemoryProfiler.report do + work + end + # report.pretty_print(scale_bytes: true) + printer = RubyProf::CallTreePrinter.new(report) + printer.print(path: '.', profile: 'profile') + # printer = RubyProf::GraphHtmlPrinter.new(result) + # printer.print(File.open("ruby_prof_graph_alloc.html", "w+")) + puts "rss after concatenation: #{print_memory_usage}" + end + puts "Finish in #{time.round(2)}" +end class User - attr_reader :attributes, :sessions + attr_reader :attributes, :sessions, :browsers, :time def initialize(attributes:, sessions:) @attributes = attributes @sessions = sessions + @browsers = sessions.map { |s| s['browser'] }.map(&:upcase).sort + @time = sessions.map { |s| s['time'] }.map(&:to_i) + end + + def total_time + "#{time.sum} min." + end + + def longest_session + "#{time.max} min." 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], - 'age' => fields[4], + 'age' => fields[4] } end -def parse_session(session) - fields = session.split(',') - parsed_result = { - 'user_id' => fields[1], +def parse_session(fields) + { 'session_id' => fields[2], 'browser' => fields[3], 'time' => fields[4], - 'date' => fields[5], + 'date' => fields[5] } 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']}" + 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)) + # report['usersStats'][user_key] = report['usersStats'][user_key].merge(yield(user)) + report['usersStats'][user_key] = { + 'sessionsCount' => user.sessions.count, + 'totalTime' => user.total_time, + 'longestSession' => user.longest_session, + 'browsers' => user.browsers.join(', '), + 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, + 'alwaysUsedChrome' => user.browsers.all? { |b| b =~ /CHROME/ }, + 'dates' => user.sessions.map { |s| s['date'] }.map { |d| Date.parse(d) }.sort.reverse.map(&:iso8601) + } end end def work - file_lines = File.read('data.txt').split("\n") + file_lines = File.read('test.txt').split("\n") - users = [] - sessions = [] + 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' + + users[cols[1]] = parse_user(cols) if cols[0] == 'user' + + next unless cols[0] == 'session' + + id = cols[1] + sessions[id] ||= [] + sessions[id] << parse_session(cols) end # Отчёт в json @@ -72,73 +121,75 @@ def work report = {} - report[:totalUsers] = users.count + report[:totalUsers] = users.keys.count + all_browsers = sessions.values.flatten.map { |s| s['browser'] } # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end - - report['uniqueBrowsersCount'] = uniqueBrowsers.count + unique_browsers = all_browsers.uniq + # sessions.values.flatten.each do |session| + # uniqueBrowsers << session['browser'] + # end + # uniqueBrowsers.uniq! - report['totalSessions'] = sessions.count + report['uniqueBrowsersCount'] = unique_browsers.count + report['totalSessions'] = sessions.values.flatten.count report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') + unique_browsers + .map(&:upcase) + .sort + .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.each.with_object([]) do |(user_id, attrs), arr| + arr << User.new(attributes: attrs, sessions: sessions[user_id]) 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.' } - end - - # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end - - # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end - - # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } - end - - # Даты сессий через запятую в обратном порядке в формате iso8601 - collect_stats_from_users(report, users_objects) do |user| - { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } - end + collect_stats_from_users(report, users_objects) + # collect_stats_from_users(report, users_objects) do |user| + # { + # 'sessionsCount' => user.sessions.count, + # 'totalTime' => user.total_time, + # 'longestSession' => user.longest_session, + # 'browsers' => user.browsers.join(', '), + # 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, + # 'alwaysUsedChrome' => user.browsers.all? { |b| b =~ /CHROME/ }, + # 'dates' => user.sessions.map { |s| s['date'] }.map { |d| Date.parse(d) }.sort.reverse.map(&:iso8601) + # } + # end + + # # Собираем количество времени по пользователям + # collect_stats_from_users(report, users_objects) do |user| + # { 'totalTime' => user.total_time } + # end + + # # Выбираем самую длинную сессию пользователя + # collect_stats_from_users(report, users_objects) do |user| + # { 'longestSession' => user.longest_session } + # end + + # # Браузеры пользователя через запятую + # collect_stats_from_users(report, users_objects) do |user| + # { 'browsers' => user.browsers.join(', ') } + # end + + # # Хоть раз использовал IE? + # collect_stats_from_users(report, users_objects) do |user| + # { 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ } } + # end + + # # Всегда использовал только Chrome? + # collect_stats_from_users(report, users_objects) do |user| + # { 'alwaysUsedChrome' => user.browsers.all? { |b| b =~ /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(&:iso8601) } + # end File.write('result.json', "#{report.to_json}\n") end From dcd8bbb75f6a7e90348ecbcfd7514370c4af00ee Mon Sep 17 00:00:00 2001 From: Andrey Paderin Date: Tue, 5 Mar 2019 23:56:39 +0300 Subject: [PATCH 2/8] #2 --- case-study.md | 23 +++++----- task-1.rb | 122 +++++++++++++++----------------------------------- 2 files changed, 48 insertions(+), 97 deletions(-) diff --git a/case-study.md b/case-study.md index 255792d..fbc0efd 100644 --- a/case-study.md +++ b/case-study.md @@ -30,28 +30,29 @@ Вот какие проблемы удалось найти и решить -### 1. Добавление элемента в массив -Данная операция занимала ~ 2,5 ГБ -`sessions = sessions + [parse_session(line)]` -Заменил на строку на `sessions << parse_session(line)` -Размер памяти сокрадился до 933МБ +### 1. Конкатенация sessions +Операция конкатенации сессий занимала ~ 2,5 ГБ +После оптимизации размер аллоцируемой памяти сократился до 933 МБ ### 2. Выборка user_sessions c использованием метода select -Занимала порядка 933 МБ -После изменений файл выполняется ~ 8 сек -размер алоцируемой памяти памяти 85 МБ +Операция занимала порядка 933 МБ +После оптимизации тестовый файл стал выполняться ~ 8 сек +размер аллоцируемой памяти уменьшился до 85 МБ ### 3. Аналогичная проблема п.1 только с users -После изменения алоцируемая память уменьшиласть до 27 МБ +После изменения аллоцируемая память уменьшиласть до 27 МБ Время работы программы сократилось до ~ 5.5 сек +### 4. Медленный сбор статистики пользователя методом `collect_stats_from_users` +Использовал ruby-prof. После оптимизации программа стала выполнять за 0,3 сек. +Проверил на реальных данных, программа выполнилась за 78 сек. -### Не качественный код +### 5. Не качественный код ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* +Удалось улучшить метрику системы с *3.7 Гб до 14 Мб* *Какими ещё результами можете поделиться* diff --git a/task-1.rb b/task-1.rb index 42b2fd3..d06d8d8 100644 --- a/task-1.rb +++ b/task-1.rb @@ -18,13 +18,13 @@ def test puts 'START' time = Benchmark.realtime do puts "rss before concatenation: #{print_memory_usage}" - report = RubyProf.profile do - # report = MemoryProfiler.report do + # report = RubyProf.profile do + report = MemoryProfiler.report do work end - # report.pretty_print(scale_bytes: true) - printer = RubyProf::CallTreePrinter.new(report) - printer.print(path: '.', profile: 'profile') + report.pretty_print(scale_bytes: true) + # printer = RubyProf::CallTreePrinter.new(report) + # printer.print(path: '.', profile: 'profile') # printer = RubyProf::GraphHtmlPrinter.new(result) # printer.print(File.open("ruby_prof_graph_alloc.html", "w+")) puts "rss after concatenation: #{print_memory_usage}" @@ -73,21 +73,21 @@ 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(yield(user)) + report['usersStats'][user_key] = { - 'sessionsCount' => user.sessions.count, - 'totalTime' => user.total_time, - 'longestSession' => user.longest_session, - 'browsers' => user.browsers.join(', '), - 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, - 'alwaysUsedChrome' => user.browsers.all? { |b| b =~ /CHROME/ }, - 'dates' => user.sessions.map { |s| s['date'] }.map { |d| Date.parse(d) }.sort.reverse.map(&:iso8601) + 'sessionsCount' => user.sessions.count, # Собираем количество сессий по пользователям + 'totalTime' => user.total_time, # Собираем количество времени по пользователям + 'longestSession' => user.longest_session, # Выбираем самую длинную сессию пользователя + 'browsers' => user.browsers.join(', '), # Браузеры пользователя через запятую + 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, # Хоть раз использовал IE? + 'alwaysUsedChrome' => user.browsers.all? { |b| b =~ /CHROME/ }, # Всегда использовал только Chrome? + 'dates' => user.sessions.map { |s| s['date'] }.map { |d| Date.parse(d) }.sort.reverse.map(&:iso8601) # Даты сессий через запятую в обратном порядке в формате iso8601 } end end def work - file_lines = File.read('test.txt').split("\n") + file_lines = File.read('test_30k.txt').split("\n") users = {} sessions = {} @@ -124,72 +124,22 @@ def work report[:totalUsers] = users.keys.count all_browsers = sessions.values.flatten.map { |s| s['browser'] } + # Подсчёт количества уникальных браузеров unique_browsers = all_browsers.uniq - # sessions.values.flatten.each do |session| - # uniqueBrowsers << session['browser'] - # end - # uniqueBrowsers.uniq! + report['usersStats'] = {} report['uniqueBrowsersCount'] = unique_browsers.count report['totalSessions'] = sessions.values.flatten.count - - report['allBrowsers'] = - unique_browsers - .map(&:upcase) - .sort - .join(',') + report['allBrowsers'] = unique_browsers.map(&:upcase).sort.join(',') # Статистика по пользователям users_objects = users.each.with_object([]) do |(user_id, attrs), arr| arr << User.new(attributes: attrs, sessions: sessions[user_id]) end - report['usersStats'] = {} - # Собираем количество сессий по пользователям collect_stats_from_users(report, users_objects) - # collect_stats_from_users(report, users_objects) do |user| - # { - # 'sessionsCount' => user.sessions.count, - # 'totalTime' => user.total_time, - # 'longestSession' => user.longest_session, - # 'browsers' => user.browsers.join(', '), - # 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, - # 'alwaysUsedChrome' => user.browsers.all? { |b| b =~ /CHROME/ }, - # 'dates' => user.sessions.map { |s| s['date'] }.map { |d| Date.parse(d) }.sort.reverse.map(&:iso8601) - # } - # end - - # # Собираем количество времени по пользователям - # collect_stats_from_users(report, users_objects) do |user| - # { 'totalTime' => user.total_time } - # end - - # # Выбираем самую длинную сессию пользователя - # collect_stats_from_users(report, users_objects) do |user| - # { 'longestSession' => user.longest_session } - # end - - # # Браузеры пользователя через запятую - # collect_stats_from_users(report, users_objects) do |user| - # { 'browsers' => user.browsers.join(', ') } - # end - - # # Хоть раз использовал IE? - # collect_stats_from_users(report, users_objects) do |user| - # { 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ } } - # end - - # # Всегда использовал только Chrome? - # collect_stats_from_users(report, users_objects) do |user| - # { 'alwaysUsedChrome' => user.browsers.all? { |b| b =~ /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(&:iso8601) } - # end File.write('result.json', "#{report.to_json}\n") end @@ -198,25 +148,25 @@ 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 -') + '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 From b1f6ffb18934d3d90840b4432cea2b7a49fb9d0b Mon Sep 17 00:00:00 2001 From: Andrey Paderin Date: Wed, 6 Mar 2019 17:03:29 +0300 Subject: [PATCH 3/8] step #1 --- case-study.md | 41 ++++++------ reference.json | 1 + task-1.rb | 172 ++++++++++++++++++++----------------------------- task-1_test.rb | 15 +++++ 4 files changed, 106 insertions(+), 123 deletions(-) create mode 100644 reference.json create mode 100644 task-1_test.rb diff --git a/case-study.md b/case-study.md index fbc0efd..28284bc 100644 --- a/case-study.md +++ b/case-study.md @@ -12,47 +12,44 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я сформировал метрику: *Объем памяти выделенной процессу* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *Объем памяти выделенной процессу* ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *36 сек.* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *~ 5 сек.* Вот как я построил `feedback_loop`: -Выдернул из файла первые 30K строк и сохранил их в отдельный файл. -Программа стала обрабатывать этот файл за 36 сек. -Объем занимаемой памяти процессом составил ~ 3,7 ГБ + +Разбил файл с данными на три файла с 10К, 100K, 1M записей. +Вынес тест в отдельный файл +Написал бенчмарк + +Проверял на 10к, программа выполнилась за 3,75 сек, размер 587 МБ ## Вникаем в детали системы, чтобы найти 20% точек роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *memory_profiler, ruby-prof* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался *memory_profiler и ruby-prof* Вот какие проблемы удалось найти и решить ### 1. Конкатенация sessions -Операция конкатенации сессий занимала ~ 2,5 ГБ -После оптимизации размер аллоцируемой памяти сократился до 933 МБ - -### 2. Выборка user_sessions c использованием метода select -Операция занимала порядка 933 МБ -После оптимизации тестовый файл стал выполняться ~ 8 сек -размер аллоцируемой памяти уменьшился до 85 МБ - -### 3. Аналогичная проблема п.1 только с users -После изменения аллоцируемая память уменьшиласть до 27 МБ -Время работы программы сократилось до ~ 5.5 сек +Использовал memory_profiler +Операция конкатенации сессий sessions += [parse_session(line)] if cols[0] == 'session' +аллоцировала память 287 МБ -### 4. Медленный сбор статистики пользователя методом `collect_stats_from_users` -Использовал ruby-prof. После оптимизации программа стала выполнять за 0,3 сек. -Проверил на реальных данных, программа выполнилась за 78 сек. +После оптимизации размер аллоцируемой памяти сократился до 293 кб +Программа стала вполняться за 4.11 сек общий размер памяти сократился до 494 МБ -### 5. Не качественный код +### 2. +О вашей находке №2 +### 3. +О вашей находке №X ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *3.7 Гб до 14 Мб* +Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* *Какими ещё результами можете поделиться* diff --git a/reference.json b/reference.json new file mode 100644 index 0000000..ad48563 --- /dev/null +++ b/reference.json @@ -0,0 +1 @@ +{"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"]}}} diff --git a/task-1.rb b/task-1.rb index d06d8d8..df5d5ed 100644 --- a/task-1.rb +++ b/task-1.rb @@ -3,56 +3,19 @@ require 'json' require 'pry' require 'date' -require 'minitest/autorun' -require 'benchmark' -require 'memory_profiler' -require 'ruby-prof' - -RubyProf.measure_mode = RubyProf::MEMORY - -def print_memory_usage - format('%d MB', (`ps -o rss= -p #{Process.pid}`.to_i / 1024)) -end - -def test - puts 'START' - time = Benchmark.realtime do - puts "rss before concatenation: #{print_memory_usage}" - # report = RubyProf.profile do - report = MemoryProfiler.report do - work - end - report.pretty_print(scale_bytes: true) - # printer = RubyProf::CallTreePrinter.new(report) - # printer.print(path: '.', profile: 'profile') - # printer = RubyProf::GraphHtmlPrinter.new(result) - # printer.print(File.open("ruby_prof_graph_alloc.html", "w+")) - puts "rss after concatenation: #{print_memory_usage}" - end - puts "Finish in #{time.round(2)}" -end class User - attr_reader :attributes, :sessions, :browsers, :time + attr_reader :attributes, :sessions def initialize(attributes:, sessions:) @attributes = attributes @sessions = sessions - @browsers = sessions.map { |s| s['browser'] }.map(&:upcase).sort - @time = sessions.map { |s| s['time'] }.map(&:to_i) - end - - def total_time - "#{time.sum} min." - end - - def longest_session - "#{time.max} min." end end -def parse_user(fields) - { +def parse_user(user) + fields = user.split(',') + parsed_result = { 'id' => fields[1], 'first_name' => fields[2], 'last_name' => fields[3], @@ -60,42 +23,33 @@ def parse_user(fields) } end -def parse_session(fields) +def parse_session(session) { - 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[5] + 'user_id' => session[1], + 'session_id' => session[2], + 'browser' => session[3], + 'time' => session[4], + 'date' => session[5] } end def collect_stats_from_users(report, users_objects) users_objects.each do |user| - user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + user_key = user.attributes['first_name'].to_s + ' ' + user.attributes['last_name'].to_s report['usersStats'][user_key] ||= {} - - report['usersStats'][user_key] = { - 'sessionsCount' => user.sessions.count, # Собираем количество сессий по пользователям - 'totalTime' => user.total_time, # Собираем количество времени по пользователям - 'longestSession' => user.longest_session, # Выбираем самую длинную сессию пользователя - 'browsers' => user.browsers.join(', '), # Браузеры пользователя через запятую - 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, # Хоть раз использовал IE? - 'alwaysUsedChrome' => user.browsers.all? { |b| b =~ /CHROME/ }, # Всегда использовал только Chrome? - 'dates' => user.sessions.map { |s| s['date'] }.map { |d| Date.parse(d) }.sort.reverse.map(&:iso8601) # Даты сессий через запятую в обратном порядке в формате iso8601 - } + report['usersStats'][user_key] = report['usersStats'][user_key].merge(yield(user)) end end -def work - file_lines = File.read('test_30k.txt').split("\n") +def work(file_name) + file_lines = File.read(file_name).split("\n") - users = {} + users = [] sessions = {} file_lines.each do |line| cols = line.split(',') - - users[cols[1]] = parse_user(cols) if cols[0] == 'user' + users += [parse_user(line)] if cols[0] == 'user' next unless cols[0] == 'session' @@ -121,57 +75,73 @@ def work report = {} - report[:totalUsers] = users.keys.count - - all_browsers = sessions.values.flatten.map { |s| s['browser'] } + report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - unique_browsers = all_browsers.uniq + uniqueBrowsers = [] + sessions.values.flatten.each do |session| + browser = session['browser'] + uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } + end + + report['uniqueBrowsersCount'] = uniqueBrowsers.count - report['usersStats'] = {} - report['uniqueBrowsersCount'] = unique_browsers.count report['totalSessions'] = sessions.values.flatten.count - report['allBrowsers'] = unique_browsers.map(&:upcase).sort.join(',') + + report['allBrowsers'] = + sessions.values.flatten + .map { |s| s['browser'] } + .map(&:upcase) + .sort + .uniq + .join(',') # Статистика по пользователям - users_objects = users.each.with_object([]) do |(user_id, attrs), arr| - arr << User.new(attributes: attrs, sessions: sessions[user_id]) + users_objects = [] + + users.each do |user| + attributes = user + user_sessions = sessions.values.flatten.select { |session| session['user_id'] == user['id'] } + user_object = User.new(attributes: attributes, sessions: user_sessions) + users_objects += [user_object] end + report['usersStats'] = {} + # Собираем количество сессий по пользователям - collect_stats_from_users(report, users_objects) + collect_stats_from_users(report, users_objects) do |user| + { 'sessionsCount' => user.sessions.count } + end - File.write('result.json', "#{report.to_json}\n") -end + # Собираем количество времени по пользователям + collect_stats_from_users(report, users_objects) do |user| + { 'totalTime' => user.sessions.map { |s| s['time'] }.map(&:to_i).sum.to_s + ' min.' } + 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 - ') + # Выбираем самую длинную сессию пользователя + collect_stats_from_users(report, users_objects) do |user| + { 'longestSession' => user.sessions.map { |s| s['time'] }.map(&:to_i).max.to_s + ' min.' } end - def test_result - 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') + # Браузеры пользователя через запятую + collect_stats_from_users(report, users_objects) do |user| + { 'browsers' => user.sessions.map { |s| s['browser'] }.map(&: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(&:iso8601) } + end + + File.write('result.json', "#{report.to_json}\n") end diff --git a/task-1_test.rb b/task-1_test.rb new file mode 100644 index 0000000..85d7142 --- /dev/null +++ b/task-1_test.rb @@ -0,0 +1,15 @@ +require 'minitest/autorun' +require './task-1' + +class Task1Test < Minitest::Test + def setup + File.write('result.json', '') + @reference_content = File.read('reference.json') + @test_file_name = 'data.txt' + end + + def test_result + work(@test_file_name) + assert_equal @reference_content, File.read('result.json') + end +end From bdf8fd80bb6011e90f57404c13f7987e5820a77b Mon Sep 17 00:00:00 2001 From: Andrey Paderin Date: Wed, 6 Mar 2019 17:18:27 +0300 Subject: [PATCH 4/8] step #2 --- case-study.md | 7 +++++-- task-1.rb | 26 ++++++++++---------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/case-study.md b/case-study.md index 28284bc..b83681e 100644 --- a/case-study.md +++ b/case-study.md @@ -41,8 +41,11 @@ После оптимизации размер аллоцируемой памяти сократился до 293 кб Программа стала вполняться за 4.11 сек общий размер памяти сократился до 494 МБ -### 2. -О вашей находке №2 +### 2. Выборка из массива сессий пользователя методом select +Использовал memory_profiler +Операция выборки сессий пользователя занимала ~ 266 МБ +После оптимизации размер сократился до 1,5 кб +Общий размер занимаемой памяти сократился до 242 МБ, время работы программы сократилось до 2 сек. ### 3. О вашей находке №X diff --git a/task-1.rb b/task-1.rb index df5d5ed..8e1ce8f 100644 --- a/task-1.rb +++ b/task-1.rb @@ -14,12 +14,11 @@ 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], + 'first_name' => user[2], + 'last_name' => user[3], + 'age' => user[4] } end @@ -44,12 +43,12 @@ def collect_stats_from_users(report, users_objects) def work(file_name) file_lines = File.read(file_name).split("\n") - users = [] + users = {} sessions = {} file_lines.each do |line| cols = line.split(',') - users += [parse_user(line)] if cols[0] == 'user' + users[cols[1]] = parse_user(cols) if cols[0] == 'user' next unless cols[0] == 'session' @@ -75,7 +74,7 @@ def work(file_name) report = {} - report[:totalUsers] = users.count + report[:totalUsers] = users.keys.count # Подсчёт количества уникальных браузеров uniqueBrowsers = [] @@ -97,13 +96,8 @@ def work(file_name) .join(',') # Статистика по пользователям - users_objects = [] - - users.each do |user| - attributes = user - user_sessions = sessions.values.flatten.select { |session| session['user_id'] == user['id'] } - user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects += [user_object] + users_objects = users.each.with_object([]) do |(user_id, attrs), arr| + arr << User.new(attributes: attrs, sessions: sessions[user_id]) end report['usersStats'] = {} From 8a3bee86ee948cf6dd50350935ab0324dbdbfc0c Mon Sep 17 00:00:00 2001 From: Andrey Paderin Date: Wed, 6 Mar 2019 21:59:45 +0300 Subject: [PATCH 5/8] step #2 --- case-study.md | 12 +++++++++-- task-1.rb | 60 ++++++++++++++++++++------------------------------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/case-study.md b/case-study.md index b83681e..8fcc613 100644 --- a/case-study.md +++ b/case-study.md @@ -47,8 +47,16 @@ После оптимизации размер сократился до 1,5 кб Общий размер занимаемой памяти сократился до 242 МБ, время работы программы сократилось до 2 сек. -### 3. -О вашей находке №X +### 3. collect_stats_from_users +Использовал ruby-prof +Медленный сбор статистики пользователя +После оптимизации программа выполняется за 1,45 сек, размер занимаемой памяти уменьшился до 207 МБ + +### 4. Date.parse +Использовал ruby-prof + 'dates' => user.sessions.map { |s| s['date'] }.map { |d| Date.iso8601(d) }.sort.reverse +После оптимизации размер уменьшился до 7,5 МБ +Программа стала выполняться за 1,35 сек, размер 185 МБ ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. diff --git a/task-1.rb b/task-1.rb index 8e1ce8f..299587b 100644 --- a/task-1.rb +++ b/task-1.rb @@ -1,15 +1,25 @@ -# Deoptimized version of homework task +# frozen_string_literal: true require 'json' require 'pry' require 'date' class User - attr_reader :attributes, :sessions + attr_reader :attributes, :sessions, :browsers, :time def initialize(attributes:, sessions:) @attributes = attributes @sessions = sessions + @browsers = sessions.map { |s| s['browser'] }.map(&:upcase).sort + @time = sessions.map { |s| s['time'] }.map(&:to_i) + end + + def total_time + "#{time.sum} min." + end + + def longest_session + "#{time.max} min." end end @@ -36,7 +46,16 @@ def collect_stats_from_users(report, users_objects) users_objects.each do |user| user_key = user.attributes['first_name'].to_s + ' ' + user.attributes['last_name'].to_s report['usersStats'][user_key] ||= {} - report['usersStats'][user_key] = report['usersStats'][user_key].merge(yield(user)) + # report['usersStats'][user_key] = report['usersStats'][user_key].merge(yield(user)) + report['usersStats'][user_key] = { + 'sessionsCount' => user.sessions.count, # Собираем количество сессий по пользователям + 'totalTime' => user.total_time, # Собираем количество времени по пользователям + 'longestSession' => user.longest_session, # Выбираем самую длинную сессию пользователя + 'browsers' => user.browsers.join(', '), # Браузеры пользователя через запятую + 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, # Хоть раз использовал IE? + 'alwaysUsedChrome' => user.browsers.uniq.all? { |b| b =~ /CHROME/ }, # Всегда использовал только Chrome? + 'dates' => user.sessions.map { |s| s['date'] }.sort.reverse.map { |d| Date.iso8601(d) } # Даты сессий через запятую в обратном порядке в формате iso8601 + } end end @@ -102,40 +121,7 @@ def work(file_name) 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(&:to_i).sum.to_s + ' min.' } - end - - # Выбираем самую длинную сессию пользователя - collect_stats_from_users(report, users_objects) do |user| - { 'longestSession' => user.sessions.map { |s| s['time'] }.map(&:to_i).max.to_s + ' min.' } - end - - # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map { |s| s['browser'] }.map(&: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(&:iso8601) } - end + collect_stats_from_users(report, users_objects) File.write('result.json', "#{report.to_json}\n") end From 44adb358dbb4ad588cc8eba48df234f0ebb45bf4 Mon Sep 17 00:00:00 2001 From: Andrey Paderin Date: Wed, 6 Mar 2019 22:56:27 +0300 Subject: [PATCH 6/8] step #4 --- case-study.md | 10 ++++++++-- task-1.rb | 8 ++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/case-study.md b/case-study.md index 8fcc613..878c42f 100644 --- a/case-study.md +++ b/case-study.md @@ -52,12 +52,18 @@ Медленный сбор статистики пользователя После оптимизации программа выполняется за 1,45 сек, размер занимаемой памяти уменьшился до 207 МБ -### 4. Date.parse +### 4. Date.parse в сортировке Использовал ruby-prof - 'dates' => user.sessions.map { |s| s['date'] }.map { |d| Date.iso8601(d) }.sort.reverse +'dates' => user.sessions.map { |s| s['date'] }.map { |d| Date.iso8601(d) }.sort.reverse После оптимизации размер уменьшился до 7,5 МБ Программа стала выполняться за 1,35 сек, размер 185 МБ +### 5. Запись отчета +Использовал ruby-prof +File.write('result.json', "#{report.to_json}\n") +После оптимизации размер сократился с 3 МБ до 2,5 МБ +Время выполения составило 1,29 сек, размер уменьшился 183 МБ + ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* diff --git a/task-1.rb b/task-1.rb index 299587b..3fa89f6 100644 --- a/task-1.rb +++ b/task-1.rb @@ -93,7 +93,7 @@ def work(file_name) report = {} - report[:totalUsers] = users.keys.count + report['totalUsers'] = users.keys.count # Подсчёт количества уникальных браузеров uniqueBrowsers = [] @@ -123,5 +123,9 @@ def work(file_name) collect_stats_from_users(report, users_objects) - File.write('result.json', "#{report.to_json}\n") + # File.write('result.json', "#{report.to_json}\n") + File.open('result.json', 'w') do |file| + file.write(report.to_json) + file.write("\n") + end end From e9094ea9b63efb901e78c12277da1f2a01b7ee12 Mon Sep 17 00:00:00 2001 From: Andrey Paderin Date: Thu, 7 Mar 2019 01:15:33 +0300 Subject: [PATCH 7/8] step #5 --- case-study.md | 2 ++ task-1.rb | 49 +++++++++++++++++++------------------------------ 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/case-study.md b/case-study.md index 878c42f..89a9e19 100644 --- a/case-study.md +++ b/case-study.md @@ -64,6 +64,8 @@ File.write('result.json', "#{report.to_json}\n") После оптимизации размер сократился с 3 МБ до 2,5 МБ Время выполения составило 1,29 сек, размер уменьшился 183 МБ +### 6. Рефакторинг +1,26 180МБ ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* diff --git a/task-1.rb b/task-1.rb index 3fa89f6..500755a 100644 --- a/task-1.rb +++ b/task-1.rb @@ -44,21 +44,24 @@ def parse_session(session) def collect_stats_from_users(report, users_objects) users_objects.each do |user| - user_key = user.attributes['first_name'].to_s + ' ' + user.attributes['last_name'].to_s + 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)) - report['usersStats'][user_key] = { - 'sessionsCount' => user.sessions.count, # Собираем количество сессий по пользователям - 'totalTime' => user.total_time, # Собираем количество времени по пользователям - 'longestSession' => user.longest_session, # Выбираем самую длинную сессию пользователя - 'browsers' => user.browsers.join(', '), # Браузеры пользователя через запятую - 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, # Хоть раз использовал IE? - 'alwaysUsedChrome' => user.browsers.uniq.all? { |b| b =~ /CHROME/ }, # Всегда использовал только Chrome? - 'dates' => user.sessions.map { |s| s['date'] }.sort.reverse.map { |d| Date.iso8601(d) } # Даты сессий через запятую в обратном порядке в формате iso8601 - } + report['usersStats'][user_key] = user_stats(user) end end +def user_stats(user) + { + 'sessionsCount' => user.sessions.count, + 'totalTime' => user.total_time, + 'longestSession' => user.longest_session, + 'browsers' => user.browsers.join(', '), + 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, + 'alwaysUsedChrome' => user.browsers.uniq.all? { |b| b =~ /CHROME/ }, + 'dates' => user.sessions.map { |s| s['date'] }.sort.reverse.map { |d| Date.iso8601(d) } + } +end + def work(file_name) file_lines = File.read(file_name).split("\n") @@ -92,38 +95,24 @@ def work(file_name) # - даты сессий в порядке убывания через запятую + report = {} - report['totalUsers'] = users.keys.count + all_browsers = sessions.values.flatten.map { |s| s['browser'] } # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.values.flatten.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end - - report['uniqueBrowsersCount'] = uniqueBrowsers.count + unique_browsers = all_browsers.uniq + report['uniqueBrowsersCount'] = unique_browsers.count report['totalSessions'] = sessions.values.flatten.count - - report['allBrowsers'] = - sessions.values.flatten - .map { |s| s['browser'] } - .map(&:upcase) - .sort - .uniq - .join(',') + report['allBrowsers'] = unique_browsers.map(&:upcase).sort.join(',') + report['usersStats'] = {} # Статистика по пользователям users_objects = users.each.with_object([]) do |(user_id, attrs), arr| arr << User.new(attributes: attrs, sessions: sessions[user_id]) end - report['usersStats'] = {} - collect_stats_from_users(report, users_objects) - # File.write('result.json', "#{report.to_json}\n") File.open('result.json', 'w') do |file| file.write(report.to_json) file.write("\n") From a9a46220b939d0f061208d256d9821127c99dc17 Mon Sep 17 00:00:00 2001 From: Andrey Paderin Date: Thu, 7 Mar 2019 16:05:32 +0300 Subject: [PATCH 8/8] refact --- case-study.md | 14 +++++++---- task-1.rb | 66 ++++++++++++++++++++++----------------------------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/case-study.md b/case-study.md index 89a9e19..21c05c9 100644 --- a/case-study.md +++ b/case-study.md @@ -62,15 +62,19 @@ Использовал ruby-prof File.write('result.json', "#{report.to_json}\n") После оптимизации размер сократился с 3 МБ до 2,5 МБ -Время выполения составило 1,29 сек, размер уменьшился 183 МБ +Время выполнения составило 1,29 сек, размер уменьшился 183 МБ ### 6. Рефакторинг -1,26 180МБ +После рефакторинга время выполнения составило 1,2 сек, +Размер памяти сократился до 179 МБ + ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* +Удалось улучшить метрику системы с *587 МБ до 179 МБ* +Время выполнения программы с тестовыми данными составило 1,2 сек. +Время работы программы с реальными данными составило 69,6 сек -*Какими ещё результами можете поделиться* +В топе проблем остались операции чтения/записи файла на диск, построчное чтение метрику не улучшило ## Защита от регресса производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написан performance тест diff --git a/task-1.rb b/task-1.rb index 500755a..3e5ca96 100644 --- a/task-1.rb +++ b/task-1.rb @@ -10,58 +10,50 @@ class User def initialize(attributes:, sessions:) @attributes = attributes @sessions = sessions - @browsers = sessions.map { |s| s['browser'] }.map(&:upcase).sort - @time = sessions.map { |s| s['time'] }.map(&:to_i) + @browsers = sessions.map { |s| s[:browser] }.map!(&:upcase).sort! + @time = sessions.map { |s| s[:time] }.map!(&:to_i) end - def total_time - "#{time.sum} min." - end - - def longest_session - "#{time.max} min." + def stats + { + sessionsCount: sessions.count, + totalTime: "#{time.sum} min.", + longestSession: "#{time.max} min.", + browsers: browsers.join(', '), + usedIE: browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, + alwaysUsedChrome: browsers.uniq.all? { |b| b =~ /CHROME/ }, + dates: sessions.map { |s| s[:date] }.map! { |d| Date.iso8601(d) }.sort! { |x, y| y <=> x } + } end end def parse_user(user) { - 'id' => user[1], - 'first_name' => user[2], - 'last_name' => user[3], - 'age' => user[4] + id: user[1], + first_name: user[2], + last_name: user[3], + age: user[4] } end def parse_session(session) { - 'user_id' => session[1], - 'session_id' => session[2], - 'browser' => session[3], - 'time' => session[4], - 'date' => session[5] + user_id: session[1], + session_id: session[2], + browser: session[3], + time: session[4], + date: session[5] } end def collect_stats_from_users(report, users_objects) users_objects.each do |user| - user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + user_key = "#{user.attributes[:first_name]} #{user.attributes[:last_name]}" report['usersStats'][user_key] ||= {} - report['usersStats'][user_key] = user_stats(user) + report['usersStats'][user_key] = user.stats end end -def user_stats(user) - { - 'sessionsCount' => user.sessions.count, - 'totalTime' => user.total_time, - 'longestSession' => user.longest_session, - 'browsers' => user.browsers.join(', '), - 'usedIE' => user.browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, - 'alwaysUsedChrome' => user.browsers.uniq.all? { |b| b =~ /CHROME/ }, - 'dates' => user.sessions.map { |s| s['date'] }.sort.reverse.map { |d| Date.iso8601(d) } - } -end - def work(file_name) file_lines = File.read(file_name).split("\n") @@ -94,16 +86,16 @@ def work(file_name) # - Всегда использовал только Хром? + # - даты сессий в порядке убывания через запятую + - report = {} - report['totalUsers'] = users.keys.count - all_browsers = sessions.values.flatten.map { |s| s['browser'] } + all_sessions = sessions.values.flatten # Подсчёт количества уникальных браузеров - unique_browsers = all_browsers.uniq + unique_browsers = all_sessions.map { |s| s[:browser] }.uniq! + report = {} + report['totalUsers'] = users.keys.count report['uniqueBrowsersCount'] = unique_browsers.count - report['totalSessions'] = sessions.values.flatten.count - report['allBrowsers'] = unique_browsers.map(&:upcase).sort.join(',') + report['totalSessions'] = all_sessions.count + report['allBrowsers'] = unique_browsers.map!(&:upcase).sort!.join(',') report['usersStats'] = {} # Статистика по пользователям