From cf46dcea455a0e057df1a1b5c52e37253a3313a5 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 26 Jan 2025 11:06:31 +0300 Subject: [PATCH 01/10] benchmark --- .gitignore | 2 + assert_performance_spec.rb | 29 ++++++ benchmark.rb | 39 ++++++++ case-study.md | 60 +++++++++++++ result.json | 1 + ruby-prof.rb | 24 +++++ stackprof.rb | 20 +++++ task-1_with_argument.rb | 176 +++++++++++++++++++++++++++++++++++++ 8 files changed, 351 insertions(+) create mode 100644 .gitignore create mode 100644 assert_performance_spec.rb create mode 100644 benchmark.rb create mode 100644 case-study.md create mode 100644 result.json create mode 100644 ruby-prof.rb create mode 100644 stackprof.rb create mode 100644 task-1_with_argument.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2b46797d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +data_large.txt +data_small.txt diff --git a/assert_performance_spec.rb b/assert_performance_spec.rb new file mode 100644 index 00000000..f9e8629f --- /dev/null +++ b/assert_performance_spec.rb @@ -0,0 +1,29 @@ +require 'rspec/core' +require 'rspec-benchmark' + +require_relative 'task-1_with_argument.rb' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end + +describe 'Performance' do + describe 'linear work' do + before do + `head -n #{8000} data_large.txt > data_small.txt` + end + it 'works under 1 ms' do + expect { + work('data_small.txt') + }.to perform_under(1000).ms.warmup(2).times.sample(10).times + end + + let(:measurement_time_seconds) { 1 } + let(:warmup_seconds) { 0.2 } + it 'works faster than 1 ips' do + expect { + work('data_small.txt') + }.to perform_at_least(1).within(measurement_time_seconds).warmup(warmup_seconds).ips + end + end +end diff --git a/benchmark.rb b/benchmark.rb new file mode 100644 index 00000000..95db9590 --- /dev/null +++ b/benchmark.rb @@ -0,0 +1,39 @@ +require 'benchmark' +require 'benchmark/ips' +require_relative 'task-1_with_argument.rb' + +COUNTERS = [1, 2, 4, 8, 16, 32] + +COUNTERS.each do |counter| + time = Benchmark.realtime do + `head -n #{counter*1000} data_large.txt > data_small.txt` + work('data_small.txt') + end + puts "Finish in #{time.round(2)}" +end + +# initial + +# 1000 - Finish in 0.03 +# 2000 - Finish in 0.13 +# 4000 - Finish in 0.31 +# 8000 - Finish in 0.97 +# 16000 - Finish in 3.98 +# 32000 - Finish in 22.26 + + +Benchmark.ips do |x| + x.config( + stats: :bootstrap, + confidence: 95, + ) + + x.report("work") do + `head -n #{16_000} data_large.txt > data_small.txt` + work('data_small.txt') + end +end + +# initial + +# work 0.236 (± 1.4%) i/s diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..ffb277b5 --- /dev/null +++ b/case-study.md @@ -0,0 +1,60 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: Файл размером 3250940 строк должен обрабатываться за 30 секунд. + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: +1. Создала копию рабочего файла, которая принимает аргументом файл меньшего размера. +2. Бенчмаркинг. Создала файл benchmark.rb для проверки времени выполнения программы на меньших данных и проверки ips на 16000 строк. +3. Assert performance. Создала файл assert_performance_spec.rb для тестирования времни выполнения и ips. +4. Профилирование. Создала файлы ruby-prof.rb и stackprof.rb для генерации отчетов. + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? + +### Ваша находка №2 +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? + +### Ваша находка №X +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. + +*Какими ещё результами можете поделиться* + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* + diff --git a/result.json b/result.json new file mode 100644 index 00000000..ad485631 --- /dev/null +++ b/result.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/ruby-prof.rb b/ruby-prof.rb new file mode 100644 index 00000000..e98aece1 --- /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-1_with_argument.rb' + +RubyProf.measure_mode = RubyProf::WALL_TIME +`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+")) + +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+')) + +printer_calltree = RubyProf::CallTreePrinter.new(result) +printer_calltree.print(:path => "ruby_prof_reports", :profile => 'callgrind') diff --git a/stackprof.rb b/stackprof.rb new file mode 100644 index 00000000..70a668ff --- /dev/null +++ b/stackprof.rb @@ -0,0 +1,20 @@ +# Stackprof report +# ruby 16-stackprof.rb +# cd stackprof_reports +# stackprof stackprof.dump +# stackprof stackprof.dump --method Object#work +require 'json' +require 'stackprof' +require_relative 'task-1_with_argument.rb' + +`head -n #{8000} data_large.txt > data_small.txt` + +StackProf.run(mode: :wall, out: 'stackprof_reports/stackprof.dump', interval: 1000) do + work("data_small.txt") +end + +profile = StackProf.run(mode: :wall, raw: true) do + work("data_small.txt") +end + +File.write('stackprof_reports/stackprof.json', JSON.generate(profile)) diff --git a/task-1_with_argument.rb b/task-1_with_argument.rb new file mode 100644 index 00000000..7e967a19 --- /dev/null +++ b/task-1_with_argument.rb @@ -0,0 +1,176 @@ +# 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(data_file) + file_lines = File.read(data_file).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") +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 = '{"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 +end From 9421d00d562c8c23ae73d763997490d43de7a6af Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 26 Jan 2025 11:20:10 +0300 Subject: [PATCH 02/10] optimize Array#select --- .gitignore | 3 +++ case-study.md | 10 +++++----- ruby-prof.rb | 4 ++-- task-1_with_argument.rb | 4 +++- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 2b46797d..267bc88a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ data_large.txt data_small.txt + +/ruby_prof_reports +/stackprof_reports diff --git a/case-study.md b/case-study.md index ffb277b5..ca1ec467 100644 --- a/case-study.md +++ b/case-study.md @@ -27,15 +27,15 @@ 4. Профилирование. Создала файлы ruby-prof.rb и stackprof.rb для генерации отчетов. ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался отчетами falt, graph и callstack от ruby-prof и cli stackprof. Вот какие проблемы удалось найти и решить ### Ваша находка №1 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? +- Все отчеты показали главную точку роста Array#select (59.64% по ruby-prof flat) +- Вместо перебора сессий в select создала массив sessins_hash c ключом user_id +- На 16000 строк ips увеличился с 0.236 до 2.581 +- Проблема перестала быть точкой роста ### Ваша находка №2 - какой отчёт показал главную точку роста diff --git a/ruby-prof.rb b/ruby-prof.rb index e98aece1..2f377b3c 100644 --- a/ruby-prof.rb +++ b/ruby-prof.rb @@ -20,5 +20,5 @@ printer_callstack = RubyProf::CallStackPrinter.new(result) printer_callstack.print(File.open('ruby_prof_reports/callstack.html', 'w+')) -printer_calltree = RubyProf::CallTreePrinter.new(result) -printer_calltree.print(:path => "ruby_prof_reports", :profile => 'callgrind') +# printer_calltree = RubyProf::CallTreePrinter.new(result) +# printer_calltree.print(:path => "ruby_prof_reports", :profile => 'callgrind') diff --git a/task-1_with_argument.rb b/task-1_with_argument.rb index 7e967a19..9b5e3d07 100644 --- a/task-1_with_argument.rb +++ b/task-1_with_argument.rb @@ -55,6 +55,8 @@ def work(data_file) sessions = sessions + [parse_session(line)] if cols[0] == 'session' end + sessions_hash = sessions.group_by { |session| session['user_id'] } + # Отчёт в json # - Сколько всего юзеров + # - Сколько всего уникальных браузеров + @@ -98,7 +100,7 @@ def work(data_file) 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] end From a13351219a94e6dc0dca97841fb82c02bf118434 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 26 Jan 2025 11:29:50 +0300 Subject: [PATCH 03/10] optimize uniqBrowsers --- case-study.md | 8 ++++---- task-1_with_argument.rb | 6 +----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/case-study.md b/case-study.md index ca1ec467..db3fd857 100644 --- a/case-study.md +++ b/case-study.md @@ -38,10 +38,10 @@ - Проблема перестала быть точкой роста ### Ваша находка №2 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? +- Все отчеты показывают точку роста в Array#all? (24,56% по ruby-prof flat) +- Вместо метода all? != использовала uniq +- На 16000 строк ips увеличился с 2.581 до 3.203 +- Проблема перестала быть точкой роста ### Ваша находка №X - какой отчёт показал главную точку роста diff --git a/task-1_with_argument.rb b/task-1_with_argument.rb index 9b5e3d07..ac50ba3f 100644 --- a/task-1_with_argument.rb +++ b/task-1_with_argument.rb @@ -77,11 +77,7 @@ def work(data_file) report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end + uniqueBrowsers = sessions.map { |session| session['browser'] }.uniq report['uniqueBrowsersCount'] = uniqueBrowsers.count From f75857236252941a453600cc73ba83a0ee8fe1c1 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 26 Jan 2025 11:34:15 +0300 Subject: [PATCH 04/10] optimize Date#parse --- case-study.md | 6 ++++++ task-1_with_argument.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/case-study.md b/case-study.md index db3fd857..55c84f6e 100644 --- a/case-study.md +++ b/case-study.md @@ -43,6 +43,12 @@ - На 16000 строк ips увеличился с 2.581 до 3.203 - Проблема перестала быть точкой роста +### Ваша находка №3 +- callstack отчет показывает точку роста в collect_stats_from_users, а конкретно в Date#parse +- Я решила сделать сортировку даты без парсинга +- На 16000 строк ips увеличился с 3.203 до 4.265 +- Проблема перестала быть точкой роста + ### Ваша находка №X - какой отчёт показал главную точку роста - как вы решили её оптимизировать diff --git a/task-1_with_argument.rb b/task-1_with_argument.rb index ac50ba3f..8abfe7e8 100644 --- a/task-1_with_argument.rb +++ b/task-1_with_argument.rb @@ -135,7 +135,7 @@ def work(data_file) # Даты сессий через запятую в обратном порядке в формате 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 2fdb8320fc69156e460c88c5f0686393e996807a Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 26 Jan 2025 11:38:04 +0300 Subject: [PATCH 05/10] optimize Array#+ --- case-study.md | 6 ++++++ task-1_with_argument.rb | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/case-study.md b/case-study.md index 55c84f6e..ba77845c 100644 --- a/case-study.md +++ b/case-study.md @@ -49,6 +49,12 @@ - На 16000 строк ips увеличился с 3.203 до 4.265 - Проблема перестала быть точкой роста +### Ваша находка №4 +- callstack отчет показывает точку роста в Array#each, а конкретно в Array#+ +- Вместо создания нового массива на Array#+ я решила добавлять элементы в массив << +- На 16000 строк ips увеличился с 4.265 до 7.329 +- Проблема перестала быть точкой роста + ### Ваша находка №X - какой отчёт показал главную точку роста - как вы решили её оптимизировать diff --git a/task-1_with_argument.rb b/task-1_with_argument.rb index 8abfe7e8..1b54bc18 100644 --- a/task-1_with_argument.rb +++ b/task-1_with_argument.rb @@ -51,8 +51,8 @@ def work(data_file) 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 = users << parse_user(line) if cols[0] == 'user' + sessions = sessions << parse_session(line) if cols[0] == 'session' end sessions_hash = sessions.group_by { |session| session['user_id'] } @@ -98,7 +98,7 @@ def work(data_file) attributes = user user_sessions = sessions_hash[user['id']] user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects = users_objects + [user_object] + users_objects = users_objects << user_object end report['usersStats'] = {} From 04157e2ce284c39e920e331c53136184d9730318 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 26 Jan 2025 11:56:17 +0300 Subject: [PATCH 06/10] optimize String#split --- benchmark.rb | 4 ++-- case-study.md | 9 +++++++++ task-1_with_argument.rb | 10 ++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/benchmark.rb b/benchmark.rb index 95db9590..4d577d7e 100644 --- a/benchmark.rb +++ b/benchmark.rb @@ -2,7 +2,7 @@ require 'benchmark/ips' require_relative 'task-1_with_argument.rb' -COUNTERS = [1, 2, 4, 8, 16, 32] +COUNTERS = [1, 2, 4, 8, 16, 32, 64, 128, 256] COUNTERS.each do |counter| time = Benchmark.realtime do @@ -29,7 +29,7 @@ ) x.report("work") do - `head -n #{16_000} data_large.txt > data_small.txt` + `head -n #{128000} data_large.txt > data_small.txt` work('data_small.txt') end end diff --git a/case-study.md b/case-study.md index ba77845c..cf4cf8c8 100644 --- a/case-study.md +++ b/case-study.md @@ -55,6 +55,15 @@ - На 16000 строк ips увеличился с 4.265 до 7.329 - Проблема перестала быть точкой роста +### Ваша находка №5 +- в этом месте кажется отчеты показывают разное, но я сосредоточилась на отчете ruby-prof flat и ruby-prof graph, которые показывают точну роста в tring#split (13.11%) +- Передаю в методы parse_user и parse_session массивы вместо строк, т.к. split уже сделан в методе, который их вызывает +- На 16000 строк ips увеличился с 7.329 до 7,640 +- Проблема все еще имеет высокий процент, но он снизился до (8.84%) + +На этом моменте я решила использовать для бенчмаркинга и профилирования большие величины, чтобы увеличить точность. +На 128_000 строк ips - 0.867i/s + ### Ваша находка №X - какой отчёт показал главную точку роста - как вы решили её оптимизировать diff --git a/task-1_with_argument.rb b/task-1_with_argument.rb index 1b54bc18..dde41552 100644 --- a/task-1_with_argument.rb +++ b/task-1_with_argument.rb @@ -14,8 +14,7 @@ def initialize(attributes:, sessions:) end end -def parse_user(user) - fields = user.split(',') +def parse_user(fields) parsed_result = { 'id' => fields[1], 'first_name' => fields[2], @@ -24,8 +23,7 @@ def parse_user(user) } end -def parse_session(session) - fields = session.split(',') +def parse_session(fields) parsed_result = { 'user_id' => fields[1], 'session_id' => fields[2], @@ -51,8 +49,8 @@ def work(data_file) 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 = users << parse_user(cols) if cols[0] == 'user' + sessions = sessions << parse_session(cols) if cols[0] == 'session' end sessions_hash = sessions.group_by { |session| session['user_id'] } From 6288d40699e534d7699862df108d4205f29eb07d Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 26 Jan 2025 12:30:45 +0300 Subject: [PATCH 07/10] optimize collect_stats_from_users --- case-study.md | 8 +++++++ ruby-prof.rb | 2 +- task-1_with_argument.rb | 48 +++++++++++++++++++---------------------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/case-study.md b/case-study.md index cf4cf8c8..cf15fab1 100644 --- a/case-study.md +++ b/case-study.md @@ -64,6 +64,14 @@ На этом моменте я решила использовать для бенчмаркинга и профилирования большие величины, чтобы увеличить точность. На 128_000 строк ips - 0.867i/s +### Ваша находка №5 +- Отчеты показывают точку роста в Object#collect_stats_from_users, а конкретно в Array#map ( 16.50%) +- В нескольких вызовах collect_stats_from_users вызывается map на массиве сессий. Объединяют эти вызовы в один. +- 128_000 строк ips увеличился с 0.867 до 1.147 +- collect_stats_from_users уменьшился с 56,19% до 41.28% + +На этом этапе проверяю, сколько выполняется программа на большом файле - 43.88 + ### Ваша находка №X - какой отчёт показал главную точку роста - как вы решили её оптимизировать diff --git a/ruby-prof.rb b/ruby-prof.rb index 2f377b3c..6ec0b53b 100644 --- a/ruby-prof.rb +++ b/ruby-prof.rb @@ -5,7 +5,7 @@ require_relative 'task-1_with_argument.rb' RubyProf.measure_mode = RubyProf::WALL_TIME -`head -n #{8000} data_large.txt > data_small.txt` +`head -n #{128000} data_large.txt > data_small.txt` result = RubyProf.profile do work("data_small.txt") diff --git a/task-1_with_argument.rb b/task-1_with_argument.rb index dde41552..3e5f01cf 100644 --- a/task-1_with_argument.rb +++ b/task-1_with_argument.rb @@ -15,7 +15,7 @@ def initialize(attributes:, sessions:) end def parse_user(fields) - parsed_result = { + { 'id' => fields[1], 'first_name' => fields[2], 'last_name' => fields[3], @@ -24,7 +24,7 @@ def parse_user(fields) end def parse_session(fields) - parsed_result = { + { 'user_id' => fields[1], 'session_id' => fields[2], 'browser' => fields[3], @@ -35,7 +35,7 @@ def parse_session(fields) 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 @@ -101,39 +101,35 @@ def work(data_file) report['usersStats'] = {} - # Собираем количество сессий по пользователям collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } - end - + user_sessions = user.sessions # Собираем количество времени по пользователям - 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 + totalTime = user_sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' # Выбираем самую длинную сессию пользователя - 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 + longestSession = user_sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end + browsers = user_sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end + usedIE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } # Всегда использовал только 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 } + alwaysUsedChrome = browsers.all? { |b| b =~ /CHROME/ } + + # Даты сессий через запятую в обратном порядке в формате iso8601| + dates = user_sessions.map{|s| s['date']}.sort.reverse + + { + 'sessionsCount' => user_sessions.count, + 'totalTime' => totalTime, + 'longestSession' => longestSession, + 'browsers' => browsers.join(', '), + 'usedIE' => usedIE, + 'alwaysUsedChrome' => alwaysUsedChrome, + 'dates' => dates + } end File.write('result.json', "#{report.to_json}\n") From 3e0b6229fab45007e05461debd565d95a81dc7ce Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 26 Jan 2025 13:23:41 +0300 Subject: [PATCH 08/10] fixes --- .gitignore | 1 + assert_performance_spec.rb | 6 ++++++ case-study.md | 4 ++-- result.json | 1 - task-1_with_argument.rb | 9 ++++----- 5 files changed, 13 insertions(+), 8 deletions(-) delete mode 100644 result.json diff --git a/.gitignore b/.gitignore index 267bc88a..d5e9cb42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ data_large.txt data_small.txt +result.json /ruby_prof_reports /stackprof_reports diff --git a/assert_performance_spec.rb b/assert_performance_spec.rb index f9e8629f..446d72bf 100644 --- a/assert_performance_spec.rb +++ b/assert_performance_spec.rb @@ -25,5 +25,11 @@ work('data_small.txt') }.to perform_at_least(1).within(measurement_time_seconds).warmup(warmup_seconds).ips end + + # it 'data_large works under 30sec' do + # expect { + # work('data_large.txt') + # }.to perform_under(30).sec.warmup(2).times.sample(10).times + # end end end diff --git a/case-study.md b/case-study.md index cf15fab1..76b17099 100644 --- a/case-study.md +++ b/case-study.md @@ -64,13 +64,13 @@ На этом моменте я решила использовать для бенчмаркинга и профилирования большие величины, чтобы увеличить точность. На 128_000 строк ips - 0.867i/s -### Ваша находка №5 +### Ваша находка №6 - Отчеты показывают точку роста в Object#collect_stats_from_users, а конкретно в Array#map ( 16.50%) - В нескольких вызовах collect_stats_from_users вызывается map на массиве сессий. Объединяют эти вызовы в один. - 128_000 строк ips увеличился с 0.867 до 1.147 - collect_stats_from_users уменьшился с 56,19% до 41.28% -На этом этапе проверяю, сколько выполняется программа на большом файле - 43.88 +На этом этапе проверяю, сколько выполняется программа на большом файле - 43.88 и 17.25 со отключенным GC ### Ваша находка №X - какой отчёт показал главную точку роста diff --git a/result.json b/result.json deleted file mode 100644 index ad485631..00000000 --- a/result.json +++ /dev/null @@ -1 +0,0 @@ -{"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_with_argument.rb b/task-1_with_argument.rb index 3e5f01cf..b8d5b591 100644 --- a/task-1_with_argument.rb +++ b/task-1_with_argument.rb @@ -2,7 +2,6 @@ require 'json' require 'pry' -require 'date' require 'minitest/autorun' class User @@ -19,7 +18,7 @@ def parse_user(fields) 'id' => fields[1], 'first_name' => fields[2], 'last_name' => fields[3], - 'age' => fields[4], + 'age' => fields[4] } end @@ -29,7 +28,7 @@ def parse_session(fields) 'session_id' => fields[2], 'browser' => fields[3], 'time' => fields[4], - 'date' => fields[5], + 'date' => fields[5] } end @@ -42,6 +41,7 @@ def collect_stats_from_users(report, users_objects, &block) end def work(data_file) + GC.disable file_lines = File.read(data_file).split("\n") users = [] @@ -95,8 +95,7 @@ def work(data_file) users.each do |user| attributes = user user_sessions = sessions_hash[user['id']] - user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects = users_objects << user_object + users_objects << User.new(attributes: attributes, sessions: user_sessions) end report['usersStats'] = {} From 87d47903d6514168190172bc04a97f1b0a4fc778 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 26 Jan 2025 21:02:09 +0300 Subject: [PATCH 09/10] optimize Array#map --- benchmark.rb | 5 +++++ case-study.md | 7 +++++++ task-1_with_argument.rb | 15 +++++++-------- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/benchmark.rb b/benchmark.rb index 4d577d7e..7e307756 100644 --- a/benchmark.rb +++ b/benchmark.rb @@ -37,3 +37,8 @@ # initial # work 0.236 (± 1.4%) i/s + +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 76b17099..60b1cb5e 100644 --- a/case-study.md +++ b/case-study.md @@ -72,6 +72,13 @@ На этом этапе проверяю, сколько выполняется программа на большом файле - 43.88 и 17.25 со отключенным GC +### Ваша находка №7 +- ruby-prof flat показывает точку роста в Array#map +- Несколько раз вызываются лишние map, убрала их. Так же заменила метод поиска уникальных браузеров на Set +- 128_000 строк ips увеличился с 1.147 до 1.265 +- Array#map уменьшился с 17% до 10.93% +Общее время выполения на большом файле - 36.78 секунды + ### Ваша находка №X - какой отчёт показал главную точку роста - как вы решили её оптимизировать diff --git a/task-1_with_argument.rb b/task-1_with_argument.rb index b8d5b591..721a957a 100644 --- a/task-1_with_argument.rb +++ b/task-1_with_argument.rb @@ -41,7 +41,6 @@ def collect_stats_from_users(report, users_objects, &block) end def work(data_file) - GC.disable file_lines = File.read(data_file).split("\n") users = [] @@ -75,7 +74,9 @@ def work(data_file) report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - uniqueBrowsers = sessions.map { |session| session['browser'] }.uniq + + uniqueBrowsers = Set.new + sessions.each { |session| uniqueBrowsers.add(session['browser']) } report['uniqueBrowsersCount'] = uniqueBrowsers.count @@ -83,8 +84,7 @@ def work(data_file) report['allBrowsers'] = sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } + .map { |s| s['browser'].upcase } .sort .uniq .join(',') @@ -103,13 +103,12 @@ def work(data_file) collect_stats_from_users(report, users_objects) do |user| user_sessions = user.sessions # Собираем количество времени по пользователям - totalTime = user_sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' + totalTime = user_sessions.map {|s| s['time'].to_i}.sum.to_s + ' min.' # Выбираем самую длинную сессию пользователя - longestSession = user_sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' - + longestSession = user.sessions.map { |s| s['time'].to_i }.max.to_s + ' min.' # Браузеры пользователя через запятую - browsers = user_sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort + browsers = user_sessions.map {|s| s['browser'].upcase}.sort # Хоть раз использовал IE? usedIE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } From 2a98c68d7da24f0790145848cb43f3f3ff2c9668 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 27 Jan 2025 17:12:04 +0300 Subject: [PATCH 10/10] last optimization --- assert_performance_spec.rb | 40 ++++++++--------- case-study.md | 13 ++---- task-1.rb | 92 +++++++++++++++++--------------------- task-1_with_argument.rb | 18 ++++---- 4 files changed, 73 insertions(+), 90 deletions(-) diff --git a/assert_performance_spec.rb b/assert_performance_spec.rb index 446d72bf..0ac0c0b3 100644 --- a/assert_performance_spec.rb +++ b/assert_performance_spec.rb @@ -8,28 +8,26 @@ end describe 'Performance' do - describe 'linear work' do - before do - `head -n #{8000} data_large.txt > data_small.txt` - end - it 'works under 1 ms' do - expect { - work('data_small.txt') - }.to perform_under(1000).ms.warmup(2).times.sample(10).times - end + before do + `head -n #{8000} data_large.txt > data_small.txt` + end + it 'works under 1 ms' do + expect { + work('data_small.txt') + }.to perform_under(1000).ms.warmup(2).times.sample(10).times + end - let(:measurement_time_seconds) { 1 } - let(:warmup_seconds) { 0.2 } - it 'works faster than 1 ips' do - expect { - work('data_small.txt') - }.to perform_at_least(1).within(measurement_time_seconds).warmup(warmup_seconds).ips - end + let(:measurement_time_seconds) { 1 } + let(:warmup_seconds) { 0.2 } + it 'works faster than 1 ips' do + expect { + work('data_small.txt') + }.to perform_at_least(1).within(measurement_time_seconds).warmup(warmup_seconds).ips + end - # it 'data_large works under 30sec' do - # expect { - # work('data_large.txt') - # }.to perform_under(30).sec.warmup(2).times.sample(10).times - # end + it 'works with data_large under 35sec' do + expect { + work('data_large.txt') + }.to perform_under(35).sec.warmup(2).times.sample(10).times end end diff --git a/case-study.md b/case-study.md index 60b1cb5e..8ee2e5fe 100644 --- a/case-study.md +++ b/case-study.md @@ -18,7 +18,7 @@ Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за (*время, которое у вас получилось*) (Разное время для каждого случая, сперва пять минут, когда точки роста очевидны потом дольше приходилось вникать) Вот как я построил `feedback_loop`: 1. Создала копию рабочего файла, которая принимает аргументом файл меньшего размера. @@ -79,18 +79,13 @@ - Array#map уменьшился с 17% до 10.93% Общее время выполения на большом файле - 36.78 секунды -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? +После этого не вижу в отчетах ничего, что могло бы мне помочь. В слепую изменяю пару методов, т.к. все еще не укладываюсь в метрику. Удалось оптимизировать время выполнения на большом файле до 33.65 секунд. ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. +Удалось улучшить метрику системы с до 33.65 что почти укладывается в заданный бюджет. -*Какими ещё результами можете поделиться* ## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написала performance spec, проверяющий, что на большом файле работа выполняется менее 35 секунд на 10 сэмплах. diff --git a/task-1.rb b/task-1.rb index 778672df..58ab85ae 100644 --- a/task-1.rb +++ b/task-1.rb @@ -2,7 +2,6 @@ require 'json' require 'pry' -require 'date' require 'minitest/autorun' class User @@ -14,47 +13,49 @@ 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], - 'age' => fields[4], + 'age' => fields[4] } 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], 'time' => fields[4], - 'date' => fields[5], + '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']}" + 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_lines = File.read('data.txt').split("\n") - users = [] sessions = [] - file_lines.each do |line| + File.read('data.txt').split("\n").each do |line| cols = line.split(',') - users = users + [parse_user(line)] if cols[0] == 'user' - sessions = sessions + [parse_session(line)] if cols[0] == 'session' + case cols[0] + when 'user' + users << parse_user(cols) + when 'session' + sessions << parse_session(cols) + end end + sessions_hash = sessions.group_by { |session| session['user_id'] } + # Отчёт в json # - Сколько всего юзеров + # - Сколько всего уникальных браузеров + @@ -75,11 +76,9 @@ def work report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end + + uniqueBrowsers = Set.new + sessions.each { |session| uniqueBrowsers.add(session['browser']) } report['uniqueBrowsersCount'] = uniqueBrowsers.count @@ -87,8 +86,7 @@ def work report['allBrowsers'] = sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } + .map { |s| s['browser'].upcase } .sort .uniq .join(',') @@ -96,48 +94,40 @@ def work # Статистика по пользователям 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.map do |user| + User.new(attributes: user, sessions: sessions_hash[user['id']] || []) end report['usersStats'] = {} - # Собираем количество сессий по пользователям collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } - end - + user_sessions = user.sessions # Собираем количество времени по пользователям - 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 + totalTime = user_sessions.map {|s| s['time'].to_i}.sum.to_s + ' min.' # Выбираем самую длинную сессию пользователя - 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 - + longestSession = user.sessions.map { |s| s['time'].to_i }.max.to_s + ' min.' # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end + browsers = user_sessions.map {|s| s['browser'].upcase}.sort # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end + usedIE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } # Всегда использовал только 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 } } + alwaysUsedChrome = browsers.all? { |b| b =~ /CHROME/ } + + # Даты сессий через запятую в обратном порядке в формате iso8601| + dates = user_sessions.map{|s| s['date']}.sort.reverse + + { + 'sessionsCount' => user_sessions.count, + 'totalTime' => totalTime, + 'longestSession' => longestSession, + 'browsers' => browsers.join(', '), + 'usedIE' => usedIE, + 'alwaysUsedChrome' => alwaysUsedChrome, + 'dates' => dates + } end File.write('result.json', "#{report.to_json}\n") diff --git a/task-1_with_argument.rb b/task-1_with_argument.rb index 721a957a..f2137476 100644 --- a/task-1_with_argument.rb +++ b/task-1_with_argument.rb @@ -41,15 +41,17 @@ def collect_stats_from_users(report, users_objects, &block) end def work(data_file) - file_lines = File.read(data_file).split("\n") - users = [] sessions = [] - file_lines.each do |line| + File.read(data_file).split("\n").each do |line| cols = line.split(',') - users = users << parse_user(cols) if cols[0] == 'user' - sessions = sessions << parse_session(cols) if cols[0] == 'session' + case cols[0] + when 'user' + users << parse_user(cols) + when 'session' + sessions << parse_session(cols) + end end sessions_hash = sessions.group_by { |session| session['user_id'] } @@ -92,10 +94,8 @@ def work(data_file) # Статистика по пользователям users_objects = [] - users.each do |user| - attributes = user - user_sessions = sessions_hash[user['id']] - users_objects << User.new(attributes: attributes, sessions: user_sessions) + users_objects = users.map do |user| + User.new(attributes: user, sessions: sessions_hash[user['id']] || []) end report['usersStats'] = {}