diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ad41dc5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +data_large.txt +*.json 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..8062e8a4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +ruby '3.3.6' +source "https://rubygems.org" + +gem 'minitest' +gem 'ruby-prof' +gem 'memory_profiler' +gem 'pry' +gem 'stackprof' +gem 'rspec-benchmark' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..61b136e3 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,52 @@ +GEM + remote: https://rubygems.org/ + specs: + benchmark-malloc (0.2.0) + benchmark-perf (0.6.0) + benchmark-trend (0.4.0) + coderay (1.1.3) + diff-lcs (1.5.1) + memory_profiler (1.1.0) + method_source (1.1.0) + minitest (5.25.4) + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) + 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.3) + 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.1) + stackprof (0.2.27) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + memory_profiler + minitest + pry + rspec-benchmark + ruby-prof + stackprof + +RUBY VERSION + ruby 3.3.6p108 + +BUNDLED WITH + 2.5.22 diff --git a/README.md b/README.md index d73dc702..03037cf3 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ Можем считать, что все сессии юзера всегда идут одним непрерывным куском. Нет такого, что сначала идёт часть сессий юзера, потом сессии другого юзера, и потом снова сессии первого. - ## План работы В этот раз переработка потребуется кардинальная, так что нужно сделать два этапа diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..22841db8 --- /dev/null +++ b/case-study.md @@ -0,0 +1,339 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумала использовать такую метрику: + +Измерила (вариант с изменениями из первого задания) с помощью предложенной команды: + +`puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)` + +Результат в начале: + +``` +# с GC + +MEMORY USAGE: 2444 MB + +# без GC + +MEMORY USAGE: 5924 MB +``` + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроила эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 35 секунд. + +Вот как я построила `feedback_loop`: + +Прописала, чтобы оценивать кол-во памяти: +```ruby +puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) +``` + +На этапе отладки потокового варианта использовала sample, к-й позволял оценить работоспособность в пределах пары секунд. + +Далее подобрала sample, с к-м фидбек можно было получить за 5-20 секунд. + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовалась `ruby-prof`. Пробовала другие (stackprof, kcachgrind (да, у меня он с k)), ничего особо нового из них не узнала. + +Вот какие проблемы удалось найти и решить + +### Ваша находка №0 + +Переход на потоковую обработку. + +По времени программа стала работать медленнее (25с => 33-34c , с gc) + +По памяти произошло улучшение: + +`MEMORY USAGE: 2444 MB` +=> +`MEMORY USAGE: 332 MB` + +Думала использовать `gem 'json-write-stream'` , но неудобно писать вложенный json для `usersStats`, поэтому решила напрямую + +### Ваша находка №1 + +memory profiler показал ,что File очень много ест памяти, но непонятно, как от него избавиться. + +Также посмотрела qcachegrind (у меня kcachegrind) - много отнимают `write_user_to_json` , `Array#each`. + +Также создала второй тред, вот его отчёт: + +``` +MEMORY USAGE: 29 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +22.006778048 +``` + +Подумала, раз `File`, то не открывать/закрывать его для каждого юзера, а открыть на запись 1 раз. + +Переписала - по памяти ничего не поменялось, но программа стала быстрее отрабатывать (27 => 21 секунд, если проверять на большом файле без профилирования) + +### Ваша находка №2 + +Далее снова посмотрела с помощью memory_profiler: + +Увидела, что String ест много памяти, причём на месте `split(',')` + +Заметила, что при проверке строки "юзер" или "сессия" вообще необязательно split делать. Но также заметила, что split делается 2 раза для каждой строки: для проверки user / session и для парсинга юзера/сессии. + +Сначала переписала на разовый `split` и передачу `cols` в `parse_session` и `parse_user`. + +```ruby +cols = line.split(',') +... +parse_user(cols) +``` + +Использование памяти на sample снизилось с: +``` +Total allocated: 179.51 MB (2546193 objects) +``` +До: + +``` +Total allocated: 131.29 MB (1761624 objects) +``` + +### Предупреждение об особенностях работы + +[Тут было ещё несколько находок и итераций, но где-то закралась ошибка, поэтому убрала их] +[+ в целом было несколько заходов туда-сюда, оставила лучший из вариантов] + +### Ваша находка номер 4 + +Теперь (кроме File): + +```ruby +current_user = User.new(attributes: parsed_user, sessions: []) +``` + +Можно оптимизировать `User` => `OptimizedUser` ++ увидела, что не используются id и age, их убираем из объекта. + +Заменила на оптимизированного юзера без лишних полей: +```ruby +OptimizedUser = Struct.new(:full_name, :sessions, keyword_init: true) + +current_user = OptimizedUser.new(full_name: "#{first_name} #{last_name}", sessions: []) +``` + +Стало: + +``` +Total allocated: 126.35 MB (1730762 objects) +``` + +### Находка 5 + +Увидела, что у меня browser.upcase 2 раза, решила сделать 1. + +``` +Total allocated: 122.13 MB (1646193 objects) +``` + +### 6 + +Сделала сессию массивом. + +``` +Total allocated: 112.82 MB (1646193 objects) +``` + ++ upcase + +``` +Total allocated: 104.38 MB (1477055 objects) +``` + +### 7 + +Сделала один split: + +``` +Total allocated: 97.00 MB (1292486 objects) +``` +### 8 + +Увидела много объектов `','`, завела константу `DELIMITER = ','` (omg, чем я занимаюсь) + +``` +Total allocated: 92.38 MB (1177056 objects) +# и ещё одну +Total allocated: 91.76 MB (1161626 objects) +``` + +### 9 + +Сделала `user_or_session.shift` для получения первого эл-та массива: +Ну такое: + +``` +Total allocated: 91.15 MB (1155726 objects) +``` + +### 10 + +Убрала `Struct`, сделала обычный класс + +``` +Total allocated: 88.68 MB (1140295 objects)s +``` + +### 11 + +Убрала объект `User` - тут не особо что-то изменилось. + +``` +Total allocated: 88.32 MB (1124864 objects) +``` + +### 12 + +Поменяла режим записи в файл: + +``` +Total allocated: 87.90 MB (1124625 objects) +``` + +### Тупик + +Далее тупик, остальные ухищрения делают только хуже. + +При этом, при замере на большом файле использование памяти практически не изменилось (после замены на потоковую обработку): + +``` +MEMORY USAGE: 27 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +10.641142708 +``` + +Проблемные места: + +```ruby +line = line.split(DELIMITER) + +dates.sort.reverse + +File.readlines +``` + +Много строк "session" создаётся при `split` + много строк типа "0", "1" и т.д. + +Посмотрела stackprof, примерно то же показывает. + +## 12 + +Попробовала переписать ещё более потоково - не накапливать сессии в рамках каждого юзера в массиве, а считать по мере прохождения по строкам в переменных и писать сразу по позможности, юзера писать сразу. + +Результат минимальный (на 100_000 строк): +``` +Total allocated: 86.70 MB (1099665 objects) +``` + +Замеры (память и время): + +На файле `data_large.txt`: + +``` +MEMORY USAGE: 27 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +9.926471411 +``` + +На 100_000 строк: +``` +MEMORY USAGE: 27 MB +0.243047239 +``` + +На 300_000 строк: +``` +MEMORY USAGE: 27 MB +MEMORY USAGE: 122 MB +MEMORY USAGE: 122 MB +MEMORY USAGE: 122 MB +3.195018119 +``` + +Далее можно использовать `oj` , но т.к. он не касается проблемных мест, то не стала использовать. + +## Находка 13 + +Решила дополнительно поресёрчить, можно ли более оптимально прочитать файл. +Просто я была уверена, что `readlines` это и есть `foreach`. Да и `open` , к-й был, сработал бы. + +Заменила `readlines` на `foreach`: + +Результат на data_large.txt: + +``` +MEMORY USAGE: 27 MB +MEMORY USAGE: 29 MB +MEMORY USAGE: 29 MB +MEMORY USAGE: 29 MB +MEMORY USAGE: 29 MB +MEMORY USAGE: 29 MB +MEMORY USAGE: 29 MB +Time: 6.599629822 seconds +TOTAL MEMORY USAGE: 29 MB +``` + +## Результаты +В результате проделанной оптимизации удалось обработать файл с данными. +Удалось улучшить метрику системы с использования 2444 MB и 30с до обработки целевого файла за 6-7 секунд и 29mb памяти, но не удалось уложиться в заданный бюджет. + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написана парочка performance-тестов. diff --git a/draft.md b/draft.md new file mode 100644 index 00000000..bb09006b --- /dev/null +++ b/draft.md @@ -0,0 +1,111 @@ + + + +На sample до оптимизации split: + +Total allocated: 179.51 MB (2546193 objects) +Total retained: 0 B (0 objects) + +cols + +---------- + +после передачи cols: + +Total allocated: 131.29 MB (1761624 objects) +Total retained: 0 B (0 objects) + + + +-------------------- +Перепишу без массива (набор переменных): + +anna@composaurus:~/apps/rails-optimization/rails-optimization-task2$ be ruby task-2.rb +Total allocated: 51.71 MB (854936 objects) +Total retained: 0 B (0 objects) + +О, круто! + + +------------------------------- +Из первого отчёта (по 100_000 строк) + + +Total allocated: 312.22 MB (2609218 objects) +Total retained: 6.58 kB (48 objects) + + +Вот хз, Array#each да readlines много занимают. + +Io.open ещё. + + +allocated memory by gem +----------------------------------- + 312.10 MB other + 121.74 kB set + 295.00 B bundled_gems + +allocated memory by file +----------------------------------- + 312.10 MB task-2.rb + 121.74 kB /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb + 295.00 B /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/bundled_gems.rb + +allocated memory by location +----------------------------------- + 131.47 MB task-2.rb:47 + 48.22 MB task-2.rb:79 + 41.43 MB task-2.rb:27 + 13.53 MB task-2.rb:28 + 9.45 MB task-2.rb:57 + 9.32 MB task-2.rb:49 + 9.03 MB task-2.rb:78 + 7.89 MB task-2.rb:87 + 6.79 MB task-2.rb:17 + 5.52 MB task-2.rb:43 + 5.26 MB task-2.rb:54 + 4.22 MB task-2.rb:94 + 4.00 MB task-2.rb:80 + 3.38 MB task-2.rb:89 + 2.70 MB task-2.rb:55 + 2.47 MB task-2.rb:18 + 1.73 MB task-2.rb:48 + 1.30 MB task-2.rb:42 + 1.30 MB task-2.rb:45 + 1.03 MB task-2.rb:56 + 747.60 kB task-2.rb:40 + 617.24 kB task-2.rb:52 + 617.24 kB task-2.rb:53 + 100.15 kB /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb:0 + 61.56 kB task-2.rb:51 + 10.00 kB /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb:512 + 8.67 kB task-2.rb:76 + 8.63 kB task-2.rb:100 + 8.52 kB task-2.rb:119 + 7.33 kB /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb:244 + 5.08 kB task-2.rb:122 + 3.86 kB /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb:218 + 400.00 B /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb:852 + 295.00 B /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/bundled_gems.rb:69 + 192.00 B task-2.rb:120 + 120.00 B task-2.rb:121 + 120.00 B task-2.rb:123 + 112.00 B task-2.rb:70 + 80.00 B task-2.rb:131 + 72.00 B task-2.rb:61 + 40.00 B task-2.rb:124 + 40.00 B task-2.rb:66 + +allocated memory by class +----------------------------------- + 130.28 MB File + 102.99 MB String + 53.61 MB Array + 20.95 MB Hash + 2.66 MB MatchData + 1.11 MB Thread::Mutex + 617.24 kB User + 3.86 kB Class + 40.00 B Range + 40.00 B Set diff --git a/measure.rb b/measure.rb new file mode 100644 index 00000000..fc04ef90 --- /dev/null +++ b/measure.rb @@ -0,0 +1,20 @@ + +require './work' + +thread1 = Thread.new do + time = Time.now + work('data_large.txt', gc: true) + after = Time.now + puts "Time: #{after - time} seconds" +end + +Thread.new do + loop do + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + sleep 1 + end +end + +thread1.join + +puts "TOTAL MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) diff --git a/spec/spec.rb b/spec/spec.rb new file mode 100644 index 00000000..9dd06de7 --- /dev/null +++ b/spec/spec.rb @@ -0,0 +1,21 @@ + +require 'rspec-benchmark' +require_relative '../work' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end + +describe 'Performance' do + it 'uses under 30MB of memory' do + expect do + work('test_data.txt') + end.to perform_allocation(31457280).bytes + end + + it 'performs under 7 seconds' do + expect do + work('data_large_sample.txt') + end.to perform_under(7).sec + end +end \ No newline at end of file diff --git a/task-2.rb b/task-2.rb index 34e09a3c..6811ee11 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,148 +1,17 @@ # Deoptimized version of homework task require 'json' -require 'pry' -require 'date' require 'minitest/autorun' +require 'memory_profiler' +require 'ruby-prof' +require './work' +require 'pry' +require 'stackprof' -class User - attr_reader :attributes, :sessions - - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions - end -end - -def parse_user(user) - fields = user.split(',') - parsed_result = { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], - } -end - -def parse_session(session) - fields = session.split(',') - parsed_result = { - 'user_id' => fields[1], - 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[5], - } -end - -def collect_stats_from_users(report, users_objects, &block) - users_objects.each do |user| - user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" - report['usersStats'][user_key] ||= {} - report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) - end -end - -def work - file_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 - - # Даты сессий через запятую в обратном порядке в формате 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) +report = MemoryProfiler.report do + work('data_large_sample.txt', gc: true) end +report.pretty_print(scale_bytes: true) class TestMe < Minitest::Test def setup @@ -172,6 +41,7 @@ def setup 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')) + res = JSON.parse(File.read('result.json')) + assert_equal expected_result, res end end diff --git a/work.rb b/work.rb new file mode 100644 index 00000000..0d2d3b5b --- /dev/null +++ b/work.rb @@ -0,0 +1,105 @@ +DELIMITER = ','.freeze +COMMA = ', '.freeze + +def write_sessions(f, cnt, time_sum, time_max, browsers, dates, ie, chrome) + f.write <<-JSON + \"sessionsCount\": #{cnt}, + \"totalTime\": "#{time_sum} min.", + \"longestSession\": "#{time_max} min.", + \"browsers\": "#{browsers.sort.join(COMMA)}", + \"usedIE\": #{ie}, + \"alwaysUsedChrome\": #{chrome}, + \"dates\": #{dates.sort.reverse} + } + JSON +end + +def work(filename = 'data.txt', gc: true, result: 'result.json') + GC.disable unless gc + + uniqueBrowsers = Set.new + totalSessions = 0 + totalUsers = 0 + # to see if we need a comma + first_user = true + user_label = 'user'.freeze + session_label = 'session'.freeze + + time_sum = 0 + time_max = 0 + browsers = [] + dates = [] + ie = false + chrome = true + sessions_cnt = 0 + + File.open(result, 'w') do |f| + f.write("{ \"usersStats\":{") + + File.foreach(filename, chomp: true).each do |line| + line_type, _, second, third, fourth, fifth = line.split(DELIMITER) + + if line_type == user_label + unless first_user + write_sessions(f, sessions_cnt, time_sum, time_max, browsers, dates, ie, chrome) + f.write DELIMITER + end + + f.write "\"#{second} #{third}\": {" + first_user = false + + time_sum = 0 + time_max = 0 + browsers = [] + dates = [] + ie = false + chrome = true + sessions_cnt = 0 + + totalUsers += 1 + elsif line_type == session_label + third.upcase! # browser + ctime = fourth.to_i + + time_sum += ctime + time_max = ctime if ctime > time_max + browsers << third + unless ie + ie = true if third =~ /INTERNET EXPLORER/ + end + if chrome + chrome = false unless third =~ /CHROME/ + end + dates << fifth + sessions_cnt += 1 + totalSessions += 1 + uniqueBrowsers.add(third) + end + end + write_sessions(f, sessions_cnt, time_sum, time_max, browsers, dates, ie, chrome) + + f.write("},") + f.write "\"uniqueBrowsersCount\": #{uniqueBrowsers.count}," + f.write "\"totalSessions\": #{totalSessions}," + f.write "\"allBrowsers\": \"#{uniqueBrowsers.sort.join(DELIMITER)}\"," + f.write "\"totalUsers\": #{totalUsers}" + f.write("}") + end + + # Отчёт в json + # - Сколько всего юзеров + + # - Сколько всего уникальных браузеров + + # - Сколько всего сессий + + # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + + # + # - По каждому пользователю + # - сколько всего сессий + + # - сколько всего времени + + # - самая длинная сессия + + # - браузеры через запятую + + # - Хоть раз использовал IE? + + # - Всегда использовал только Хром? + + # - даты сессий в порядке убывания через запятую + + + # Подсчёт количества уникальных браузеров +end