diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..12b2e711 --- /dev/null +++ b/Gemfile @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'ruby-prof' +gem 'rspec-benchmark' +gem 'ruby-progressbar' +gem 'stackprof' +gem 'pry' +gem 'minitest' +gem 'memory_profiler' \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ad234a45 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +all_reports: + ruby ruby-prof-allocations.rb + ruby ruby-prof-memory.rb + ruby memory-profiler.rb + ruby benchmark.rb diff --git a/a.md b/a.md new file mode 100644 index 00000000..e69de29b diff --git a/benchmark.rb b/benchmark.rb new file mode 100644 index 00000000..706b6813 --- /dev/null +++ b/benchmark.rb @@ -0,0 +1,4 @@ +require_relative 'task-2' +require 'benchmark' + +puts Benchmark.realtime { work("spec/fixtures/files/data_40000.txt", disable_gc: true) } \ No newline at end of file diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..7b6f5568 --- /dev/null +++ b/case-study.md @@ -0,0 +1,281 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: программа не должна потреблять больше **70Мб** памяти при обработке файла `data_large.txt` в течение всей своей работы. +(Учел ошибку первой домашней работы и сделал объем данных больше, для того чтобы проблемы были более очевидны на частичных файлах от основного) +Первичные измерения: +1. `data_20000.txt` +``` +GC enabled: MEMORY USAGE: 131 MB +GC disabled MEMORY USAGE: 1287 MB +``` +2. `data_40000.txt` +``` +GC enabled: MEMORY USAGE: 226 MB +GC disabled MEMORY USAGE: 4856 MB +``` + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 15-20 секунд. + +Вот как я построил `feedback_loop`: +- Собрал в одну команду выполнение всех профилировщиков, для удобства + +Следующие шаги выполняются в цикле +- Зафиксировал бюджет метрики, которую определили выше. + То-есть выбрать объем данных, который выполняется за адекватное время, на первой итерации например 5 сек. (20_000 строк кода на моей машине) и посмотреть на потребляемую память, которая равна 146 мб. + Когда потребляемая память сильно сократиться, для удобства перейти на файлы размером 40_000 и затем 60_000. +- Защитить метрику от дальнейшей деградации, написать тест +- С помощью профилировщика найти главную точку роста +- Внести оптимизационные правки +- С помощью профилировщика проверить есть ли улучшения +- Запустить тест, проверить, если улучшения с включенным GC есть тогда закоммитить. + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался ruby-prof, memory-profiler и qcachegrind + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- Использую data_20000.txt, memory-profiler указал на главную точку роста, а именно: +``` +MEMORY USAGE: 1371 MB +allocated memory by location +----------------------------------- + 1.15 GB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:57 + + Строка 57: + sessions = sessions + [parse_session(line)] if cols[0] == 'session' +``` +Проблема в создании новой переменной на каждой итерации. Заодно исправил этот момент во всем коде. +- Строка 57: +``` +sessions << parse_session(line) if cols[0] == 'session' +``` +- Строка 56 +``` +users << parse_user(line) if cols[0] == 'user' +``` +- Строка 83: +``` +uniqueBrowsers << browser if uniqueBrowsers.all? { |b| b != browser } +``` +- Строка 105: +``` +users_objects << user_object +``` +- как изменилась метрика +``` +GC disabled MEMORY USAGE: 198 MB +GС enabled MEMORY USAGE: 117 MB +``` +- как изменился отчёт профилировщика +``` +MEMORY USAGE: 198 MB +allocated memory by location +----------------------------------- + 800.00 kB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:57 + 800.00 kB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:56 +``` +- В целом, кажется что главная точка роста исправлена со 146 мб удалось получить 117 мб. Коммит и переход к следующей. + +### Ваша находка №2 +- Использую data_20000.txt, memory-profiler указал на главную точку роста, а именно: +``` +MEMORY USAGE: 197 MB +allocated memory by location +----------------------------------- + 413.26 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:103 + + + Строка 103: + user_sessions = sessions.select { |session| session['user_id'] == user['id'] } +``` +- Как и в первой домашней работе заменил перебор всех сессий на хэш с группированные данных по user_id. В данном конкретном месте алгоритмическая сложность с O(n) изменилась на O(1). +``` + user_sessions = sessions.group_by { |session| session['user_id'] } + users_objects = users.map do |user| + User.new(attributes: user, sessions: user_sessions[user['id']] || []) + end +``` +-- как изменилась метрика +``` +GC disabled MEMORY USAGE: 105 MB +GС enabled MEMORY USAGE: 62 MB +``` +- как изменился отчёт профилировщика +``` +MEMORY USAGE: 186 MB +allocated memory by location +----------------------------------- + 700.74 kB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:100 + 536.14 kB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:101 + 121.88 kB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:102 +``` +- В целом, кажется что главная точка роста исправлена со 117 мб удалось получить 62 мб. Коммит и переход к следующей. + + +### Ваша находка №3 +- Использую data_20000.txt, memory-profiler указал на главную точку роста, а именно: +``` +MEMORY USAGE: 186 MB +allocated memory by location +----------------------------------- + 15.44 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:139 + + + Строка 139: + collect_stats_from_users(report, users_objects) do |user| + { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } + end +``` +- Тут видется проблема неоптимального использования метода collect_stats_from_users. + Оптимизировал, а именно избавился от collect_stats_from_users и начал подготавливать данные для отчета за один проход. +``` + users_objects.each do |user| + user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + + # Подготовим данные для сессий + sessions = user.sessions + times = sessions.map { |s| s['time'].to_i } + browsers = sessions.map { |s| s['browser'].upcase } + + report['usersStats'][user_key] = { + # Количество сессий + 'sessionsCount' => sessions.count, + # Общее время + 'totalTime' => "#{times.sum} min.", + # Самая длинная сессия + 'longestSession' => "#{times.max} min.", + # Браузеры через запятую + 'browsers' => browsers.sort.join(', '), + # Хоть раз использовал IE? + 'usedIE' => browsers.any? { |b| b.include?('INTERNET EXPLORER') }, + # Всегда использовал только Chrome? + 'alwaysUsedChrome' => browsers.all? { |b| b.include?('CHROME') }, + # Даты сессий через запятую в обратном порядке в формате iso8601 + 'dates' => user.sessions.map { |s| s['date'] }.sort.reverse + } + end +``` +- как изменилась метрика +``` +GC disabled MEMORY USAGE: 75 MB +GС enabled MEMORY USAGE: 59 MB +``` +- как изменился отчёт профилировщика +``` +MEMORY USAGE: 116 MB +allocated memory by location +----------------------------------- + 121.84 kB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:108 + 242.98 kB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:112 + 921.14 kB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:113 + 633.57 kB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:115 +``` +- Главная точка роста исправлена со 62 мб удалось получить 59 мб. Коммит и переход к следующей. + +### Ваша находка №4 +- Перешел на использование data_40000.txt, профайлеры указал на главную точку роста, а именно: +``` +MEMORY USAGE: 206 MB +allocated memory by file +----------------------------------- + 18.95 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:55 + 16.25 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:28 + + Строка 28: + fields = session.split(',') + + Строка 55: + cols = line.split(',') +``` +Так же удалось поставить kcachegrind на ubuntu. Были проблемы с зависимостями. + +- Тут проблема неоптимального использования метода split. + Оптимизировал, а именно избавился от избыточного использования split. Перешел на использование переменных. +``` +def parse_session(user_id, session_id, browser, time, date) + { + 'user_id' => user_id, + 'session_id' => session_id, + 'browser' => browser, + 'time' => time, + 'date' => date, + } +end +``` +- как изменилась метрика +``` +GC disabled MEMORY USAGE: 66 MB +GС enabled MEMORY USAGE: 58 MB +``` +- как изменился отчёт профилировщика +``` +MEMORY USAGE: 156 MB +allocated memory by file +----------------------------------- + 18.95 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:53 + 1.03 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:24 +``` +- Главная точка роста исправлена со 59 мб удалось получить 58 мб. Коммит и переход к следующей. + +### Ваша находка №5 +- Использую data_40000.txt, профайлеры показывают, что точки роста есть, но они незначительные. Скорее всего их исправление не поможет уложиться в 70 мБ для полного файла. + К тому же на данном этапе мы сохраняем весь файл целиком в файл, только на чтение `data_large.txt` и сохранение в `result.json` потребуется значительно больше памяти. + Поэтому дальше попробую воспользоваться подсказкой из readme перепишу в "потоковом" стиле, то-есть чтение/запись по строкам. Это должно кратно сократить потребление памяти на больших объемах. + +- Проблема в считывании всего файла и подготовки отчета, целиком. +``` +``` +- как изменилась метрика +``` +GC disabled MEMORY USAGE: 86 MB +GС enabled MEMORY USAGE: 36 MB +``` +- как изменился отчёт профилировщика. +``` +MEMORY USAGE: 199 MB +allocated memory by file +----------------------------------- + 75.94 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb + 15.42 kB /home/alex/.asdf/installs/ruby/2.7.8/lib/ruby/2.7.0/set.rb + 40.00 B memory-profiler.rb + +allocated memory by location +----------------------------------- + 30.05 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:7 + 12.89 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:9 + 6.57 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:43 + 4.74 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:58 + 4.02 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:59 + 3.10 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:64 + 2.40 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:8 + 2.11 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:39 + 2.03 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:71 + 1.79 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:42 + 1.55 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:22 + 1.40 MB /home/alex/documents/studies/rails-optimization-task2/task-2.rb:19 +``` +- Пришлось переписать весть код, перейти на построчное считывание данных и их запись. Так же запустил тест на файле `data_large.txt`, в итоге уложился в метрику с двухкратным запасом. Файл потребляет ~38 мБ. + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с 131 MB при выполнении 20_000к строк и 226 MB при выполнении 40_000к строк в начале, до ~38 мБ на файле `data_large.txt` и уложиться в заданный бюджет. + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы был написан performance тест для полного объема файла `data_large.txt` diff --git a/data_large.txt.gz b/data_large.txt.gz deleted file mode 100644 index 91c7e45e..00000000 Binary files a/data_large.txt.gz and /dev/null differ diff --git a/memory-profiler.rb b/memory-profiler.rb new file mode 100644 index 00000000..c5b9f094 --- /dev/null +++ b/memory-profiler.rb @@ -0,0 +1,8 @@ +require_relative 'task-2' +require 'benchmark' +require 'memory_profiler' + +report = MemoryProfiler.report do + work('spec/fixtures/files/data_60000.txt', true) +end +report.pretty_print(scale_bytes: true) diff --git a/ruby-prof-allocations.rb b/ruby-prof-allocations.rb new file mode 100644 index 00000000..80058381 --- /dev/null +++ b/ruby-prof-allocations.rb @@ -0,0 +1,20 @@ +require 'ruby-prof' +require_relative 'task-2' + +RubyProf.measure_mode = RubyProf::ALLOCATIONS + +result = RubyProf::Profile.profile do + work("spec/fixtures/files/data_40000.txt", true) +end + +printer = RubyProf::FlatPrinter.new(result) +printer.print(File.open('ruby_prof_reports/flat_allocations.txt', 'w+')) + +printer = RubyProf::GraphHtmlPrinter.new(result) +printer.print(File.open('ruby_prof_reports/graph_allocations.html', 'w+')) + +printer = RubyProf::CallStackPrinter.new(result) +printer.print(File.open('ruby_prof_reports/callstack_allocations.html', 'w+')) + +printer = RubyProf::CallTreePrinter.new(result) +printer.print(path: 'ruby_prof_reports', profile: 'profile') diff --git a/ruby-prof-memory.rb b/ruby-prof-memory.rb new file mode 100644 index 00000000..2ff6e7a0 --- /dev/null +++ b/ruby-prof-memory.rb @@ -0,0 +1,20 @@ +require 'ruby-prof' +require_relative 'task-2' + +RubyProf.measure_mode = RubyProf::MEMORY + +result = RubyProf::Profile.profile do + work("spec/fixtures/files/data_40000.txt", true) +end + +printer = RubyProf::FlatPrinter.new(result) +printer.print(File.open('ruby_prof_reports/flat_memory.txt', 'w+')) + +printer = RubyProf::GraphHtmlPrinter.new(result) +printer.print(File.open('ruby_prof_reports/graph_memory.html', 'w+')) + +printer = RubyProf::CallStackPrinter.new(result) +printer.print(File.open('ruby_prof_reports/callstack_memory.html', 'w+')) + +printer = RubyProf::CallTreePrinter.new(result) +printer.print(path: 'ruby_prof_reports', profile: 'profile') \ No newline at end of file diff --git a/spec/fixtures/files/data.txt b/spec/fixtures/files/data.txt new file mode 100644 index 00000000..393b0b8b --- /dev/null +++ b/spec/fixtures/files/data.txt @@ -0,0 +1,18 @@ +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 diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000..51226909 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,5 @@ +require 'rspec-benchmark' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end diff --git a/spec/task-2_spec.rb b/spec/task-2_spec.rb new file mode 100644 index 00000000..9eb4a10c --- /dev/null +++ b/spec/task-2_spec.rb @@ -0,0 +1,29 @@ +require_relative './rails_helper' +require_relative '../task-2' + +describe 'task-1' do + let(:result) { './spec/fixtures/files/result.json' } + + describe 'Result' do + let(:file) { './spec/fixtures/files/data.txt' } + + let(:expected_result) do + # '{"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" + '{"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"]}},"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"}' + end + + it 'returns equal' do + work(file) + expect(File.read(result)).to eq(expected_result) + end + end + + describe 'Memory usage' do + let(:data_file_path) { "./data_large.txt" } + + it 'performs success with data_large.txt' do + work(data_file_path) + expect((`ps -o rss= -p #{Process.pid}`.to_i / 1024)).to be < 38 + end + end +end diff --git a/stackprof.rb b/stackprof.rb new file mode 100644 index 00000000..7a2d02b6 --- /dev/null +++ b/stackprof.rb @@ -0,0 +1,6 @@ +require_relative 'task-2' +require 'stackprof' + +StackProf.run(mode: :object, out: 'ruby_prof_reports/stackprof.dump', row: true) do + work('./spec/fixtures/files/data_20000.txt', true) +end diff --git a/task-2.rb b/task-2.rb index 34e09a3c..531b7584 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,177 +1,92 @@ -# Deoptimized version of homework task +# frozen_string_literal: true require 'json' -require 'pry' -require 'date' -require 'minitest/autorun' +require 'set' -class User - attr_reader :attributes, :sessions - - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions - end +def parse_line(line) + type, id, *params = line.split(',') + [type, id, *params] end -def parse_user(user) - fields = user.split(',') - parsed_result = { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], +def initialize_user(params) + { + user_key: "#{params[0]} #{params[1]}", + sessionsCount: 0, + totalTime: 0, + longestSession: 0, + usedIE: false, + alwaysUsedChrome: true, + dates: [], + browsers: [] } 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], - } +def update_user_with_session(user, browser, time, date) + user[:browsers] << browser + user[:usedIE] ||= browser.include?('INTERNET EXPLORER') + user[:alwaysUsedChrome] &&= browser.include?('CHROME') + user[:totalTime] += time.to_i + user[:sessionsCount] += 1 + user[:longestSession] = [user[:longestSession], time.to_i].max + user[:dates] << date end -def collect_stats_from_users(report, users_objects, &block) - users_objects.each do |user| - user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" - report['usersStats'][user_key] ||= {} - report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) - end +def build_user_stat(user) + { + sessionsCount: user[:sessionsCount], + totalTime: "#{user[:totalTime]} min.", + longestSession: "#{user[:longestSession]} min.", + browsers: user[:browsers].sort.join(', '), + usedIE: user[:usedIE], + alwaysUsedChrome: user[:alwaysUsedChrome], + dates: user[:dates].sort.reverse + }.to_json 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' +def work(file_path, disable_gc = false) + GC.disable if disable_gc + + unique_browsers = Set.new + total_users = 0 + total_sessions = 0 + current_user = nil + is_first_user = true + + File.open('spec/fixtures/files/result.json', 'w') do |result| + result.write('{"usersStats":{') + + File.foreach(file_path, chomp: true) do |line| + type, _id, *params = parse_line(line) + + if type == 'user' + if current_user + result.write(',') unless is_first_user + result.write("\"#{current_user[:user_key]}\":#{build_user_stat(current_user)}") + is_first_user = false + end + + current_user = initialize_user(params) + total_users += 1 + elsif type == 'session' + browser = params[1].upcase + update_user_with_session(current_user, browser, params[2], params[3]) + unique_browsers.add(browser) + total_sessions += 1 + end + end + + if current_user + result.write(',') unless is_first_user + result.write("\"#{current_user[:user_key]}\":#{build_user_stat(current_user)}") + end + + result.write('},') + result.write("\"totalUsers\":#{total_users},") + result.write("\"uniqueBrowsersCount\":#{unique_browsers.count},") + result.write("\"totalSessions\":#{total_sessions},") + result.write("\"allBrowsers\":\"#{unique_browsers.sort.join(',')}\"") + result.write('}') end - # Отчёт в json - # - Сколько всего юзеров + - # - Сколько всего уникальных браузеров + - # - Сколько всего сессий + - # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + - # - # - По каждому пользователю - # - сколько всего сессий + - # - сколько всего времени + - # - самая длинная сессия + - # - браузеры через запятую + - # - Хоть раз использовал IE? + - # - Всегда использовал только Хром? + - # - даты сессий в порядке убывания через запятую + - - report = {} - - report[:totalUsers] = users.count - - # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end - - report['uniqueBrowsersCount'] = uniqueBrowsers.count - - report['totalSessions'] = sessions.count - - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') - - # Статистика по пользователям - users_objects = [] - - users.each do |user| - attributes = user - user_sessions = sessions.select { |session| session['user_id'] == user['id'] } - user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects = users_objects + [user_object] - end - - report['usersStats'] = {} - - # Собираем количество сессий по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } - end - - # Собираем количество времени по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' } - end - - # Выбираем самую длинную сессию пользователя - collect_stats_from_users(report, users_objects) do |user| - { 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' } - end - - # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end - - # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end - - # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } - end - - # Даты сессий через запятую в обратном порядке в формате iso8601 - collect_stats_from_users(report, users_objects) do |user| - { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } - end - - File.write('result.json', "#{report.to_json}\n") puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end - -class TestMe < Minitest::Test - def setup - File.write('result.json', '') - File.write('data.txt', -'user,0,Leida,Cira,0 -session,0,0,Safari 29,87,2016-10-23 -session,0,1,Firefox 12,118,2017-02-27 -session,0,2,Internet Explorer 28,31,2017-03-28 -session,0,3,Internet Explorer 28,109,2016-09-15 -session,0,4,Safari 39,104,2017-09-27 -session,0,5,Internet Explorer 35,6,2016-09-01 -user,1,Palmer,Katrina,65 -session,1,0,Safari 17,12,2016-10-21 -session,1,1,Firefox 32,3,2016-12-20 -session,1,2,Chrome 6,59,2016-11-11 -session,1,3,Internet Explorer 10,28,2017-04-29 -session,1,4,Chrome 13,116,2016-12-28 -user,2,Gregory,Santos,86 -session,2,0,Chrome 35,6,2018-09-21 -session,2,1,Safari 49,85,2017-05-22 -session,2,2,Firefox 47,17,2018-02-02 -session,2,3,Chrome 20,84,2016-11-25 -') - end - - def test_result - work - expected_result = JSON.parse('{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}') - assert_equal expected_result, JSON.parse(File.read('result.json')) - end -end