diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a5d7025b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +data_large.txt +result.json +benchmarks/CPU/flat.txt +data_large.txt.gz diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/benchmarks/CPU/benchmark-ips.rb b/benchmarks/CPU/benchmark-ips.rb new file mode 100644 index 00000000..e3415627 --- /dev/null +++ b/benchmarks/CPU/benchmark-ips.rb @@ -0,0 +1,16 @@ +require 'benchmark/ips' + +require_relative '../../task-1' + +Benchmark.ips do |x| + # The default is :stats => :sd, which doesn't have a configurable confidence + # confidence is 95% by default, so it can be omitted + x.config( + stats: :bootstrap, + confidence: 95, + ) + + x.report("work") do + work('../data.txt') + end +end diff --git a/benchmarks/CPU/benchmark.rb b/benchmarks/CPU/benchmark.rb new file mode 100644 index 00000000..10dbaba4 --- /dev/null +++ b/benchmarks/CPU/benchmark.rb @@ -0,0 +1,9 @@ +require 'benchmark' + +require_relative '../../task-1' + +time = Benchmark.realtime do |x| + work('../../data_large.txt') +end + +puts "Finish in #{time}" \ No newline at end of file diff --git a/benchmarks/CPU/other-benchmark-ips.rb b/benchmarks/CPU/other-benchmark-ips.rb new file mode 100644 index 00000000..125d2503 --- /dev/null +++ b/benchmarks/CPU/other-benchmark-ips.rb @@ -0,0 +1,27 @@ +require 'benchmark/ips' +require 'oj' +require 'json' +require 'multi_json' + +Benchmark.ips do |x| + + hash = {: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"]}}} + + x.report("oj") do + Oj.dump(hash) + end + + x.report('json') do + hash.to_json + end + + x.report('MultiJson') do + MultiJson.dump(hash) + end + + x.report('MultiJson pretty') do + MultiJson.dump(hash, :pretty => true) + end + + x.compare! +end diff --git a/benchmarks/CPU/ruby-prof-flat.rb b/benchmarks/CPU/ruby-prof-flat.rb new file mode 100644 index 00000000..b7213a52 --- /dev/null +++ b/benchmarks/CPU/ruby-prof-flat.rb @@ -0,0 +1,12 @@ +require 'ruby-prof' +require_relative '../../task-1' + +RubyProf.measure_mode = RubyProf::WALL_TIME + +result = RubyProf.profile do + GC.disable + work('../../data.txt') +end + +printer = RubyProf::FlatPrinter.new(result) +printer.print(File.open("flat.txt", "w+")) \ No newline at end of file diff --git a/case-study-template.md b/case-study-template.md deleted file mode 100644 index b572c8ce..00000000 --- a/case-study-template.md +++ /dev/null @@ -1,46 +0,0 @@ -# Case-study оптимизации - -## Актуальная проблема -В нашем проекте возникла серьёзная проблема. - -Необходимо было обработать файл с данными, чуть больше ста мегабайт. - -У нас уже была программа на `ruby`, которая умела делать нужную обработку. - -Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. - -Я решил исправить эту проблему, оптимизировав эту программу. - -## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* - -## Гарантия корректности работы оптимизированной программы -Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. - -## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* - -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* - -## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* - -Вот какие проблемы удалось найти и решить - -### Ваша находка №1 -О вашей находке №1 - -### Ваша находка №2 -О вашей находке №2 - -### Ваша находка №X -О вашей находке №X - -## Результаты -В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. - -*Какими ещё результами можете поделиться* - -## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..80148b25 --- /dev/null +++ b/case-study.md @@ -0,0 +1,203 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: Количество итераций в секунду. + +При чтении и парсинга файла data.txt Выходные данные из benchmark-ips + +`2.995k (± 1.5%) i/s` + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. +Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. +Для упрощение кода и выделение абстракций я вынес тестирование программы в отдельную директорию `spec` +Тестирование будет заниматься библиотека `rspec` +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: + +* Тестирование +* Создание профайла. Поиск точек старта +* Оптимизация кода + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался библиотеками + +- rbspy +- benchmark +- ruby-prof + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +Вынес тест отдельно из программы. Чтобы не мазолил глаза. Не место тестам в теле основной программы. +Добавил возможность менять подключаемый файл с данными и отключение GC. Через аттрибуты метода work. +Для поиска точки роста использовал профилировщики: +* rbspy - не дал результатов. Основная нагрузка была в самом block in work. Метод work грамозкий и является антипатерном GodObject. +Для получения более подробной информации метод надо разбить на простые методы. +* ruby-prof - Указал на высокую нагрузку CPU при чтении и записи данных. А так же при парсинге даты. +* benchmarks/ips (file: data.txt) +`2.995k (± 1.5%) i/s` + +Бросается на глаза чтение всего файла, дробление по строчно и запись строк в массив. +Так как при большом объеме данных забивается память. Решение использовать читать файл с данными по строчно. + +* Использовал метод File#foreach. Чтобы не записывать все данные в память. А проводить все операции по строчно. +Обернул реализацию парсинга данных в блок #foreach + +И всё пошло по бороде. Если начинать с точки старта, требуется для начало провести не хилый рефакт + +Довел код до зеленных тестов. Пока получилось правда с запашком. + +Треды не включал. Всё происходит синхронно. Тесты запускаю в IDE Rubymine, что возможно могут дать не точные результаты. + А они вообще могут быть точными? + +#### Результаты benchmarks/ips на маленьком файле + +`2.111k (± 3.4%) i/s - 10.441k in 5.035906s` + +#### Результат ruby-prof на большом файле +``` +Total: 52.027112 + +14.89 39.910 7.745 0.000 32.166 1 #foreach +10.85 8.168 5.642 0.000 2.525 2750940 User#update +7.90 4.108 4.108 0.000 0.000 3250940 String#split +7.76 8.193 4.037 0.000 4.157 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json +``` + +### Ваша находка №2 +Вынес код чтения файла и формирование отчета для пользователя в отдельный метод + +#### Результаты benchmarks/ips на маленьком файле + +``` +2.929k (± 8.9%) i/s - 7.608k in 25.412108s + with 95.0% confidence +``` +#### Результат ruby-prof на большом файле +``` +Total: 48.899145 + + 15.33 40.670 7.496 0.000 33.174 1 #foreach + 11.68 8.254 5.713 0.000 2.541 2750940 User#update + 8.74 4.275 4.275 0.000 0.000 3250940 String#split + 7.17 3.505 3.505 0.000 0.000 8250940 #instance + 7.06 16.015 3.450 0.000 12.565 500000 Object#make_report +``` + +### Ваша находка №3 +Операции с потоками вода/вывода данных очень тяжелые. Если с записью файлов пока ни чего не сделать. +То на с чтением можно по работать. Как раз на это и указывает профайл от ruby-prof. +Я вынес чтение файла в отдельный метод. Файл так же читается по строчно и заполняет массив строк. +Агрегирование данных будет происходить из данного массива. + +#### benchmarks/ips Дал не значительный прирост c маленьким файлом. + +#### Результат ruby-prof на большом файле +А тут уже интересно) +``` +Total: 36.541374 +Sort by: self_time + + %self total self wait child calls name location + 13.75 28.591 5.026 0.000 23.565 1 Array#reject! + 11.51 5.818 4.206 0.000 1.612 2750940 User#update /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:11 + 8.84 3.229 3.229 0.000 0.000 3250940 String#split + 8.44 5.849 3.085 0.000 2.764 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json + 7.31 11.575 2.671 0.000 8.904 500000 Object#make_report /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:39 + 6.55 2.392 2.392 0.000 0.000 8250940 #instance /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:15 + 5.36 1.957 1.957 0.000 0.000 8126279 String#encode + 3.30 1.205 1.205 0.000 0.000 1 #foreach + 3.01 1.099 1.099 0.000 0.000 2750940 Set#add /usr/share/rvm/ +``` + +Но чтобы память не забивалась. Требуется очищать массив от устаревших данных. +А так же можно будет поиграться с тредами. + +C тредами поялвилась проблема с последовательностью записи данных в отчет. Чтобы не забивать память массивом строк (а после это появились фризы). Решил откатить на время измерения. Буду продолжать отимизацию читаю файл по строчно. + +### Ваша находка №4 +Отчет ruby-prof указал на точку раста создания юзера. +``` +12.91 29.410 5.906 0.000 23.505 3250940 Object#make_user /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:59 + 10.76 4.924 4.924 0.000 0.000 3250940 String#split + 9.58 6.181 4.381 0.000 1.800 2750940 User#update /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:10 + 7.41 37.725 3.391 0.000 34.334 1 #foreach + 7.20 7.423 3.293 0.000 4.130 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json +``` +При агргировании данных формируется излишний класс User после которого идёт агрегация данных. +Решение: +Убрать абстракию User + +Провел очередной рефакт. Убрал абстракцию User. + +###Результаты +На агреггирование большого файала ушло время +Finish in 25.983555996994255 + +В рамки бюджета уложились + +ruby-prof показал такую информацию + +``` + Total: 0.000528 + Sort by: self_time + + %self total self wait child calls name location + 15.18 0.000 0.000 0.000 0.000 18 Object#make_report /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:39 + 11.48 0.000 0.000 0.000 0.000 1 File#initialize + 10.68 0.000 0.000 0.000 0.000 1 IO#close + 8.59 0.000 0.000 0.000 0.000 1 #foreach + 5.49 0.000 0.000 0.000 0.000 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json + 3.88 0.000 0.000 0.000 0.000 18 String#split + 3.81 0.001 0.000 0.000 0.001 1 [global]# ruby-prof-flat.rb:7 + 3.69 0.000 0.000 0.000 0.000 36 Object#user? /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:31 + 2.62 0.001 0.000 0.000 0.000 1 Object#work /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:4 + 2.59 0.000 0.000 0.000 0.000 54 String#encode + 2.20 0.000 0.000 0.000 0.000 10 Array#join + 2.07 0.000 0.000 0.000 0.000 30 Object#browser /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:35 + 1.75 0.000 0.000 0.000 0.000 15 Set#add /usr/share/rvm/rubies/ruby-2.6.3/lib/ruby/2.6.0/set.rb:348 + 1.72 0.000 0.000 0.000 0.000 44 String#upcase +``` +Слишком часто вызывается метод предикат user. Оптимизировал данный процесс. + +Проверил сериализацию данных из хеш в json, (хещ был взять с данных тестового файла) таких гемов: +* to_json +* oj +* multi_json + +``` +Comparison: + oj: 199997.6 i/s + MultiJson: 120658.5 i/s - 1.66x slower + MultiJson pretty: 96294.8 i/s - 2.08x slower + json: 54341.7 i/s - 3.68x slower + +``` + +Oj стал фаворитом. Но сталкнулся с проблемой. Он не переводит символы в строки. (подобная проблема уже есть в issue) +Поставил библиотеку multi_json + +Результат +Finish in 21.9 - 23.8 + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными за 21-23 секунды. + + + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы был добавлен тест агреггирование 100_000 строк данных diff --git a/data_large.txt.gz b/data_large.txt.gz deleted file mode 100644 index 823c793a..00000000 Binary files a/data_large.txt.gz and /dev/null differ diff --git a/result2.json b/result2.json new file mode 100644 index 00000000..ad485631 --- /dev/null +++ b/result2.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_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..f2e95328 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,12 @@ +require_relative '../task-1' +require 'rspec-benchmark' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers + config.shared_context_metadata_behavior = :apply_to_host_groups + + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + +end diff --git a/spec/work_spec.rb b/spec/work_spec.rb new file mode 100644 index 00000000..54d41f59 --- /dev/null +++ b/spec/work_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe 'parser small file' do + let(: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" } + let(:file_result) { File.read('result.json') } + + it 'should write result in to file' do + work('data.txt') + expect(file_result).to eq expected_result + end +end + +describe 'parser large file' do + it 'should perform under 30 s' do + expect{ work('data_large.txt') }.to perform_under(30).sec + end +end diff --git a/task-1.rb b/task-1.rb index 778672df..6d025314 100644 --- a/task-1.rb +++ b/task-1.rb @@ -1,176 +1,66 @@ -# Deoptimized version of homework task +require 'multi_json' +require 'set' -require 'json' -require 'pry' -require 'date' -require 'minitest/autorun' +def work(file = 'data.txt') + filer = File.new('result.json', 'w') + @report = { totalUsers: 0, uniqueBrowsersCount: 0, totalSessions: 0, allBrowsers: Set.new, usersStats: {} } + @user = '' -class User - attr_reader :attributes, :sessions + File.foreach(file) do |line| + cols = line.split(',') + is_user = cols.first.eql? 'user' + @user = is_user ? "#{cols[2]} #{cols[3]}" : @user - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions + make_report(cols, @user, is_user) 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], - } + prepare_report + filer.write"#{MultiJson.dump(@report)}\n" + filer.close 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 +private -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 browser_decoration(browsers) + browsers.map(&:upcase).sort.join(',') end -def work - file_lines = File.read('data.txt').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 +def browser(name) + name.upcase +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 } } +def make_report(cols, user, is_user = false) + if is_user + @report[:usersStats][user] = {sessionsCount: 0, + totalTime: [0, 'min.'], + longestSession: [0, 'min.'], + browsers: [], + usedIE: false, + alwaysUsedChrome: true, + dates: []} + @report[:totalUsers] += 1 + elsif cols.first.eql? 'session' + @report[:totalSessions] += 1 + @report[:allBrowsers].add(browser(cols[3])) + + @report[:usersStats][user][:sessionsCount] += 1 + @report[:usersStats][user][:browsers] << browser(cols[3]) + @report[:usersStats][user][:usedIE] = true if @report[:usersStats][user][:usedIE] || cols[3] =~ /Internet Explorer/ + @report[:usersStats][user][:alwaysUsedChrome] = false if !@report[:usersStats][user][:alwaysUsedChrome] || cols[3] !~ /Chrome/ + @report[:usersStats][user][:dates] << cols[5].chomp + @report[:usersStats][user][:totalTime][0] += cols[4].to_i + @report[:usersStats][user][:longestSession][0] = cols[4].to_i if @report[:usersStats][user][:longestSession][0] < cols[4].to_i 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 prepare_report + @report[:uniqueBrowsersCount] = @report[:allBrowsers].length + @report[:allBrowsers] = browser_decoration(@report[:allBrowsers]) - 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') + @report[:usersStats].each_value do |user| + user[:totalTime] = user[:totalTime].join(' ') + user[:browsers] = user[:browsers].sort.join(', ') + user[:longestSession] = user[:longestSession].join(' ') + user[:dates] = user[:dates].sort.reverse end end