diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..98bcccb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +reports/* +result.json +fixtures/* + diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..04b75a57 --- /dev/null +++ b/Gemfile @@ -0,0 +1,11 @@ +ruby '3.3.4' + +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + + +gem 'pry' +gem 'rspec-benchmark' +gem 'ruby-progressbar' +gem 'ruby-prof' +gem 'stackprof' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..fb2e1ef5 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,50 @@ +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) + method_source (1.1.0) + pry (0.15.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.2) + 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) + ruby-progressbar (1.13.0) + stackprof (0.2.27) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + pry + rspec-benchmark + ruby-prof + ruby-progressbar + stackprof + +RUBY VERSION + ruby 3.3.4p94 + +BUNDLED WITH + 2.5.23 diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..3e7ea1a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +run: + ruby bin/runner + +unzip: + gzip -dk fixtures/data_large.txt.gz + +prepare_data: + head -n 1000 fixtures/data_large.txt > fixtures/data1000.txt + head -n 2000 fixtures/data_large.txt > fixtures/data2000.txt + head -n 4000 fixtures/data_large.txt > fixtures/data4000.txt + head -n 8000 fixtures/data_large.txt > fixtures/data8000.txt + head -n 100000 fixtures/data_large.txt > fixtures/data100000.txt + head -n 200000 fixtures/data_large.txt > fixtures/data200000.txt + +test: + ENVIRONMENT=test ruby tests/task-1_test.rb + +perform_test: + ENVIRONMENT=test rspec tests/task_perform_spec.rb + +all_tests: + make test + make perform_test + +simple_benchmark: + ENVIRONMENT=test ruby lib/benchmarks/simple_benchmarks.rb + +simple_benchmark_gb_dis: + ruby lib/benchmarks/simple_benchmarks.rb true + +benchmarks_ips: + ruby lib/benchmarks/benchmarks_ips.rb + +benchmarks_ips_gb_dis: + ruby lib/benchmarks/benchmarks_ips.rb true + +report: + ruby lib/reporters/prof_reporter.rb $(T) + +all_reports: + make report T='flat' + make report T='graph' + make report T='callstack' + make report T='callgrind' + make report T='stack-prof-cli' + make report T='stack-prof-json' + +.PHONY: test diff --git a/bin/runner b/bin/runner new file mode 100755 index 00000000..996a11a0 --- /dev/null +++ b/bin/runner @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby + +require_relative "../lib/task-1" + +work('fixtures/data1000.txt') \ No newline at end of file diff --git a/case-study-template.md b/case-study-template.md index d41034d9..3451b427 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,15 +12,69 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: +- обрабатывать 200_000 строк не больше чем за 1,5 сек, если я уложусь в это время, то обработка всего файла уложится в бюджет + +Асимптотика перед рефакторингом: +``` bash +"Processing time from file 1000 rows: 0.0177" +"Processing time from file 2000 rows: 0.0504" +"Processing time from file 4000 rows: 0.1516" +"Processing time from file 8000 rows: 0.504" +``` + +Асимптотика после первой итерации рефаторинга(формирование и группировка сессий по user_id): + +```bash +"Processing time from file 1000 rows: 0.0078" +"Processing time from file 2000 rows: 0.0185" +"Processing time from file 4000 rows: 0.0358" +"Processing time from file 8000 rows: 0.0685" +``` + +Асимптотика после второй итерации(создание пользователей сразу при обходе строк) + +```bash +"Processing time from file 1000 rows: 0.0084" +"Processing time from file 2000 rows: 0.0176" +"Processing time from file 4000 rows: 0.0372" +"Processing time from file 8000 rows: 0.0707" +``` + +Асимптотика после третьей итерации(оптимизация `collect_stat_from user`) + +```bash +"Processing time from file 1000 rows: 0.0102" +"Processing time from file 2000 rows: 0.0234" +"Processing time from file 4000 rows: 0.0592" +"Processing time from file 8000 rows: 0.0849" +"Processing time from file 100000 rows: 1.0904" +"Processing time from file 1000000 rows: 41.2784" // полный отчет +``` + +Асимптотика финальная - видно что она почти линейная +```bash +"Processing time from file 1000 rows: 0.007" +"Processing time from file 2000 rows: 0.012" +"Processing time from file 4000 rows: 0.0232" +"Processing time from file 8000 rows: 0.0452" +"Processing time from file 100000 rows: 0.5962" +"Processing time from file 1000000 rows: 29.8777" +``` ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 5 минут(внес изменеия -> запустил бенчмарк и профилировщик-> посмотрел результат) -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +Вот как я построил `feedback_loop`: +1. Создал Makefile с короткими алиасами для быстрого запуска тестов отчетов и бенчмарков +2. Запуск бенчмарков и профилировщика +3. Поиск точки роста +4. Внисение изменений +5. Проверка гипотизы +6. На каждой тиерации увеличивать данные на вход ## Вникаем в детали системы, чтобы найти главные точки роста Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* @@ -28,29 +82,44 @@ Вот какие проблемы удалось найти и решить ### Ваша находка №1 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? +1. Отчет graph html показал что главная точка роста является вызов `Array#select` программа проводит там 24% времени +2. Недостаток текущей реализации был в том что программа на каждом пользователи фильтровала массив обектов сессий + Решение - группировать сессии пользователя по `user_id` при парсинге файла и потом достовать их уже по ключу +3. После этого при входящих данных в 8000 строк время работы уменьшилось с 0.504 до 0.0685, то есть программа ускорилась ~7 раз +4. После этого исправленная проблемма перестала быть точкой роста + ### Ваша находка №2 +1. Отчет callstack.htm показал что теперь основное время программа находится в `Array#each` вызовы которой происходит в нескольких местах: обход строк файла, обход пользователей для формирования объектов пользователей. Так как обход строк файла нам необходим +2. Решил оптимизировать обход объектов пользователей - не делать повторный обход, а создавть пользователя сразу при обходе строк +3. Прирост метрики по бенчмаркам был незначительный 5% +4. По отчету точка роста изменилась - теперь программа большую часть времени стала проводить в методе `collect_stats_from_users` 34% + Но я приблизился к бюджету - после оптимизации этой точки роста сборка отчета всех данных занимает 44 сек. + +### Ваша находка №3 +1. Отчет callstack.htm показал что теперь основное время программа находится в `collect_stats_from_users` +2. решил повторно не обходить массив обектов пользователя, а собирать по ним данные сразу во время парсинга +3. Время полног отчета хоть и уменьшилось до 41 сек, но большого прироста я не получил, по этому сделал откат назат - какой отчёт показал главную точку роста - как вы решили её оптимизировать - как изменилась метрика - как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? +### Ваша находка №4 +1. Еще раз сделал отчет callstack.htm с выключенным gc - отчет показа что есть точка рост в методе который собирает сессии пользователей, а также на инкрементирование прогресс бара +2. Оптимизация заключалась в следующем - добавлять сессии сразу в обект пользователя при обходе строк файла и отключить прогресс-бар при бенчмарках +3. Получил ощютимый прирост - формирование полного за 29.8777 сек. +4. После этого исправленная проблемма перестала быть точкой роста и я вышел на целевой бюджет - время формирования отчета за 30 сек + ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. +Удалось улучшить метрику системы с *9,36 до 1.3274* и уложиться в заданный бюджет(полный отчет формируется за 29.8777). *Какими ещё результами можете поделиться* +1. Пробовал запускать коллектинг данных по пользователям в несколько тредов(на кажый вызов `collect_stats_from_users(report, users_objects) do |user|` отдельный тред) - в моем случае это привело к регресу производительности с 37 сек. до 60 +2. метод `size` работает гораздо быстрее чем `count` - удалось выйграть на большом колличестве данных 3 сек. ## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написал perfomance тест, который запускает метод `work` 10 раз и проверяет на 200_000 строках что он исполняется не более 1,5 сек. 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/data_large.txt.gz b/data_large.txt.gz deleted file mode 100644 index 823c793a..00000000 Binary files a/data_large.txt.gz and /dev/null differ diff --git a/lib/benchmarks/benchmarks_ips.rb b/lib/benchmarks/benchmarks_ips.rb new file mode 100644 index 00000000..1deccd81 --- /dev/null +++ b/lib/benchmarks/benchmarks_ips.rb @@ -0,0 +1,32 @@ +require 'benchmark/ips' +require_relative '../../lib/task-1' +require_relative '../utils/artifact_cleaner' + +gb_disable = ARGV[0] + +config = { stats: :bootstrap, confidence: 95} + + + +Benchmark.ips do |x| + + x.config(**config) + + x.report('1000 rows') do + work('fixtures/data1000.txt', gb_disable) + end + + x.report('2000 rows') do + work('fixtures/data2000.txt', gb_disable) + end + + x.report('4000 rows') do + work('fixtures/data4000.txt', gb_disable) + end + + x.report('8000 rows') do + work('fixtures/data8000.txt', gb_disable) + end +end + +ArtifactCleaner.clean('result.json') \ No newline at end of file diff --git a/lib/benchmarks/simple_benchmarks.rb b/lib/benchmarks/simple_benchmarks.rb new file mode 100644 index 00000000..12d67376 --- /dev/null +++ b/lib/benchmarks/simple_benchmarks.rb @@ -0,0 +1,48 @@ +require 'benchmark' +require_relative '../../lib/task-1' +require_relative '../utils/artifact_cleaner' + +gb_disable = ARGV[0] + + +time_1000rows = Benchmark.realtime do + work('fixtures/data1000.txt', gb_disable) +end + +time_2000rows = Benchmark.realtime do + work('fixtures/data2000.txt', gb_disable) +end + +time_4000rows = Benchmark.realtime do + work('fixtures/data4000.txt', gb_disable) +end + +time_8000rows = Benchmark.realtime do + work('fixtures/data8000.txt', gb_disable) +end + +time_100000rows = Benchmark.realtime do + work('fixtures/data100000.txt', gb_disable) +end + +time_200000rows = Benchmark.realtime do + work('fixtures/data200000.txt', gb_disable) +end + +time_large = Benchmark.realtime do + work('fixtures/data_large.txt', gb_disable) +end + +def printer(time, rows = 1000) + pp "Processing time from file #{rows} rows: #{time.round(4)}" +end + +printer(time_1000rows) +printer(time_2000rows, 2000) +printer(time_4000rows, 4000) +printer(time_8000rows, 8000) +printer(time_100000rows, 100_000) +printer(time_200000rows, 200_000) +printer(time_large, 1_000_000) + +ArtifactCleaner.clean('result.json') \ No newline at end of file diff --git a/lib/reporters/prof_reporter.rb b/lib/reporters/prof_reporter.rb new file mode 100644 index 00000000..9febf9cf --- /dev/null +++ b/lib/reporters/prof_reporter.rb @@ -0,0 +1,44 @@ +require 'ruby-prof' +require 'stackprof' +require 'json' +require_relative '../task-1' + +format = ARGV[0] + + + +def build_format(format) + RubyProf.measure_mode = RubyProf::WALL_TIME + + result = RubyProf.profile do + work('fixtures/data100000.txt', true) + end + + case format + when 'flat' + RubyProf::FlatPrinter.new(result).print(File.open('reports/flat.txt', 'w+')) + when 'graph' + RubyProf::GraphHtmlPrinter.new(result).print(File.open('reports/graph.html', 'w+')) + when 'callstack' + RubyProf::CallStackPrinter.new(result).print(File.open('reports/callstack.html', 'w+')) + when 'callgrind' + # qcachegrind reports/ + RubyProf::CallTreePrinter.new(result).print(:path => 'reports', :profile => 'callgrind') + when 'stack-prof-cli' + # stackprof reports/stackprof.dump + # stackprof reports/stackprof.dump --method Object#work + StackProf.run(mode: :wall, out: 'reports/stackprof.dump', interval: 1000) do + work('fixtures/data100000.txt', true) + end + when 'stack-prof-json' + profile = StackProf.run(mode: :wall, raw: true) do + work('fixtures/data100000.txt', true) + end + File.write('reports/stackprof.json', JSON.generate(profile)) + else + puts("Unknow format #{format}") + end +end + + +build_format(format) diff --git a/lib/task-1.rb b/lib/task-1.rb new file mode 100644 index 00000000..64bec970 --- /dev/null +++ b/lib/task-1.rb @@ -0,0 +1,151 @@ +# Deoptimized version of homework task + +require 'json' +require 'pry' +require 'date' +require_relative 'utils/progress_bar_factory' +require_relative 'user' + + + +def parse_user(user) + _, id, first_name, last_name, age = user.split(',') + full_name = "#{first_name}" + ' ' + "#{last_name}" + { + id:, + first_name:, + last_name:, + full_name:, + age:, + } +end + +def parse_session_by_user_id(session) + _, user_id, session_id, browser, time, date = session.split(',') + [ + user_id, + browser, + { + user_id:, + session_id:, + browser:, + time:, + date:, + } + ] +end + +def collect_stats_from_users(report, users_objects, &block) + users_objects.each do |user| + user_key = user.full_name + report['usersStats'][user_key] ||= {} + report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) + end +end + +def work(path = 'fixtures/data.txt', gb_disable = false) + GC.disable if gb_disable + file_lines = File.read(path).split("\n") + file_lines_count = file_lines.count + + users_storage = {} + unique_browsers = Set[] + total_session = 0 + + + user_attributes = {} + i = 0 + while i < file_lines_count + line = file_lines[i] + cols = line.split(',') + if cols[0] == 'user' + user_attributes = parse_user(line) + + user = User.new(**user_attributes, sessions:[]) || users_storage[user_attributes[:id]] + users_storage[user_attributes[:id]] = user + end + if cols[0] == 'session' + user_id, browser, session_data = parse_session_by_user_id(line) + # Подсчёт количества уникальных браузеров + unique_browsers.add(browser.upcase) + user = users_storage[user_id] + if user + time = session_data[:time].to_i + user.add_sessions(session_data) + user.inc + user.update_total_time(time) + user.use_ie = !(session_data[:browser] =~ /Internet Explorer/).nil? unless user.use_ie + user.update_longest_session(time) + user.add_dates(session_data[:date]) + end + total_session += 1 + end + i += 1 + end + + users_objects = users_storage.values + # Отчёт в json + # - Сколько всего юзеров + + # - Сколько всего уникальных браузеров + + # - Сколько всего сессий + + # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + + # + # - По каждому пользователю + # - сколько всего сессий + + # - сколько всего времени + + # - самая длинная сессия + + # - браузеры через запятую + + # - Хоть раз использовал IE? + + # - Всегда использовал только Хром? + + # - даты сессий в порядке убывания через запятую + + + report = {} + + report['totalUsers'] = users_storage.keys.count + + report['uniqueBrowsersCount'] = unique_browsers.size + + report['totalSessions'] = total_session + + report['allBrowsers'] = unique_browsers.sort.join(',') + + 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.total_time.to_s + ' min.' } + end + + # Выбираем самую длинную сессию пользователя + collect_stats_from_users(report, users_objects) do |user| + { 'longestSession' => user.longest_session.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.use_ie } + 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.dates.sort.reverse } + end + + File.write('result.json', "#{report.to_json}\n") +end \ No newline at end of file diff --git a/lib/user.rb b/lib/user.rb new file mode 100644 index 00000000..b1836dbf --- /dev/null +++ b/lib/user.rb @@ -0,0 +1,39 @@ +class User + attr_accessor :id, :first_name, :last_name, + :full_name, :age, :sessions, + :sessions_count, :total_time, :use_ie, :longest_session, :dates + + def initialize(id:, first_name:, last_name:, full_name:, age:, sessions:[]) + @id = id + @first_name = first_name + @last_name = last_name + @full_name = full_name + @age = age + @sessions = sessions + @sessions_count = 0 + @total_time = 0 + @use_ie = false + @longest_session = 0 + @dates = [] + end + + def add_sessions(session) + self.sessions << session + end + + def inc + self.sessions_count += 1 + end + + def update_total_time(time) + self.total_time += time + end + + def update_longest_session(time) + self.longest_session = self.longest_session > time ? self.longest_session : time + end + + def add_dates(date) + self.dates << date + end +end \ No newline at end of file diff --git a/lib/utils/artifact_cleaner.rb b/lib/utils/artifact_cleaner.rb new file mode 100644 index 00000000..bb001877 --- /dev/null +++ b/lib/utils/artifact_cleaner.rb @@ -0,0 +1,7 @@ +class ArtifactCleaner + class << self + def clean(path) + File.delete(path) if File.exist?(path) + end + end +end \ No newline at end of file diff --git a/lib/utils/progress_bar_factory.rb b/lib/utils/progress_bar_factory.rb new file mode 100644 index 00000000..ef5ae31d --- /dev/null +++ b/lib/utils/progress_bar_factory.rb @@ -0,0 +1,26 @@ +require 'ruby-progressbar' + +class ProgressBarFactory + FORMAT= '%a, %J, %E %B'.freeze + attr_accessor :total, :format, :output + + def initialize(total, format = FORMAT) + @total = total + @format = format + @output = File.open(File::NULL, 'w') if ENV['ENVIRONMENT'] == 'test' + end + + def create + ProgressBar.create(**build_params) + end + + private + + def build_params + { + total:, + format:, + output: + }.compact + end +end \ No newline at end of file diff --git a/task-1.rb b/task-1.rb deleted file mode 100644 index 778672df..00000000 --- a/task-1.rb +++ /dev/null @@ -1,176 +0,0 @@ -# Deoptimized version of homework task - -require 'json' -require 'pry' -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") -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 = '{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}' + "\n" - assert_equal expected_result, File.read('result.json') - end -end diff --git a/tests/rspec_helper.rb b/tests/rspec_helper.rb new file mode 100644 index 00000000..a426dd03 --- /dev/null +++ b/tests/rspec_helper.rb @@ -0,0 +1,7 @@ +require 'rspec-benchmark' +require_relative "../lib/task-1" + + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end \ No newline at end of file diff --git a/tests/task-1_test.rb b/tests/task-1_test.rb new file mode 100644 index 00000000..d3c8e445 --- /dev/null +++ b/tests/task-1_test.rb @@ -0,0 +1,15 @@ +require 'minitest/autorun' +require_relative "../lib/task-1" + +class TestTask < Minitest::Test + def setup + File.write('result.json', '') + end + + def test_result + work + expected_result = '{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}' + "\n" + assert_equal expected_result, File.read('result.json') + File.delete('result.json') + end +end \ No newline at end of file diff --git a/tests/task_perform_spec.rb b/tests/task_perform_spec.rb new file mode 100644 index 00000000..6855eed4 --- /dev/null +++ b/tests/task_perform_spec.rb @@ -0,0 +1,12 @@ +require_relative 'rspec_helper' + +describe 'Performance reporter' do + let(:file_path) { 'fixtures/data200000.txt' } + let(:time) { 1 } + + it 'create report' do + expect { + work(file_path) + }.to perform_under(time).sec.warmup(2).times.sample(10).times + end +end