diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..9c25013d --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.6 diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..a7264629 --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +ruby '3.3.6' +source "https://rubygems.org" + +gem 'minitest' +gem 'ruby-prof' +gem 'rspec-benchmark' +gem 'stackprof' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..df00f775 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,44 @@ +GEM + remote: https://rubygems.org/ + specs: + benchmark-malloc (0.2.0) + benchmark-perf (0.6.0) + benchmark-trend (0.4.0) + diff-lcs (1.5.1) + minitest (5.25.4) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-benchmark (0.6.0) + benchmark-malloc (~> 0.2) + benchmark-perf (~> 0.6) + benchmark-trend (~> 0.4) + rspec (>= 3.0) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.2) + ruby-prof (1.7.0) + stackprof (0.2.27) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + minitest + rspec-benchmark + ruby-prof + stackprof + +RUBY VERSION + ruby 3.3.6p108 + +BUNDLED WITH + 2.5.16 diff --git a/case-study-template.md b/case-study-template.md deleted file mode 100644 index d41034d9..00000000 --- a/case-study-template.md +++ /dev/null @@ -1,56 +0,0 @@ -# Case-study оптимизации - -## Актуальная проблема -В нашем проекте возникла серьёзная проблема. - -Необходимо было обработать файл с данными, чуть больше ста мегабайт. - -У нас уже была программа на `ruby`, которая умела делать нужную обработку. - -Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. - -Я решил исправить эту проблему, оптимизировав эту программу. - -## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* - -## Гарантия корректности работы оптимизированной программы -Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. - -## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* - -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* - -## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* - -Вот какие проблемы удалось найти и решить - -### Ваша находка №1 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? - -### Ваша находка №2 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? - -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? - -## Результаты -В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. - -*Какими ещё результами можете поделиться* - -## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* - diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..f3633dcc --- /dev/null +++ b/case-study.md @@ -0,0 +1,127 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: + +замер времени между началом и концом работы на семпле в 50000 строк, далее на `data_large.txt` +Замеряла просто вычитая время конца из времени начала. Но опять же, большую часть времени смотрела прогресс в отчёте `ruby-prof` + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построила (мой неидеальный) `feedback_loop`: + +- подобрала sample , на котором программа выполняется за обозримое время (5-50 секунд) +- после того, как программа стала выполняться за 90 секунд, проверяла на большом объёме данных + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовалась `ruby-prof` (как основным инструментом). +Сначала в режиме flat, потом быстро перешла на `RubyProf::WALL_TIME`, в принципе этого хватило. Также пробовала stackprof. + +Вот какие проблемы удалось найти и решить: + +### Ваша находка №1 +- какой отчёт показал главную точку роста +ruby-prof: call tree +``` +7.59% (7.80%) Array#all? [8464 calls, 10000 total] +``` +на sample (10000) +5.62 => 4.8 + +Заменила `uniqueBrowsers` на `Set` (но основная оптимизация кмк за счёт убирания прохода по всем браузерам, можно было и `Array.include`) + +не сильно изменилось, но это была бесячая вещь... + +### Ваша находка №2 + +В принципе сразу тоже: + +``` +86.97% (89.02%) Array#select [1536 calls, 1536 total] +``` + +Нужно перестать проходиться по всем сессиям при обработке каждого пользователя. +Например, сделать ассоциативный массив с ключом user и значением - массив сессий. + +А то и сразу собрать массив объектов `user` и его `sessions`. +Также сразу можем собрать `uniqueBrowsers` и общее кол-во сессий, чтобы потом не считать. + +Также, `report['allBrowsers']` - это и есть `uniqueBrowsers` , только нужно отстортировать и заджойнить. + +Теперь результат - 0.22433002200000374 + +Результат профилировщика изменился )) + +Сразу попробуем на больших данных. + +72.75583059900032 - уже можно жить + +### Находка 3 + +Теперь много времени тратим на `Object#collect_stats_from_users`, в т.ч. `map`. +В принципе можно просто разок пройтись по юзерам и сессиям и собрать `report['usersStats'][user_key]` + +52.531833646999985 сек + +### Находка 4 + +Вижу, что много времени тратится на парсинг даты, закэширую даты в Hash, вдруг повезёт. + +39.95 - неплохо! + +### Находка 5 + +Теперь `parse_sessions` много времени отнимает. +Сделаем без массива: + +```ruby +_, user_id, session_id, browser, time, date = cols.split(',') + { + 'user_id' => user_id, + 'session_id' => session_id, + 'browser' => browser, + 'time' => time, + 'date' => date, + } +``` + +Ещё пыталась парсить с `csv` , но это было супердолго! + +36.794 - ну окей, пусть будет + +### Находка 6 + +И всё-таки не нравится мне эти `file_lines`... +прочитаем построчно `File.readlines` + +36.31289 - в пределах погрешности? Ну пусть будет + +### Время проверить без профилировщика и с GC! + +26.353673987 + +Okay. + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с "невозможно дождаться выполнения на большом объёме данных, 5,6 с на 50000 строк" и уложиться в заданный бюджет. + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы: + +- написан минимальный тест на 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/spec/spec.rb b/spec/spec.rb new file mode 100644 index 00000000..9dc0c2e1 --- /dev/null +++ b/spec/spec.rb @@ -0,0 +1,19 @@ + + +require 'rspec' +require 'rspec-benchmark' +require_relative '../work' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end + +describe 'Performance' do + describe 'work' do + it 'works in 30 seconds' do + expect do + work('data_large.txt') + end.to perform_under(30).sec + end + end +end diff --git a/spec/test.rb b/spec/test.rb new file mode 100644 index 00000000..a5e3322c --- /dev/null +++ b/spec/test.rb @@ -0,0 +1,32 @@ +require_relative '../work' +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 + 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 diff --git a/task-1.rb b/task-1.rb index 778672df..f10c5007 100644 --- a/task-1.rb +++ b/task-1.rb @@ -1,176 +1,32 @@ -# Deoptimized version of homework task +require './work' -require 'json' -require 'pry' -require 'date' -require 'minitest/autorun' +time = Time.now +work('data_large.txt') +end_time = Time.now +p end_time - time -class User - attr_reader :attributes, :sessions +# Sorry, decided to keep! +# +# profile with ruby-prof - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions - end -end +# require 'ruby-prof' -def parse_user(user) - fields = user.split(',') - parsed_result = { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], - } -end +# GC.disable +# RubyProf.measure_mode = RubyProf::WALL_TIME -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 +# result = RubyProf::Profile.profile do +# work('data_large.txt') +# 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 +# printer = RubyProf::CallStackPrinter.new(result) +# printer.print(File.open('ruby_prof_reports/callstack.html', 'w+')) -def work - file_lines = File.read('data.txt').split("\n") +# profile with stackprof - users = [] - sessions = [] +# profile = StackProf.run(mode: :wall, raw: true) do +# work('data_large.txt') +# end - 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 +# File.write('stackprof_reports/stackprof.json', JSON.generate(profile)) - # Отчёт в 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 - 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 diff --git a/work.rb b/work.rb new file mode 100644 index 00000000..229ffb1d --- /dev/null +++ b/work.rb @@ -0,0 +1,109 @@ +require 'json' +require 'date' +require 'minitest/autorun' +require 'stackprof' + +class User + attr_reader :attributes, :sessions + + def initialize(attributes:, sessions:) + @attributes = attributes + @sessions = sessions + end +end + +def parse_user(cols) + _, id, first_name, last_name, age = cols.split(',') + { + 'id' => id, + 'first_name' => first_name, + 'last_name' => last_name, + 'age' => age, + } +end + +def parse_session(cols) + _, user_id, session_id, browser, time, date = cols.split(',') + { + 'user_id' => user_id, + 'session_id' => session_id, + 'browser' => browser, + 'time' => time, + 'date' => date, + } +end + +def work(filename = 'data.txt') + report = {} + + current_user = nil + uniqueBrowsers = Set.new + totalSessions = 0 + user_object = nil + users_objects = [] + + File.readlines(filename, chomp: true).each do |line| + cols = line.split(',') + if cols[0] == 'user' + current_user = parse_user(line) + user_object = User.new(attributes: current_user, sessions: []) + users_objects.push user_object + elsif cols[0] == 'session' + session = parse_session(line) + user_object.sessions.push session + + totalSessions += 1 + uniqueBrowsers.add(session['browser'].upcase) + end + end + + # Отчёт в json + # - Сколько всего юзеров + + # - Сколько всего уникальных браузеров + + # - Сколько всего сессий + + # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + + # + # - По каждому пользователю + # - сколько всего сессий + + # - сколько всего времени + + # - самая длинная сессия + + # - браузеры через запятую + + # - Хоть раз использовал IE? + + # - Всегда использовал только Хром? + + # - даты сессий в порядке убывания через запятую + + + report['totalUsers'] = users_objects.count + + # Подсчёт количества уникальных браузеров + report['uniqueBrowsersCount'] = uniqueBrowsers.count + report['totalSessions'] = totalSessions + report['allBrowsers'] = uniqueBrowsers.sort.join(',') + + report['usersStats'] = {} + + cached_dates = {} + + users_objects.each do |user| + user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + + times = user.sessions.map { |s| s['time'].to_i } + browsers = user.sessions.map { |s| s['browser'].upcase } + + dates = user.sessions.map do |session| + cached_dates[session['date']] ||= Date.parse(session['date']) + cached_dates[session['date']] + end + + report['usersStats'][user_key] = { + 'sessionsCount' => user.sessions.count, + 'totalTime' => "#{times.sum.to_s} min.", + 'longestSession' => "#{times.max.to_s} min.", + 'browsers' => browsers.sort.join(', '), + 'usedIE' => browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, + 'alwaysUsedChrome' => browsers.all? { |b| b =~ /CHROME/ }, + 'dates' => dates.sort.reverse.map { |d| d.iso8601 } + } + end + + File.write('result.json', "#{report.to_json}\n") +end