diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a44f64c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/profiling/results/* +/samples/data_* +/test/data.txt +/test/result.json diff --git a/case-study-2.md b/case-study-2.md new file mode 100644 index 0000000..e21eae8 --- /dev/null +++ b/case-study-2.md @@ -0,0 +1,124 @@ +# Case-study оптимизация. Часть 2. + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: объем затрачиваемой памяти и время выполнения + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. + +## Асимптотика +До оптимизации: +``` +Calculating ------------------------------------- + File: 512Kb 4.817 (± 5.3%) i/s - 24.000 in 5.032638s + File: 1Mb 2.527 (± 6.3%) i/s - 13.000 in 5.183298s + File: 2Mb 1.203 (±15.4%) i/s - 6.000 in 5.132240s + File: 4Mb 0.586 (±19.5%) i/s - 3.000 in 5.269748s + File: 8Mb 0.287 (±14.0%) i/s - 2.000 in 7.100129s + with 99.0% confidence + +Comparison: + File: 512Kb: 4.8 i/s + File: 1Mb: 2.5 i/s - 1.91x (± 0.15) slower + File: 2Mb: 1.2 i/s - 4.00x (± 0.68) slower + File: 4Mb: 0.6 i/s - 8.18x (± 1.96) slower + File: 8Mb: 0.3 i/s - 16.77x (± 3.11) slower + +``` +Исходя из данных показателей мы видим, что увеличение объема обрабатываемых данных в 2 раза ведет к росту времени работы в 2 раза + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений в среднем за 10 секунд + +Вот как я построил `feedback_loop`: +1. Изначально выбрал маленький размер исходных данных (около 1Мб) позволяющий скрипту успешно отработать без оптимизаций +2. Поиск базовой метрики (время и память) +3. Дописал тест на регрессию по времени и памяти +4. Профилирование и поиск "точек роста" +5. Внесение изменений в код +6. Повторное тестирование и сбор новых метрик +7. Увеличение объема данных + +## Вникаем в детали системы, чтобы найти 20% точек роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался + +Гемы: +* ruby-prof +* memory_profiler +* get_process_mem +* benchmark-ips +Stdlib: +* benchmark +C +* valgrind (tool - massif) + +Вот какие проблемы удалось найти и решить +Согласно результатам ruby-prof в режиме cpu: +``` + %self total self wait child calls name + 23.99 9.674 2.918 0.000 6.756 1 IO#each_line + 6.89 1.007 0.838 0.000 0.169 48639 Array#map + 6.74 1.413 0.819 0.000 0.594 89009 #iso8601 + 6.71 2.488 0.816 0.000 1.672 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json + 5.94 2.302 0.722 0.000 1.581 89009 Object#parse_session + 5.64 3.431 0.685 0.000 2.746 16214 Object#aggregate_user_stats +``` +### Находка №1 +Проанализировав исходные данные выяснилось, что даты изначально приходят в нужном формате, поэтому нет нужды создавать Date объект +Также были убраны неиспользуемые поля, а вместо полей first_name и last_name было сделано поле full_name. +``` +Comparison: + File: 512Kb: 10.9 i/s + File: 1Mb: 5.9 i/s - 1.85x (± 0.14) slower + File: 2Mb: 2.8 i/s - 3.94x (± 0.57) slower + File: 4Mb: 1.4 i/s - 7.90x (± 1.57) slower + File: 8Mb: 0.6 i/s - 16.93x (± 5.53) slower + with 99.0% confidence +``` +Фактически мы видим двукратный прирост производительности + +### Находка №2 +Также стандартная json библиотека не самая производительная заменив ее на gem oj, мы получили еще небольшой прирост: +``` +Comparison: + File: 512Kb: 12.5 i/s + File: 1Mb: 7.0 i/s - 1.78x (± 0.12) slower + File: 2Mb: 3.4 i/s - 3.68x (± 0.42) slower + File: 4Mb: 1.6 i/s - 7.71x (± 1.27) slower + File: 8Mb: 0.8 i/s - 15.36x (± 3.33) slower + with 99.0% confidence +``` + +Однако нерешенной остается проблема резкого выделения [памяти](https://imgur.com/cvkmvMS) связанная с формирование json строки + +### Находка №3 +Вместо библиотеки, которая целиком парсит hash в json, можно использовать yajl, которая позволяет "стримить" json в файл +Мы немного проигрываем в производительности на маленьких файлах +``` +Comparison: + File: 512Kb: 12.3 i/s + File: 1Mb: 6.8 i/s - 1.81x (± 0.13) slower + File: 2Mb: 3.3 i/s - 3.67x (± 0.33) slower + File: 4Mb: 1.6 i/s - 7.56x (± 1.22) slower + File: 8Mb: 0.8 i/s - 16.21x (± 5.33) slower + with 99.0% confidence +``` +Однако мы существенно сокращаем объемы потребляемой ![памяти](https://imgur.com/pzdL2kW) + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +В среднем файл обрабатывается за 28 - 29 секунд при средних затратах по памяти 600 - 620Мб без резких скачков по памяти + +## Защита от регресса производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавлены дополнительные тесты для защиты от регрессий по памяти и времени выполнения diff --git a/case-study-template.md b/case-study-template.md index e0eef00..30121f0 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,35 +12,74 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: объем затрачиваемой памяти и время выполнения ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений в среднем за 10 секунд -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +Вот как я построил `feedback_loop`: +1. Изначально выбрал маленький размер исходных данных (около 1Мб) позволяющий скрипту успешно отработать без оптимизаций +2. Поиск базовой метрики (время и память) +3. Дописал тест на регрессию по времени и памяти +4. Профилирование и поиск "точек роста" +5. Внесение изменений в код +6. Повторное тестирование и сбор новых метрик +7. Увеличение объема данных ## Вникаем в детали системы, чтобы найти 20% точек роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался + +Гемы: +* ruby-prof +* memory_profiler +* get_process_mem + +Stdlib: +* benchmark Вот какие проблемы удалось найти и решить -### Ваша находка №1 -О вашей находке №1 +### Находка №1 +Считывание всего файла в строку с дальнейшим разбиением на массив строк крайне неэффективно и было одним из основным блокеров для работы с большими файлами + +Использование построчной обработки существенно ускорило выполнение программы примерно в 2,3 раза + +### Находка №2 +Memory_profiler показал, что аллоцируется огромное количество строк, хоть большинство из них собираются GC, однако все равно увеличивают объем потребляемой памяти и время выполнения (как минимум за счет работы GC) + +Для исправления этой проблемы был использован ```# frozen_string_literal: true``` и в качестве ключей стали использоваться Symbol + +### Находка №3 +Избыточное и неоптимальное использование итераторов + +По возможности, обработка данных переписана так, чтобы использовать минимальное число итераций и использоание "in-place" модификаций чтобы снизить расходы по памяти -### Ваша находка №2 -О вашей находке №2 +### Находка №4 +Генерация "сущностей без надобности" -### Ваша находка №X -О вашей находке №X +Был убран класс User, а так же вместо отдельных массивов users и sessions был использован хеш ключем, которого стал user_id, что упростило связывание пользователей и сессий + +### Находка №5 +Неоптимальная аггрегация данных + +Вместо того, чтобы агрегировать статистику по завершении обработки файла, код был переписан так, чтобы статистика аггрегировалась "по ходу" обработки файла, также для снижения потребляемой памяти, посчитанные данные удалялись + +### Находка №6 +Использование "медленных" методов без особой надобности + +Ruby-prof показал, что метод Date#parse занимает порядка 8% от всего времени выполнения, поэтому он был заменен на Date#strptime + +### Находка №7 +Использование регулярных выражений без особой надобности + +Поиск по регулярным выражениям был заменен на поиск по вхождению подстроки, поскольку он более производительный ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* - -*Какими ещё результами можете поделиться* +В среднем файл обрабатывается за 29 - 30 секунд при средних затратах по памяти 850 - 860Мб ## Защита от регресса производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавлены дополнительные тесты для защиты от регрессий по памяти и времени выполнения diff --git a/data_large.txt.gz b/data_large.txt.gz deleted file mode 100644 index 823c793..0000000 Binary files a/data_large.txt.gz and /dev/null differ diff --git a/lib/task-1.rb b/lib/task-1.rb new file mode 100644 index 0000000..2fd709b --- /dev/null +++ b/lib/task-1.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'yajl' +#require 'ruby-progressbar' + +def parse_user(fields) + { + id: fields[1], + full_name: fields[2] << " " << fields[3] + } +end + +def parse_session(fields) + { + user_id: fields[1], + browser: fields[3].upcase!, + time: fields[4].to_i, + date: fields[5].chomp!, + } +end + +def aggregate_user_stats(data) + return {} unless data + + user = data[1][:user] + sessions = data[1][:sessions] + time = sessions.map {|s| s[:time] } + browsers = sessions.map { |s| s[:browser] } + { + user[:full_name]=> { + sessionsCount: sessions.count, + totalTime: time.sum.to_s << ' min.', + longestSession: time.max.to_s << ' min.', + browsers: browsers.sort!.join(', '), + usedIE: browsers.any? { |b| b.include?('INTERNET EXPLORER') }, + alwaysUsedChrome: browsers.all? { |b| b.include?('CHROME')}, + dates: sessions.map{|s| s[:date]}.sort!.reverse! + } + } +end + +def work(input: "data.txt", output: "result.json") + # Отчёт в json + # - Сколько всего юзеров + + # - Сколько всего уникальных браузеров + + # - Сколько всего сессий + + # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + + # + # - По каждому пользователю + # - сколько всего сессий + + # - сколько всего времени + + # - самая длинная сессия + + # - браузеры через запятую + + # - Хоть раз использовал IE? + + # - Всегда использовал только Хром? + + # - даты сессий в порядке убывания через запятую + + + data = {} + _tmp_hash = {} + report = { + totalUsers: 0, + uniqueBrowsersCount: 0, + totalSessions: 0, + allBrowsers: [], + usersStats: {} + } + + #bar_output = ENV["NOPROGRESS"] ? File.open(File::NULL, "w") : $stdout + #bar = ProgressBar.create(total: nil, output: bar_output) + File.open(input) do |f| + f.each_line do |line| + cols = line.split(',') + key = cols[1] + if cols[0] == "user" + if data[key].nil? + report[:usersStats].merge!(aggregate_user_stats(data.shift)) + end + data[key] ||= {} + data[key][:user] = parse_user(cols) + report[:totalUsers] += 1 + else + data[key] ||= {} + data[key][:sessions] ||= [] + session = parse_session(cols) + browser = session[:browser] + if _tmp_hash[browser].nil? + _tmp_hash[browser] = 1 + report[:allBrowsers].push(browser) + report[:uniqueBrowsersCount] += 1 + end + report[:totalSessions] += 1 + data[key][:sessions].push(session) + end + # bar.increment + end + report[:usersStats].merge!(aggregate_user_stats(data.shift)) + report[:allBrowsers] = report[:allBrowsers].sort!.join(',') + # bar.finish + end + File.open(output, 'w') do |f| + Yajl::Encoder.encode(report, f) + f << "\n" + end +end diff --git a/profiling/ips.rb b/profiling/ips.rb new file mode 100644 index 0000000..cce79cd --- /dev/null +++ b/profiling/ips.rb @@ -0,0 +1,46 @@ +ENV["NOPROGRESS"] = '1' +require_relative '../lib/task-1.rb' +require 'benchmark/ips' + +FILES = { + "512Kb" => 524_268, + "1Mb" => 1_048_576, + "2Mb" => 2_097_152, + "4Mb" => 4_194_304, + "8Mb" => 8_388_608, +}.freeze + +class GCSuite + def warming(*) + run_gc + end + + def running(*) + run_gc + end + + def warmup_stats(*) + end + + def add_report(*) + end + + private + + def run_gc + GC.enable + GC.start + GC.disable + end +end + + +suite = GCSuite.new + +Benchmark.ips do |x| + x.config(suite: suite, stats: :bootstrap, confidence: 99) + FILES.keys.each do |size| + x.report("File: #{size}") { work(input: "../samples/data_#{size}", output: "/dev/null") } + end + x.compare! +end diff --git a/profiling/ruby-prof.rb b/profiling/ruby-prof.rb new file mode 100644 index 0000000..3ba9b71 --- /dev/null +++ b/profiling/ruby-prof.rb @@ -0,0 +1,14 @@ +ENV["NOPROGRESS"] = '1' + +require_relative '../lib/task-1.rb' +require 'ruby-prof' + +RubyProf.measure_mode = RubyProf::CPU_TIME +result = RubyProf.profile do + work(input: "../samples/data_4Mb", output: "/dev/null") +end + +printer = RubyProf::MultiPrinter.new(result) +printer2 = RubyProf::CallTreePrinter.new(result) +printer.print(path: "./results", profile: "task-1") +printer2.print(path: "./results", profile: "task-1_calltree") diff --git a/profiling/stack-prof.rb b/profiling/stack-prof.rb new file mode 100644 index 0000000..9afe482 --- /dev/null +++ b/profiling/stack-prof.rb @@ -0,0 +1,10 @@ +ENV["NOPROGRESS"] = '1' + +require_relative '../lib/task-1.rb' +require 'stackprof' + +mode = ENV["MODE"] || :wall + +StackProf.run(mode: mode.to_sym, out: "./results/stackprof.dump") do + work(input: "../samples/data_4Mb", output: "/dev/null") +end diff --git a/samples/create_samples.rb b/samples/create_samples.rb new file mode 100644 index 0000000..45f06c1 --- /dev/null +++ b/samples/create_samples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +FILES = { + "512Kb" => 524_268, + "1Mb" => 1_048_576, + "2Mb" => 2_097_152, + "4Mb" => 4_194_304, + "8Mb" => 8_388_608, +}.freeze + +FILES.each do |name, size| + copied = 0 + source = File.open("data_large.txt") + target = File.open("data_#{name}", "w+") + while (line = source.gets) + break if copied >= size + target.puts line + copied += line.size + end +ensure + source.close + target.close +end diff --git a/task-1.rb b/task-1.rb deleted file mode 100644 index 778672d..0000000 --- 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/test/task-1_test.rb b/test/task-1_test.rb new file mode 100644 index 0000000..f2fc0a2 --- /dev/null +++ b/test/task-1_test.rb @@ -0,0 +1,49 @@ +require 'minitest/autorun' +require 'benchmark' +require 'get_process_mem' + +require_relative '../lib/task-1.rb' + +ENV["NOPROGRESS"] = '1' +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 + + def test_time + time = Benchmark.realtime { work(input: '../samples/data_2Mb', output: '/dev/null') } + assert(time.round(2) < 1) + end + + def test_memory + work(input: '../samples/data_2Mb', output: '/dev/null') + mem_after = GetProcessMem.new + assert(mem_after.mb.round(2) < 55) + end +end diff --git a/work.rb b/work.rb new file mode 100644 index 0000000..372925c --- /dev/null +++ b/work.rb @@ -0,0 +1,2 @@ +require_relative "./lib/task-1.rb" +work(input: './samples/data_large.txt')