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/Gemfile b/Gemfile new file mode 100644 index 00000000..2be733cc --- /dev/null +++ b/Gemfile @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + + +gem 'stackprof' +gem 'ruby-prof' +gem 'memory_profiler' +gem 'rspec' +gem 'byebug' +gem 'rspec-benchmark' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..c73aac59 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,44 @@ +GEM + remote: https://rubygems.org/ + specs: + benchmark-malloc (0.2.0) + benchmark-perf (0.6.0) + benchmark-trend (0.4.0) + byebug (11.1.3) + diff-lcs (1.6.0) + memory_profiler (1.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 + byebug + memory_profiler + rspec + rspec-benchmark + ruby-prof + stackprof + +BUNDLED WITH + 2.5.18 diff --git a/README.md b/README.md index 934cd8a2..56923c7a 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,10 @@ puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)" - файл `case-study.md` с описанием проделанной оптимизации; ## Checklist -- [ ] Потренироваться с `memory_profiler` -- [ ] Потренироваться с `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`; -- [ ] Потренироваться с `stackprof` + `CLI` и `Speedscope` -- [ ] Потренироваться со вторым тредом для мониторинга памяти +- [x] Потренироваться с `memory_profiler` +- [x] Потренироваться с `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`; +- [x] Потренироваться с `stackprof` + `CLI` и `Speedscope` +- [x] Потренироваться со вторым тредом для мониторинга памяти ## Формат шагов case-study Каждый шаг оптимизации в `case-study` должен содержать четыре составляющих: diff --git a/case-study-template.md b/case-study-template.md index c3279664..08487d99 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,44 +12,38 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: количество потребляемой памяти процессом ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за несколько секунд (запуск measurer -> запуск profiler -> анализ) -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +Вот как я построил `feedback_loop`: + - Вынес вычисление метрики в Measurer, в котором запускаются 2 потока - один выполняет бизнес логику, а второй выступает предохранителем при использовании процессом памяти - больше заданного лимита + - Вынес профилирование в Profiler ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался memory_profiler, ruby-prof, stackprof Вот какие проблемы удалось найти и решить ### Ваша находка №1 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +- Одну из главных точек роста я выявил в отчёте memory_profile - модификация массива не in_place +- Убрал лишние аллокации +- Метрика изменилась, но не сильно с 80 МБ до 57 для 15_000 строк +- Количество общих аллокаций по Array стало меньше, но не сильно ### Ваша находка №2 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +- Понял (по подсказке + отчёту memory_profiler), что для значительного буста по памяти необходимо составлять отчёт на лету, без хранения сессий и пользователей, то есть в каждый момент времени будет обрабатываться только один пользователь. +- Добавил составление отчёта на лету при чтении каждой строки +- Метрика изменилась с 57 для 15_000 МБ до 38 для полного объёма +- Значительно уменьшилось количество общий аллокаций по Array и другим классам ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. - -*Какими ещё результами можете поделиться* +Удалось улучшить метрику системы с 15_000 lines: 57 MB, до полной обработки: 38 MB и уложиться в заданный бюджет. ## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы были добавлены тесты, проверяющие корректность бизнес-логики и укладывание в заданные лимиты \ No newline at end of file diff --git a/main.rb b/main.rb new file mode 100644 index 00000000..a9f5c3ac --- /dev/null +++ b/main.rb @@ -0,0 +1,114 @@ +class Main + PARTIAL_VOLUME_FILE_NAME = "dataN.txt" + + def initialize(options: {}) + @source_file_name = options[:source_file_name] || 'data_large.txt' + @count_lines = options[:count_lines] + @current_user = nil + @current_stat = nil + end + + def call = work + + private + + attr_reader :source_file_name, :count_lines + + def work + `head -n #{count_lines} #{source_file_name} > #{PARTIAL_VOLUME_FILE_NAME}` if count_lines + + report = {} + sessions_count = 0 + users_count = 0 + uniq_browsers = Set.new + + File.open('result.json', 'w') do |result_file| + result_file.write('{"usersStats":{') + + File.foreach(count_lines ? PARTIAL_VOLUME_FILE_NAME : source_file_name).each do |line| + fields = line.split(',') + case fields[0] + when 'session' + session = parse_session(fields) + sessions_count += 1 + uniq_browsers.add(session['browser']) + @current_stat = build_current_stat(@current_user, session) + when 'user' + write_user_stat(result_file) + @current_user = parse_user(fields) + @current_stat = build_initial_stat + users_count += 1 + end + end + + write_user_stat(result_file, with_delimeter: false) + + result_file.write("},\"totalUsers\":#{users_count},\"uniqueBrowsersCount\":#{uniq_browsers.count},\"totalSessions\":#{sessions_count},\"allBrowsers\":\"#{uniq_browsers.sort.join(',')}\"}\n") + end + end + + def write_user_stat(file, with_delimeter: true) + return unless @current_user + + file.write("\"#{@current_user['key']}\":#{build_final_stat.to_json}") + file.write(",") if with_delimeter + + @current_user = nil + end + + def build_final_stat + { + 'sessionsCount' => @current_stat['sessionsCount'], + 'totalTime' => "#{@current_stat['totalTime']} min.", + 'longestSession' => "#{@current_stat['longestSession']} min.", + 'browsers' => @current_stat['browsers'].sort.join(', '), + 'usedIE' => @current_stat['usedIE'], + 'alwaysUsedChrome' => @current_stat['alwaysUsedChrome'], + 'dates' => @current_stat['dates'].sort.reverse + } + end + + def build_current_stat(user, session) + { + 'sessionsCount' => @current_stat['sessionsCount'] + 1, + 'totalTime' => @current_stat['totalTime'] + session['time'].to_i, + 'longestSession' => @current_stat['longestSession'] > session['time'] ? @current_stat['longestSession'] : session['time'], + 'browsers' => @current_stat['browsers'] << session['browser'], + 'usedIE' => @current_stat['usedIE'] ? true : !!(session['browser'] =~ /INTERNET EXPLORER/), + 'alwaysUsedChrome' => @current_stat['alwaysUsedChrome'] ? !!(session['browser'].upcase =~ /CHROME/) : false, + 'dates' => @current_stat['dates'] << Date.strptime(session['date'], '%Y-%m-%d').iso8601 + } + end + + def build_initial_stat + { + 'sessionsCount' => 0, + 'totalTime' => 0, + 'longestSession' => 0, + 'browsers' => [], + 'usedIE' => false, + 'alwaysUsedChrome' => true, + 'dates' => [] + } + end + + def parse_user(fields) + { + 'id' => fields[1], + 'first_name' => fields[2], + 'last_name' => fields[3], + 'age' => fields[4], + 'key' => "#{fields[2]} #{fields[3]}" + } + end + + def parse_session(fields) + { + 'user_id' => fields[1], + 'session_id' => fields[2], + 'browser' => fields[3].upcase, + 'time' => fields[4].to_i, + 'date' => fields[5], + } + end +end diff --git a/measurer.rb b/measurer.rb new file mode 100644 index 00000000..514d6053 --- /dev/null +++ b/measurer.rb @@ -0,0 +1,36 @@ +require_relative 'main' + +class Measurer + MEMORY_USAGE_LIMIT_MB = 70 + CHECK_USAGE_INTERVAL_SEC = 1 + + def initialize(count_lines: nil) + @count_lines = count_lines + end + + def call = [memory_measurer_thread, work_thread].map(&:join) + + private + + attr_reader :count_lines + + def work_thread = @work_thread ||= Thread.new { Main.new(options: { count_lines: }).call } + + def memory_measurer_thread = Thread.new do + while work_thread.alive? + sleep CHECK_USAGE_INTERVAL_SEC + current_memory_usage_mb = `ps -o rss= -p #{Process.pid}`.to_i / 1024 + if current_memory_usage_mb > MEMORY_USAGE_LIMIT_MB + puts "MEMORY USAGE: #{current_memory_usage_mb} MB, BUT LIMIT: #{MEMORY_USAGE_LIMIT_MB}. TERMINATING." + exit + else + puts "MEMORY USAGE: #{current_memory_usage_mb} MB" + end + end + end +end + +# Zero iteration: +# 15_000 lines: 57 MB (optimizing users and sessions collecting) +# First iteration: +# full: 38 MB (10 sec) (optimizing file stream processing) \ No newline at end of file diff --git a/profiler.rb b/profiler.rb new file mode 100644 index 00000000..0099c34c --- /dev/null +++ b/profiler.rb @@ -0,0 +1,49 @@ +require 'memory_profiler' +require 'stackprof' +require 'ruby-prof' + +require_relative 'main' + +class Profiler + def initialize(count_lines:) + @count_lines = count_lines + end + + def call + profile_by_memory_profiler + profile_memory_by_ruby_prof + profile_allocations_by_ruby_prof + profile_allocations_by_stack_prof + end + + private + + attr_reader :count_lines + + def profile_by_memory_profiler + report = MemoryProfiler.report { action } + report.pretty_print(to_file: './profiles/memory_profiler') + end + + def profile_memory_by_ruby_prof + RubyProf.measure_mode = RubyProf::MEMORY + result = RubyProf.profile { action } + printer = RubyProf::CallTreePrinter.new(result) + printer.print(:path => ".", :profile => "profiles/ruby_prof_memory_profile") + end + + def profile_allocations_by_ruby_prof + RubyProf.measure_mode = RubyProf::ALLOCATIONS + result = RubyProf.profile { action } + printer = RubyProf::CallTreePrinter.new(result) + printer.print(:path => ".", :profile => "profiles/ruby_prof_allocations_profile") + end + + def profile_allocations_by_stack_prof + StackProf.run(mode: :object, out: 'profiles/stackprof.dump') do + action + end + end + + def action = Main.new(options: { count_lines: }).call +end diff --git a/profiles/memory_profiler b/profiles/memory_profiler new file mode 100644 index 00000000..7e7cfdce --- /dev/null +++ b/profiles/memory_profiler @@ -0,0 +1,143 @@ +Total allocated: 19320 bytes (29 objects) +Total retained: 80 bytes (1 objects) + +allocated memory by gem +----------------------------------- + 19160 other + 160 set + +allocated memory by file +----------------------------------- + 18600 /home/nikita/rails_optimization/task_2/main.rb + 560 /home/nikita/rails_optimization/task_2/profiler.rb + 160 /usr/share/rvm/rubies/ruby-3.3.0/lib/ruby/3.3.0/set.rb + +allocated memory by location +----------------------------------- + 8680 /home/nikita/rails_optimization/task_2/main.rb:28 + 8560 /home/nikita/rails_optimization/task_2/main.rb:25 + 728 /home/nikita/rails_optimization/task_2/main.rb:18 + 560 /home/nikita/rails_optimization/task_2/profiler.rb:48 + 280 /home/nikita/rails_optimization/task_2/main.rb:46 + 160 /home/nikita/rails_optimization/task_2/main.rb:20 + 160 /usr/share/rvm/rubies/ruby-3.3.0/lib/ruby/3.3.0/set.rb:244 + 112 /home/nikita/rails_optimization/task_2/main.rb:26 + 40 /home/nikita/rails_optimization/task_2/main.rb:23 + 40 /home/nikita/rails_optimization/task_2/main.rb:5 + +allocated memory by class +----------------------------------- + 16880 File + 960 Hash + 720 String + 248 IO + 160 Enumerator + 80 Array + 80 Main + 80 Process::Status + 72 Thread::Mutex + 40 Set + +allocated objects by gem +----------------------------------- + 28 other + 1 set + +allocated objects by file +----------------------------------- + 24 /home/nikita/rails_optimization/task_2/main.rb + 4 /home/nikita/rails_optimization/task_2/profiler.rb + 1 /usr/share/rvm/rubies/ruby-3.3.0/lib/ruby/3.3.0/set.rb + +allocated objects by location +----------------------------------- + 7 /home/nikita/rails_optimization/task_2/main.rb:18 + 4 /home/nikita/rails_optimization/task_2/main.rb:25 + 4 /home/nikita/rails_optimization/task_2/main.rb:28 + 4 /home/nikita/rails_optimization/task_2/main.rb:46 + 4 /home/nikita/rails_optimization/task_2/profiler.rb:48 + 2 /home/nikita/rails_optimization/task_2/main.rb:26 + 1 /home/nikita/rails_optimization/task_2/main.rb:20 + 1 /home/nikita/rails_optimization/task_2/main.rb:23 + 1 /home/nikita/rails_optimization/task_2/main.rb:5 + 1 /usr/share/rvm/rubies/ruby-3.3.0/lib/ruby/3.3.0/set.rb:244 + +allocated objects by class +----------------------------------- + 13 String + 6 Hash + 2 Array + 2 File + 1 Enumerator + 1 IO + 1 Main + 1 Process::Status + 1 Set + 1 Thread::Mutex + +retained memory by gem +----------------------------------- + 80 other + +retained memory by file +----------------------------------- + 80 /home/nikita/rails_optimization/task_2/main.rb + +retained memory by location +----------------------------------- + 80 /home/nikita/rails_optimization/task_2/main.rb:18 + +retained memory by class +----------------------------------- + 80 Process::Status + +retained objects by gem +----------------------------------- + 1 other + +retained objects by file +----------------------------------- + 1 /home/nikita/rails_optimization/task_2/main.rb + +retained objects by location +----------------------------------- + 1 /home/nikita/rails_optimization/task_2/main.rb:18 + +retained objects by class +----------------------------------- + 1 Process::Status + + +Allocated String Report +----------------------------------- + 2 "" + 1 /home/nikita/rails_optimization/task_2/main.rb:18 + 1 /home/nikita/rails_optimization/task_2/main.rb:46 + + 2 "head -n 50000 data_large.txt > dataN.txt" + 2 /home/nikita/rails_optimization/task_2/main.rb:18 + + 2 "result.json" + 2 /home/nikita/rails_optimization/task_2/main.rb:25 + + 1 "," + 1 /home/nikita/rails_optimization/task_2/main.rb:46 + + 1 "50000" + 1 /home/nikita/rails_optimization/task_2/main.rb:18 + + 1 "dataN.txt" + 1 /home/nikita/rails_optimization/task_2/main.rb:28 + + 1 "data_large.txt" + 1 /home/nikita/rails_optimization/task_2/main.rb:5 + + 1 "w" + 1 /home/nikita/rails_optimization/task_2/main.rb:25 + + 1 "{\"usersStats\":{" + 1 /home/nikita/rails_optimization/task_2/main.rb:26 + + 1 "},\"totalUsers\":0,\"uniqueBrowsersCount\":0,\"totalSessions\":0,\"allBrowsers\":\"\"}\n" + 1 /home/nikita/rails_optimization/task_2/main.rb:46 + diff --git a/profiles/stackprof.dump b/profiles/stackprof.dump new file mode 100644 index 00000000..3a3afcfa Binary files /dev/null and b/profiles/stackprof.dump differ diff --git a/spec/main_spec.rb b/spec/main_spec.rb new file mode 100644 index 00000000..92c6f6c3 --- /dev/null +++ b/spec/main_spec.rb @@ -0,0 +1,33 @@ +require_relative "spec_helper" + +describe Main do + before do + 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 + + it "записывает в файл корректный результат" do + Main.new(options: { source_file_name: "data.txt" }).call + expected_result = '{"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"}' + "\n" + expect(File.read('result.json')).to eq(expected_result) + end +end diff --git a/spec/performance_spec.rb b/spec/performance_spec.rb new file mode 100644 index 00000000..36d7d44e --- /dev/null +++ b/spec/performance_spec.rb @@ -0,0 +1,8 @@ +require_relative "spec_helper" + +describe Main do + it "использование памяти не превышает заданный лимит" do + described_class.new.call + expect((`ps -o rss= -p #{Process.pid}`.to_i / 1024)).to be < 70 + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..8dad3d3b --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,105 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + +require 'json' +require 'byebug' +require 'date' +require_relative "../user" +require_relative "../main" + +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/task-2.rb b/task-2.rb index 34e09a3c..dff0b927 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,177 +1,10 @@ # Deoptimized version of homework task require 'json' -require 'pry' +require 'byebug' require 'date' -require 'minitest/autorun' - -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) -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 +require 'benchmark' +require_relative 'user' +require_relative 'measurer' +require_relative 'profiler' +require_relative 'main' diff --git a/user.rb b/user.rb new file mode 100644 index 00000000..c0a90d89 --- /dev/null +++ b/user.rb @@ -0,0 +1,8 @@ +class User + attr_reader :attributes, :sessions + + def initialize(attributes:, sessions:) + @attributes = attributes + @sessions = sessions + end +end