From 1546f19dcca8028aaf14d39808683953fbf3bb04 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 29 Jan 2025 18:34:24 +0300 Subject: [PATCH 1/3] add profilers --- .gitignore | 6 ++ case-study.md | 64 +++++++++++++++ memory-profiler.rb | 12 +++ memory_usage.rb | 17 ++++ ruby-prof.rb | 24 ++++++ stackprof.rb | 14 ++++ task-2-with-argument.rb | 177 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 314 insertions(+) create mode 100644 .gitignore create mode 100644 case-study.md create mode 100644 memory-profiler.rb create mode 100644 memory_usage.rb create mode 100644 ruby-prof.rb create mode 100644 stackprof.rb create mode 100644 task-2-with-argument.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d5e9cb42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +data_large.txt +data_small.txt +result.json + +/ruby_prof_reports +/stackprof_reports diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..82519cce --- /dev/null +++ b/case-study.md @@ -0,0 +1,64 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Я проверила memory usage на файлах с меньшим числом строк кода, и увидела, что бюджет превышается уже на 8000. + +1000 MEMORY USAGE: 33 MB +2000 MEMORY USAGE: 38 MB +4000 MEMORY USAGE: 53 MB +8000 MEMORY USAGE: 87 MB +16000 MEMORY USAGE: 158 MB +32000 MEMORY USAGE: 241 MB + +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: memory usage в конце выполнения программы с файлом на 16000 строк. + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: *как вы построили feedback_loop* + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика + +### Ваша находка №2 +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика + +### Ваша находка №X +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. + +*Какими ещё результами можете поделиться* + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* diff --git a/memory-profiler.rb b/memory-profiler.rb new file mode 100644 index 00000000..8007ed99 --- /dev/null +++ b/memory-profiler.rb @@ -0,0 +1,12 @@ +# memory_profiler (ruby 2.3.8+) +# allocated - total memory allocated during profiler run +# retained - survived after MemoryProfiler finished + +require 'benchmark' +require 'memory_profiler' +require_relative 'task-2-with-argument.rb' + +report = MemoryProfiler.report do + work('data_small.txt') +end +report.pretty_print(scale_bytes: true) diff --git a/memory_usage.rb b/memory_usage.rb new file mode 100644 index 00000000..d4f55857 --- /dev/null +++ b/memory_usage.rb @@ -0,0 +1,17 @@ +require_relative 'task-2-with-argument.rb' + +COUNTERS = [1, 2, 4, 8, 16, 32] + +COUNTERS.each do |counter| + `head -n #{counter*1000} data_large.txt > data_small.txt` + work('data_small.txt') +end + +# initial + +# 1000 MEMORY USAGE: 33 MB +# 2000 MEMORY USAGE: 38 MB +# 4000 MEMORY USAGE: 53 MB +# 8000 MEMORY USAGE: 87 MB +# 16000 MEMORY USAGE: 158 MB +# 32000 MEMORY USAGE: 241 MB diff --git a/ruby-prof.rb b/ruby-prof.rb new file mode 100644 index 00000000..4098e0e5 --- /dev/null +++ b/ruby-prof.rb @@ -0,0 +1,24 @@ +# RubyProf Flat report +# ruby 12-ruby-prof-flat.rb +# cat ruby_prof_reports/flat.txt +require 'ruby-prof' +require_relative 'task-2-with-argument.rb' + +RubyProf.measure_mode = RubyProf::ALLOCATIONS +`head -n #{8000} data_large.txt > data_small.txt` + +result = RubyProf.profile do + work("data_small.txt") +end + +flat_printer = RubyProf::FlatPrinter.new(result) +flat_printer.print(File.open("ruby_prof_reports/flat.txt", "w+")) + +dot_printer = RubyProf::DotPrinter.new(result) +dot_printer.print(File.open('ruby_prof_reports/graphviz.dot', 'w+')) + +graph_printer = RubyProf::GraphHtmlPrinter.new(result) +graph_printer.print(File.open("ruby_prof_reports/graph.html", "w+")) + +printer_callstack = RubyProf::CallStackPrinter.new(result) +printer_callstack.print(File.open('ruby_prof_reports/callstack.html', 'w+')) diff --git a/stackprof.rb b/stackprof.rb new file mode 100644 index 00000000..5ce6b6d1 --- /dev/null +++ b/stackprof.rb @@ -0,0 +1,14 @@ +# Stackprof report +# ruby 16-stackprof.rb +# cd stackprof_reports +# stackprof stackprof.dump +# stackprof stackprof.dump --method Object#work + +require 'stackprof' +require_relative 'task-2-with-argument.rb' + +`head -n #{8000} data_large.txt > data_small.txt` + +StackProf.run(mode: :object, out: 'stackprof_reports/stackprof.dump', interval: 1000) do + work("data_small.txt") +end diff --git a/task-2-with-argument.rb b/task-2-with-argument.rb new file mode 100644 index 00000000..518f960d --- /dev/null +++ b/task-2-with-argument.rb @@ -0,0 +1,177 @@ +# Deoptimized version of homework task + +require 'json' +require 'pry' +require 'date' +require 'minitest/autorun' + +class User + attr_reader :attributes, :sessions + + def initialize(attributes:, sessions:) + @attributes = attributes + @sessions = sessions + end +end + +def parse_user(user) + fields = user.split(',') + parsed_result = { + 'id' => fields[1], + 'first_name' => fields[2], + 'last_name' => fields[3], + 'age' => fields[4], + } +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], + } +end + +def collect_stats_from_users(report, users_objects, &block) + users_objects.each do |user| + user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" + report['usersStats'][user_key] ||= {} + report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) + end +end + +def work(file_name) + file_lines = File.read(file_name).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' + end + + # Отчёт в json + # - Сколько всего юзеров + + # - Сколько всего уникальных браузеров + + # - Сколько всего сессий + + # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + + # + # - По каждому пользователю + # - сколько всего сессий + + # - сколько всего времени + + # - самая длинная сессия + + # - браузеры через запятую + + # - Хоть раз использовал IE? + + # - Всегда использовал только Хром? + + # - даты сессий в порядке убывания через запятую + + + report = {} + + report[:totalUsers] = users.count + + # Подсчёт количества уникальных браузеров + uniqueBrowsers = [] + sessions.each do |session| + browser = session['browser'] + uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } + end + + report['uniqueBrowsersCount'] = uniqueBrowsers.count + + report['totalSessions'] = sessions.count + + report['allBrowsers'] = + sessions + .map { |s| s['browser'] } + .map { |b| b.upcase } + .sort + .uniq + .join(',') + + # Статистика по пользователям + users_objects = [] + + users.each do |user| + attributes = user + user_sessions = sessions.select { |session| session['user_id'] == user['id'] } + user_object = User.new(attributes: attributes, sessions: user_sessions) + users_objects = users_objects + [user_object] + end + + report['usersStats'] = {} + + # Собираем количество сессий по пользователям + collect_stats_from_users(report, users_objects) do |user| + { 'sessionsCount' => user.sessions.count } + end + + # Собираем количество времени по пользователям + collect_stats_from_users(report, users_objects) do |user| + { 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' } + end + + # Выбираем самую длинную сессию пользователя + collect_stats_from_users(report, users_objects) do |user| + { 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' } + 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 + + File.write('result.json', "#{report.to_json}\n") + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) +end + +class TestMe < Minitest::Test + def setup + File.write('result.json', '') + File.write('data.txt', +'user,0,Leida,Cira,0 +session,0,0,Safari 29,87,2016-10-23 +session,0,1,Firefox 12,118,2017-02-27 +session,0,2,Internet Explorer 28,31,2017-03-28 +session,0,3,Internet Explorer 28,109,2016-09-15 +session,0,4,Safari 39,104,2017-09-27 +session,0,5,Internet Explorer 35,6,2016-09-01 +user,1,Palmer,Katrina,65 +session,1,0,Safari 17,12,2016-10-21 +session,1,1,Firefox 32,3,2016-12-20 +session,1,2,Chrome 6,59,2016-11-11 +session,1,3,Internet Explorer 10,28,2017-04-29 +session,1,4,Chrome 13,116,2016-12-28 +user,2,Gregory,Santos,86 +session,2,0,Chrome 35,6,2018-09-21 +session,2,1,Safari 49,85,2017-05-22 +session,2,2,Firefox 47,17,2018-02-02 +session,2,3,Chrome 20,84,2016-11-25 +') + end + + def test_result + work('data.txt') + expected_result = JSON.parse('{"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"]}}}') + assert_equal expected_result, JSON.parse(File.read('result.json')) + end +end From 3865adfc076649986672f8da9288dde8bd58d9be Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 1 Feb 2025 19:51:39 +0300 Subject: [PATCH 2/3] optimize 1-8 --- case-study.md | 36 ++++++++++++++++++++++++++++++------ memory-profiler.rb | 2 ++ ruby-prof.rb | 2 +- stackprof.rb | 2 +- task-2-with-argument.rb | 24 ++++++++++++------------ 5 files changed, 46 insertions(+), 20 deletions(-) diff --git a/case-study.md b/case-study.md index 82519cce..04699fe9 100644 --- a/case-study.md +++ b/case-study.md @@ -32,15 +32,39 @@ Вот как я построил `feedback_loop`: *как вы построили feedback_loop* ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовалась memory_profiler, а так же falt, graph и callstack от ruby-prof и cli stackprof Вот какие проблемы удалось найти и решить -### Ваша находка №1 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +Первые отчеты показывали разные точки роста. +# stackprof: +118 (31.6%) 118 (31.6%) String#split +# rubyprof : +31.40 117541.000 117541.000 0.000 0.000 16001 String#split +# memory-profiler + 184.23 MB /home/iris/learning/optimization/rails-optimization-task2/task-2-with-argument.rb:55 + `sessions = sessions + [parse_session(line)] if cols[0] == 'session'` + /home/iris/learning/optimization/rails-optimization-task2/task-2-with-argument.rb:140 +`{ 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } }` + +Я решила на первых этапах пользоваться результатами отчета по количеству памяти от memory-profiler, т.к. он показывает не только аллокации, но и потребляему память +Кроме того, т.к. первые находки соотвествуют тем, что были в прошлых заданиях, я не буду подробно их расписывать. Исправляю по очереди, когда меняется главная точка роста. + +### Находки №1-5 +184.23 MB `sessions = sessions + [parse_session(line)] if cols[0] == 'session'` +66.67 MB `user_sessions = sessions.select { |session| session['user_id'] == user['id'] }` +6.48 MB `users = users + [parse_user(line)] if cols[0] == 'user'` +6.25 MB `{ 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } }` +6.16 MB `users_objects = users_objects + [user_object]` + +## Находка 6 +3.86 MB и 62770 `cols = line.split(',')` - в данной версии программы особо не изменить, пропускаю пока что + +## Находки 7-8 +3.32 MB `fields = session.split(',')` +1.75 MB `user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}"` +К моменту исправления восьмой проблемы на 16000 строк в конце программы MEMORY USAGE: 46 MB и 85 MB с отключенным GC +Изменения на этом этапе не значительные, поэтому исходя из находки №6 решаю переписать программу на поточный алгоритм. ### Ваша находка №2 - какой отчёт показал главную точку роста diff --git a/memory-profiler.rb b/memory-profiler.rb index 8007ed99..00a3d5d1 100644 --- a/memory-profiler.rb +++ b/memory-profiler.rb @@ -6,6 +6,8 @@ require 'memory_profiler' require_relative 'task-2-with-argument.rb' +`head -n #{8000} data_large.txt > data_small.txt` + report = MemoryProfiler.report do work('data_small.txt') end diff --git a/ruby-prof.rb b/ruby-prof.rb index 4098e0e5..b68044ff 100644 --- a/ruby-prof.rb +++ b/ruby-prof.rb @@ -5,7 +5,7 @@ require_relative 'task-2-with-argument.rb' RubyProf.measure_mode = RubyProf::ALLOCATIONS -`head -n #{8000} data_large.txt > data_small.txt` +`head -n #{16000} data_large.txt > data_small.txt` result = RubyProf.profile do work("data_small.txt") diff --git a/stackprof.rb b/stackprof.rb index 5ce6b6d1..988e6aca 100644 --- a/stackprof.rb +++ b/stackprof.rb @@ -7,7 +7,7 @@ require 'stackprof' require_relative 'task-2-with-argument.rb' -`head -n #{8000} data_large.txt > data_small.txt` +`head -n #{16000} data_large.txt > data_small.txt` StackProf.run(mode: :object, out: 'stackprof_reports/stackprof.dump', interval: 1000) do work("data_small.txt") diff --git a/task-2-with-argument.rb b/task-2-with-argument.rb index 518f960d..9ff95fb9 100644 --- a/task-2-with-argument.rb +++ b/task-2-with-argument.rb @@ -14,9 +14,8 @@ def initialize(attributes:, sessions:) end end -def parse_user(user) - fields = user.split(',') - parsed_result = { +def parse_user(fields) + { 'id' => fields[1], 'first_name' => fields[2], 'last_name' => fields[3], @@ -24,9 +23,8 @@ def parse_user(user) } end -def parse_session(session) - fields = session.split(',') - parsed_result = { +def parse_session(fields) + { 'user_id' => fields[1], 'session_id' => fields[2], 'browser' => fields[3], @@ -37,7 +35,7 @@ def parse_session(session) def collect_stats_from_users(report, users_objects, &block) 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)) end @@ -51,8 +49,8 @@ def work(file_name) 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 << parse_user(cols) if cols[0] == 'user' + sessions << parse_session(cols) if cols[0] == 'session' end # Отчёт в json @@ -96,11 +94,13 @@ def work(file_name) # Статистика по пользователям users_objects = [] + sessions_hash = sessions.group_by { |session| session['user_id'] } + users.each do |user| attributes = user - user_sessions = sessions.select { |session| session['user_id'] == user['id'] } + user_sessions = sessions_hash[user['id']] user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects = users_objects + [user_object] + users_objects << user_object end report['usersStats'] = {} @@ -137,7 +137,7 @@ def work(file_name) # Даты сессий через запятую в обратном порядке в формате 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") From b47dfe1376648b344edcb79955cfc1ca99ad97f8 Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 8 Feb 2025 15:57:29 +0300 Subject: [PATCH 3/3] rewrite on stream --- benchmark.rb | 7 ++ case-study.md | 16 +-- memory_usage.rb | 17 +++ task-2-with-argument.rb | 225 +++++++++++++--------------------------- 4 files changed, 100 insertions(+), 165 deletions(-) create mode 100644 benchmark.rb diff --git a/benchmark.rb b/benchmark.rb new file mode 100644 index 00000000..77aff6e2 --- /dev/null +++ b/benchmark.rb @@ -0,0 +1,7 @@ +require 'benchmark' +require_relative 'task-2-with-argument.rb' + +time = Benchmark.realtime do + work('data_large.txt') +end +puts "Finish in #{time.round(2)}" diff --git a/case-study.md b/case-study.md index 04699fe9..c3e34091 100644 --- a/case-study.md +++ b/case-study.md @@ -66,21 +66,13 @@ К моменту исправления восьмой проблемы на 16000 строк в конце программы MEMORY USAGE: 46 MB и 85 MB с отключенным GC Изменения на этом этапе не значительные, поэтому исходя из находки №6 решаю переписать программу на поточный алгоритм. -### Ваша находка №2 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +После переписывания программы на поточный алгоритм memory_usage в конце выполнения программы на 1000-32000 строк одинаковый 26 MB. После этого проверяю для полного файла - MEMORY USAGE в конце исполнения такой же - 26 MB. Запускаю benchmark - время выполнения на большом файле - 12,38 секунд! + +На этом этапе добавила треды, чтобы проверить, что memory usage не превышает заданного параметра на всем протяжении работы программы, а не только в конце. Максимальный объем памяти - 42MB ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. +Удалось улучшить метрику системы до 27MB в конце выполнения программы и уложиться в заданный бюджет. *Какими ещё результами можете поделиться* diff --git a/memory_usage.rb b/memory_usage.rb index d4f55857..a5015181 100644 --- a/memory_usage.rb +++ b/memory_usage.rb @@ -1,5 +1,7 @@ require_relative 'task-2-with-argument.rb' +class MemoryUsageError < StandardError; end + COUNTERS = [1, 2, 4, 8, 16, 32] COUNTERS.each do |counter| @@ -7,6 +9,21 @@ work('data_small.txt') end +thread1 = Thread.new do + work('data_large.txt') +end + +Thread.new do + loop do + memory = `ps -o rss= -p #{Process.pid}`.to_i / 1024 + puts "MEMORY USAGE: %d MB" % (memory) + sleep 1 + raise MemoryUsageError if memory >= 70 + end +end + +thread1.join + # initial # 1000 MEMORY USAGE: 33 MB diff --git a/task-2-with-argument.rb b/task-2-with-argument.rb index 9ff95fb9..447613cc 100644 --- a/task-2-with-argument.rb +++ b/task-2-with-argument.rb @@ -1,177 +1,96 @@ -# Deoptimized version of homework task - require 'json' -require 'pry' -require 'date' -require 'minitest/autorun' +require 'oj' class User - attr_reader :attributes, :sessions - - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions + attr_accessor :session_stats, :sessions, :first_name, :last_name + + def initialize(id, first_name, last_name, age) + @id = id + @first_name = first_name + @last_name = last_name + @age = age + @sessions = [] + @session_stats = {} end end -def parse_user(fields) +def parse_session(fields) { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], + 'user_id' => fields[0], + 'session_id' => fields[1], + 'browser' => fields[2], + 'time' => fields[3], + 'date' => fields[4] } end -def parse_session(fields) - { - 'user_id' => fields[1], - 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[5], +def collect_stats_from_user(user) + return {} unless user + + stats = { + 'sessionsCount' => user.sessions.count, + 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.', + 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.', + 'browsers' => user.sessions.map {|s| s['browser']}, + 'dates' => user.sessions.map { |s| s['date'] }.sort.reverse } + + stats['usedIE'] = stats['browsers'].any? { |b| b =~ /INTERNET EXPLORER/ } + stats['alwaysUsedChrome'] = stats['browsers'].all? { |b| b =~ /CHROME/ } + stats['browsers'] = stats['browsers'].sort.join(', ') + stats['dates'].sort!.reverse! + stats end -def collect_stats_from_users(report, users_objects, &block) - users_objects.each do |user| - user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" - report['usersStats'][user_key] ||= {} - report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) - end +def write_user(user, stream_writer) + stream_writer.push_key("#{user.first_name} #{user.last_name}") + stream_writer.push_object + user.session_stats.each { |key, value| stream_writer.push_value(value, key.to_s) } + stream_writer.pop end def work(file_name) - file_lines = File.read(file_name).split("\n") - - users = [] - sessions = [] - - file_lines.each do |line| - cols = line.split(',') - users << parse_user(cols) if cols[0] == 'user' - sessions << parse_session(cols) if cols[0] == 'session' - end - - # Отчёт в json - # - Сколько всего юзеров + - # - Сколько всего уникальных браузеров + - # - Сколько всего сессий + - # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + - # - # - По каждому пользователю - # - сколько всего сессий + - # - сколько всего времени + - # - самая длинная сессия + - # - браузеры через запятую + - # - Хоть раз использовал IE? + - # - Всегда использовал только Хром? + - # - даты сессий в порядке убывания через запятую + - - report = {} - - report[:totalUsers] = users.count - - # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } + total_users = 0 + total_sessions = 0 + unique_browsers = Set.new + user = nil + + result_file = File.open('result.json', 'w') + + stream_writer = Oj::StreamWriter.new(result_file) + stream_writer.push_object + stream_writer.push_key('usersStats') + stream_writer.push_object + + File.foreach(file_name) do |line| + type, *info = line.strip!.split(',') + if type == 'user' + total_users += 1 + user.session_stats = collect_stats_from_user(user) if user + write_user(user, stream_writer) if user + user = User.new(*info) + end + + if type == 'session' + total_sessions += 1 + session = parse_session(info) + user.sessions << session + unique_browsers << session['browser'].upcase! + end end - report['uniqueBrowsersCount'] = uniqueBrowsers.count - - report['totalSessions'] = sessions.count - - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') + user.session_stats = collect_stats_from_user(user) if user + write_user(user, stream_writer) if user - # Статистика по пользователям - users_objects = [] + stream_writer.pop - sessions_hash = sessions.group_by { |session| session['user_id'] } + stream_writer.push_value(total_users, 'totalUsers') + stream_writer.push_value(unique_browsers.count, 'uniqueBrowsersCount') + stream_writer.push_value(total_sessions, 'totalSessions') + stream_writer.push_value(unique_browsers.sort.join(','), 'allBrowsers') - users.each do |user| - attributes = user - user_sessions = sessions_hash[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) do |user| - { 'sessionsCount' => user.sessions.count } - end + stream_writer.pop_all + result_file.close - # Собираем количество времени по пользователям - 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 "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end - -class TestMe < Minitest::Test - def setup - File.write('result.json', '') - File.write('data.txt', -'user,0,Leida,Cira,0 -session,0,0,Safari 29,87,2016-10-23 -session,0,1,Firefox 12,118,2017-02-27 -session,0,2,Internet Explorer 28,31,2017-03-28 -session,0,3,Internet Explorer 28,109,2016-09-15 -session,0,4,Safari 39,104,2017-09-27 -session,0,5,Internet Explorer 35,6,2016-09-01 -user,1,Palmer,Katrina,65 -session,1,0,Safari 17,12,2016-10-21 -session,1,1,Firefox 32,3,2016-12-20 -session,1,2,Chrome 6,59,2016-11-11 -session,1,3,Internet Explorer 10,28,2017-04-29 -session,1,4,Chrome 13,116,2016-12-28 -user,2,Gregory,Santos,86 -session,2,0,Chrome 35,6,2018-09-21 -session,2,1,Safari 49,85,2017-05-22 -session,2,2,Firefox 47,17,2018-02-02 -session,2,3,Chrome 20,84,2016-11-25 -') - end - - def test_result - work('data.txt') - expected_result = JSON.parse('{"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"]}}}') - assert_equal expected_result, JSON.parse(File.read('result.json')) - end -end