From a1ae836555b850cf3fab20584367ea985e1b71e1 Mon Sep 17 00:00:00 2001 From: potashin Date: Tue, 7 May 2024 01:46:52 +0300 Subject: [PATCH 1/2] chore: optimize --- .gitignore | 24 +++++ .ruby-version | 1 + Gemfile | 7 ++ Gemfile.lock | 42 ++++++++ README.md | 18 ++-- case-study.md | 71 +++++++++++++ data.txt | 18 ---- memory_profiler/report.rb | 10 ++ ruby_prof/allocation.rb | 22 ++++ ruby_prof/memory.rb | 14 +++ spec/task_spec.rb | 27 +++++ stackprof/allocation.rb | 8 ++ task-2.rb | 215 ++++++++++++++++---------------------- test/test.rb | 35 +++++++ tmp/.keep | 0 tmp/memory_profiler/.keep | 0 tmp/ruby_prof/.keep | 0 tmp/stackprof/.keep | 0 tmp/valgrind/.keep | 0 work.rb | 5 + 20 files changed, 367 insertions(+), 150 deletions(-) create mode 100644 .gitignore create mode 100644 .ruby-version create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 case-study.md delete mode 100644 data.txt create mode 100644 memory_profiler/report.rb create mode 100644 ruby_prof/allocation.rb create mode 100644 ruby_prof/memory.rb create mode 100644 spec/task_spec.rb create mode 100644 stackprof/allocation.rb create mode 100644 test/test.rb create mode 100644 tmp/.keep create mode 100644 tmp/memory_profiler/.keep create mode 100644 tmp/ruby_prof/.keep create mode 100644 tmp/stackprof/.keep create mode 100644 tmp/valgrind/.keep create mode 100644 work.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..886fb4ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +result.json +data*.txt + +/tmp/* +!/tmp/.keep + +/tmp/memory_profiler/* +!/tmp/memory_profiler/ +!/tmp/memory_profiler/.keep + +/tmp/ruby_prof/* +!/tmp/ruby_prof/ +!/tmp/ruby_prof/.keep + +/tmp/stackprof/* +!/tmp/stackprof/ +!/tmp/stackprof/.keep + +/tmp/valgrind/* +!/tmp/valgrind/ +!/tmp/valgrind/.keep + +# Ignore MacOS system files +.DS_Store diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..0fa4ae48 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.0 \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..b801f94c --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gem 'memory_profiler' +gem 'ruby-prof' +gem 'stackprof' +gem 'rspec' +gem 'rspec-benchmark' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..37a96063 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,42 @@ +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) + memory_profiler (1.0.1) + 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.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.1) + ruby-prof (1.7.0) + stackprof (0.2.26) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + memory_profiler + rspec + rspec-benchmark + ruby-prof + stackprof + +BUNDLED WITH + 2.5.4 diff --git a/README.md b/README.md index d73dc702..a12d3d96 100644 --- a/README.md +++ b/README.md @@ -44,15 +44,15 @@ puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)" - в описание `PR` добавьте чеклист и отметьте, что из него сделали; для получения максимальной пользы надо отметить всё. ## Checklist -- [ ] Построить и проанализировать отчёт гемом `memory_profiler` -- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`; -- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`; -- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`; -- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`; -- [ ] Построить и проанализировать текстовый отчёт `stackprof`; -- [ ] Построить и проанализировать отчёт `flamegraph` с помощью `stackprof` и визуализировать его в `speedscope.app`; -- [ ] Построить график потребления памяти в `valgrind massif visualier` и включить скриншот в описание вашего `PR`; -- [ ] Написать тест, на то что программа укладывается в бюджет по памяти +- [x] Построить и проанализировать отчёт гемом `memory_profiler` +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`; +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`; +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`; +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`; +- [x] Построить и проанализировать текстовый отчёт `stackprof`; +- [x] Построить и проанализировать отчёт `flamegraph` с помощью `stackprof` и визуализировать его в `speedscope.app`; +- [x] Построить график потребления памяти в `valgrind massif visualier` и включить скриншот в описание вашего `PR`; +- [x] Написать тест, на то что программа укладывается в бюджет по памяти Не нужно включать в `PR` выводы всех этих отчётов, просто используйте каждый хотя бы по разу в вашем `Case-study`. diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..fe8e3f4a --- /dev/null +++ b/case-study.md @@ -0,0 +1,71 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: количество потребляемой памяти при просчете 100к записей (885 мб на старте, 11.4 секунды) + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: профилирование - изменение кода - тестирование – бенчмаркинг – откат при отсутствии разницы от оптимизации/сохранение результатов + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался memoty_profiler, ruby-prof (allocation/memory), stackprof (allocation), valgrind + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- memory_profiler показал, что больше всего памяти `3587123560` аллоцируется в `task-2.rb:74` (по классу лидирует`5212433896 Array`, что похоже на то, что происходит в `task-2.rb:74`) +- вместо создания нового массива `sessions = sessions + [parse_session(line)] if cols[0] == 'session'` будем использовать пуш `<<` в уже определенный массив (для аналогично для `users`). +- метрика снизилась с 885 до 287 мб +- профилировщик показывает `1305579360 task-2.rb:128` как лидирующую позицию. Количество аллоцированных массивов снизилось до `1499267656 Array` + +### Ваша находка №2 +- memory_profiler показал, что больше всего памяти `1305579360` аллоцируется в `task-2.rb:128` (по классу лидирует`1499267656 Array`, что похоже на то, что происходит в `task-2.rb:128`) +- заменяем `sessions.select { |session| session['user_id'] == user['id'] }` на поиск по предварительно сформированному хэшу `sessions_by_user = sessions.group_by { |session| session['user_id'] }` +- метрика снизилась с 287 до 140 мб +- профилировщик показывает `120416072 task-2.rb:132` как лидирующую позицию. Количество аллоцированных массивов снизилось до `206505392 Array`, лидирующее место теперь занимает `344381297 String`. + +### Ваша находка №3 +- memory_profiler показал, что больше всего памяти `120416072` аллоцируется в `task-2.rb:132` (по классу лидирует `344381297 String`, что не похоже на то, что происходит в `task-2.rb:132`, поскольку там происходят операции с массивами, а не строками). +- вместо `users_objects = users_objects + [user_object]` использую `users_objects << user_object` +- метрика снизилась незначительно, будем считать, что это те же 140 мб +- профилировщик показывает `97472465 task-2.rb:169` как лидирующую позицию. Количество аллоцированных массивов снизилось до `86230952 Array`, лидирующее место по-прежнему занимает `344381297 String`. + +### Ваша находка №4 +- memory_profiler показал, что больше всего памяти `97472465` аллоцируется в `task-2.rb:169` (по классу лидирует `344381297 String`, что похоже на то, что происходит в `task-2.rb:169`) +- заменяю `user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 }` на `user.sessions.map{|s| s['date']}.sort { |d1, d2| d2 <=> d1 }` +- метрика снизилась с 140 до 133 мб +- профилировщик показывает `96369656 json-2.7.2/lib/json/common.rb:220` как лидирующую позицию. Количество аллоцированных строк снизилось до `313934727 String`, это по-прежнему лидирующая позиция. + +### Ваша находка №5 +- профилировщик показывает `96369656 json-2.7.2/lib/json/common.rb:220` как лидирующую позицию +- вместо записи/чтения файла из каждого треда, агрегирую репорты из каждого треда, после чего 1 раз пишу ее содержимое в файл +- метрика снизилась с 133 до 58 мб +- профилировщик показывает `48221760 task-2.rb:72` как лидирующую позицию. Количество аллоцированных строк снизилось до `160207766 String`, это по-прежнему лидирующая позиция. + +### Ваша находка №6 +- профилировщик показывает `48221760 task-2.rb:72` как лидирующую позицию, но дальнейшие попытки оптимизации не приводят к снижению потребляемой памяти: основной точкой роста является ограничение накапливаемых данных +- переписываю программу в потоковом стиле (накапливаем данные только по 1 пользователю и его сессиям за раз, после накопления собираем статистику, пишем в файл и начинаем сначала) +- снизилась до 21мб для 100к (для всего файла тоже 21мб) +- количество строк сократилось до `51749209 String` + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с 885мб до 21мб и уложиться в заданный бюджет (70мб). + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы были написаны тесты на асимптотику, время работы и количество аллоцированных объектов на 10к строк \ No newline at end of file diff --git a/data.txt b/data.txt deleted file mode 100644 index 393b0b8b..00000000 --- a/data.txt +++ /dev/null @@ -1,18 +0,0 @@ -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/memory_profiler/report.rb b/memory_profiler/report.rb new file mode 100644 index 00000000..7b635675 --- /dev/null +++ b/memory_profiler/report.rb @@ -0,0 +1,10 @@ +require_relative '../task-2' + +require 'memory_profiler' + +report = MemoryProfiler.report do + GC.disable + work +end + +report.pretty_print diff --git a/ruby_prof/allocation.rb b/ruby_prof/allocation.rb new file mode 100644 index 00000000..7cf818a9 --- /dev/null +++ b/ruby_prof/allocation.rb @@ -0,0 +1,22 @@ +require_relative '../task-2' + +require 'ruby-prof' + +RubyProf.measure_mode = RubyProf::ALLOCATIONS + +result = RubyProf.profile do + GC.disable + work +end + +printer = RubyProf::FlatPrinter.new(result) +printer.print(File.open('tmp/ruby_prof/flat.txt', 'w+')) + +# printer = RubyProf::DotPrinter.new(result) +# printer.print(File.open('tmp/ruby_prof/graphviz.dot', 'w+')) + +printer = RubyProf::GraphHtmlPrinter.new(result) +printer.print(File.open('tmp/ruby_prof/graph.html', 'w+')) + +printer = RubyProf::CallStackPrinter.new(result) +printer.print(File.open('tmp/ruby_prof/callstack.html', 'w+')) diff --git a/ruby_prof/memory.rb b/ruby_prof/memory.rb new file mode 100644 index 00000000..f7dd94b6 --- /dev/null +++ b/ruby_prof/memory.rb @@ -0,0 +1,14 @@ +require_relative '../task-2' + +require 'ruby-prof' + +RubyProf.measure_mode = RubyProf::MEMORY + +result = RubyProf.profile do + GC.disable + work +end + + +printer = RubyProf::CallTreePrinter.new(result) +printer.print(path: 'tmp/ruby_prof', profile: 'profile') diff --git a/spec/task_spec.rb b/spec/task_spec.rb new file mode 100644 index 00000000..58be06f7 --- /dev/null +++ b/spec/task_spec.rb @@ -0,0 +1,27 @@ +require 'rspec' +require 'rspec-benchmark' +require_relative '../task-2' + +RSpec.describe 'work' do + include RSpec::Benchmark::Matchers + + it 'should be linear' do + expect { |number, _| + `head -n #{number * 1000} data_large.txt > data.txt` + + work + }.to perform_linear.in_range(1, 100) + end + + it 'should perform under 5 seconds' do + `head -n 1000000 data_large.txt > data.txt` + + expect { work }.to perform_under(5).sec + end + + it 'should not allocate more than 110000 objects' do + `head -n 10000 data_large.txt > data.txt` + + expect { work }.to perform_allocation(109643) + end +end diff --git a/stackprof/allocation.rb b/stackprof/allocation.rb new file mode 100644 index 00000000..a0f09725 --- /dev/null +++ b/stackprof/allocation.rb @@ -0,0 +1,8 @@ +require_relative '../task-2' + +require 'stackprof' + +StackProf.run(mode: :object, out: "tmp/stackprof/allocation_#{Time.now.to_i}.dump", raw: true) do + GC.disable + work +end diff --git a/task-2.rb b/task-2.rb index 34e09a3c..17237905 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,9 +1,9 @@ +# frozen_string_literal: true + # Deoptimized version of homework task require 'json' require 'pry' -require 'date' -require 'minitest/autorun' class User attr_reader :attributes, :sessions @@ -14,9 +14,8 @@ def initialize(attributes:, sessions:) end end -def parse_user(user) - fields = user.split(',') - parsed_result = { +def parse_user(fields) + { 'id' => fields[1], 'first_name' => fields[2], 'last_name' => fields[3], @@ -24,37 +23,106 @@ def parse_user(user) } end -def parse_session(session) - fields = session.split(',') - parsed_result = { +def parse_session(fields) + { 'user_id' => fields[1], 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], + 'browser' => fields[3].upcase!, + 'time' => fields[4].to_i, '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)) +def collect_stats_from_users(report:, user:, sessions:, report_file:, &block) + user_stats = { + 'sessionsCount' => 0, + # Собираем количество времени по пользователям + 'totalTime' => 0, + # Выбираем самую длинную сессию пользователя + 'longestSession' => 0, + # Браузеры пользователя через запятую + 'browsers' => [], + # Хоть раз использовал IE? + 'usedIE' => false, + # Всегда использовал только Chrome? + 'alwaysUsedChrome' => false, + # Даты сессий через запятую в обратном порядке в формате iso8601 + 'dates' => [] + } + + report['totalUsers'] += 1 + + sessions.each do |session| + user_stats['sessionsCount'] += 1 + user_stats['totalTime'] += session['time'] + user_stats['browsers'] << session['browser'] + + if user_stats['longestSession'] < session['time'] + user_stats['longestSession'] = session['time'] + end + + unless report['allBrowsers'].include?(session['browser']) + report['allBrowsers'] << session['browser'] + end + report['totalSessions'] += 1 + + user_stats['usedIE'] ||= session['browser'].match?(/INTERNET EXPLORER/) + + user_stats['alwaysUsedChrome'] &&= session['browser'].match?(/CHROME/) + + user_stats['dates'] << session['date'] end + + user_stats['browsers'] = user_stats['browsers'].sort!.join(', ') + user_stats['totalTime'] = user_stats['totalTime'].to_s << ' min.' + user_stats['longestSession'] = user_stats['longestSession'].to_s << ' min.' + user_stats['dates'].sort! { |d1, d2| d2 <=> d1 } + + report_file.write(',') if report['totalUsers'] > 1 + report_file.write({"#{user['first_name']} #{user['last_name']}" => user_stats}.to_json[1..-2]) end -def work - file_lines = File.read('data.txt').split("\n") +def work(file_name: 'data.txt') + # Статистика по пользователям + report = { + 'totalUsers' => 0, + 'uniqueBrowsersCount' => 0, + 'totalSessions' => 0, + 'allBrowsers' => [], + } + + report_file = File.open('result.json', 'a') - users = [] + user = nil sessions = [] - file_lines.each do |line| + report_file.write('{"usersStats":{' ) + + File.foreach(file_name, chomp: true) do |line| cols = line.split(',') - users = users + [parse_user(line)] if cols[0] == 'user' - sessions = sessions + [parse_session(line)] if cols[0] == 'session' + + if cols[0] == 'session' + sessions << parse_session(cols) + else + work_partial(user:, sessions:, report:, report_file:) + + user = parse_user(cols) + end end + work_partial(user:, sessions:, report:, report_file:) + + report['allBrowsers'].sort! + # Подсчёт количества уникальных браузеров + report['uniqueBrowsersCount'] = report['allBrowsers'].size + report['allBrowsers'] = report['allBrowsers'].join(',') + + report_file.write("},#{report.to_json[1..]}\n") + report_file.close +end + +def work_partial(user:, sessions:, report:, report_file: nil) + return unless user # Отчёт в json # - Сколько всего юзеров + # - Сколько всего уникальных браузеров + @@ -70,108 +138,9 @@ def work # - Всегда использовал только Хром? + # - даты сессий в порядке убывания через запятую + - 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:, user:, sessions:, report_file:) - # Браузеры пользователя через запятую - 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 + sessions.clear + user = nil end diff --git a/test/test.rb b/test/test.rb new file mode 100644 index 00000000..62b1db46 --- /dev/null +++ b/test/test.rb @@ -0,0 +1,35 @@ +require_relative '../task-2' +require 'minitest/autorun' + + +class TestMe < Minitest::Test + def setup + File.write('result.json', '') + File.write('data_test.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(file_name: 'data_test.txt') + 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 diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 00000000..e69de29b diff --git a/tmp/memory_profiler/.keep b/tmp/memory_profiler/.keep new file mode 100644 index 00000000..e69de29b diff --git a/tmp/ruby_prof/.keep b/tmp/ruby_prof/.keep new file mode 100644 index 00000000..e69de29b diff --git a/tmp/stackprof/.keep b/tmp/stackprof/.keep new file mode 100644 index 00000000..e69de29b diff --git a/tmp/valgrind/.keep b/tmp/valgrind/.keep new file mode 100644 index 00000000..e69de29b diff --git a/work.rb b/work.rb new file mode 100644 index 00000000..ac5552b2 --- /dev/null +++ b/work.rb @@ -0,0 +1,5 @@ +require_relative 'task-2' + +work(file_name: 'data.txt') + +puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) From 47bf92437defdf342878753d77cf46853a2b029b Mon Sep 17 00:00:00 2001 From: potashin Date: Sat, 11 May 2024 22:19:38 +0300 Subject: [PATCH 2/2] chore: futher optimizations --- task-2.rb | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/task-2.rb b/task-2.rb index 17237905..7e9aad6d 100644 --- a/task-2.rb +++ b/task-2.rb @@ -14,14 +14,8 @@ def initialize(attributes:, sessions:) end end -def parse_user(fields) - { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], - } -end + USER_COLUMNS = %w[id first_name last_name age].freeze + SESSION_COLUMNS = %w[user_id session_id browser time date].freeze def parse_session(fields) { @@ -53,6 +47,9 @@ def collect_stats_from_users(report:, user:, sessions:, report_file:, &block) report['totalUsers'] += 1 sessions.each do |session| + session['time'] = session['time'].to_i + session['browser'].upcase! + user_stats['sessionsCount'] += 1 user_stats['totalTime'] += session['time'] user_stats['browsers'] << session['browser'] @@ -61,15 +58,11 @@ def collect_stats_from_users(report:, user:, sessions:, report_file:, &block) user_stats['longestSession'] = session['time'] end - unless report['allBrowsers'].include?(session['browser']) - report['allBrowsers'] << session['browser'] - end + report['allBrowsers'] << session['browser'] report['totalSessions'] += 1 user_stats['usedIE'] ||= session['browser'].match?(/INTERNET EXPLORER/) - user_stats['alwaysUsedChrome'] &&= session['browser'].match?(/CHROME/) - user_stats['dates'] << session['date'] end @@ -88,31 +81,29 @@ def work(file_name: 'data.txt') 'totalUsers' => 0, 'uniqueBrowsersCount' => 0, 'totalSessions' => 0, - 'allBrowsers' => [], + 'allBrowsers' => Set.new, } report_file = File.open('result.json', 'a') - user = nil + user = {} sessions = [] report_file.write('{"usersStats":{' ) File.foreach(file_name, chomp: true) do |line| - cols = line.split(',') - - if cols[0] == 'session' - sessions << parse_session(cols) + if line.start_with?('session') + sessions << parse({}, SESSION_COLUMNS, line) else work_partial(user:, sessions:, report:, report_file:) - user = parse_user(cols) + parse(user, USER_COLUMNS, line) end end work_partial(user:, sessions:, report:, report_file:) - report['allBrowsers'].sort! + report['allBrowsers'] = report['allBrowsers'].to_a.sort! # Подсчёт количества уникальных браузеров report['uniqueBrowsersCount'] = report['allBrowsers'].size report['allBrowsers'] = report['allBrowsers'].join(',') @@ -121,8 +112,21 @@ def work(file_name: 'data.txt') report_file.close end +def parse(object, columns, line) + col_index = -2 + + line.split(',') do |col| + col_index += 1 + next if col_index < 0 + + object[columns[col_index]] = col + end + + object +end + def work_partial(user:, sessions:, report:, report_file: nil) - return unless user + return if user.empty? # Отчёт в json # - Сколько всего юзеров + # - Сколько всего уникальных браузеров + @@ -142,5 +146,5 @@ def work_partial(user:, sessions:, report:, report_file: nil) collect_stats_from_users(report:, user:, sessions:, report_file:) sessions.clear - user = nil + user.clear end