diff --git a/case-study-template.md b/case-study-template.md index c3279664..658183bb 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,44 +12,296 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: объем памяти использыуемый приложением на конец выполнения. И время выполнения для сравнения с оптимизациями по CPU ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 40 с (до оптимизации). Возможно время выполнения пока достаточно большое, но предыдущий опыт показал, что первые оптимизации довольно быстро уменьшают время, что в первой задаче привело к тому, что стало просто неочевидно - сказались изменения или нет. Либо я что-то делаю не так. -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +Вот как я построил `feedback_loop`: добавил work аргумент для передачи имени файла, чтобы замер метрики, профилирование и тесты можно было осуществлять одновременно и на разных пакетах данных. Сделал тестовый вариант данных объем - 16250 строк +Показатели: MEMORY USAGE: 170 MB Work time: 40.490744165999786. +Попробовал запустить метод с опросом по памяти в конце обработки каждой строки. Максимальное значение, что увидел, примерное такое же (169) +/- погрешность. Но до 10000 строк значение составляли до 80 МВ, и только после выросло выше 100. Либо данные так скомпонованы, либо что-то накапливается. В любом случае, на первом этапе этот размер данных полагаю подходящим. +Все измерения на ноуте с питанием от сети, CPU в режиме perfomance. +update: далее быстро выяснил, что запуск с профилировщиком и таким объемом данных это нерабочая схема. Для метрики можно использовать такой пакет, но для профилирования возьму 3250 строк. ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался: MemoryProfiler, StackProf, RubyProf(flat, graph, stack, callgrind) Вот какие проблемы удалось найти и решить ### Ваша находка №1 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +- MemoryProfiler. запустил профилирование на 16250 строках данных, посмотрел в htop как процесс съедает всю оперативку(MEMORY USAGE: 14667 MB), начинает swap. Причем 3 из 4 попытки пристрелила сама система. Вывод 1 - профилирование буду проводить на меньшем объеме. +Total allocated: 12.62 GB (5554306 objects) + +allocated memory by location +----------------------------------- + 12.21 GB /home/denio/thinknetika/rails-optimization-task2/task-2.rb:108 + 141.62 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb:104 + + allocated objects by location +----------------------------------- + 2758207 /home/denio/thinknetika/rails-optimization-task2/task-2.rb:104 + 1039633 /home/denio/thinknetika/rails-optimization-task2/task-2.rb:97 + + allocated objects by class +----------------------------------- + 4608799 String + 340624 Array + 320648 Hash + 177303 MatchData +---------------------------------------------------------------------------- + +Данные на 3250 строк (и отработало за 8 сек) +---------------------------------------------------------------------------- + +MEMORY USAGE: 736 MB +Total allocated: 608.00 MB (1072647 objects) +Total retained: 4.68 kB (9 objects) + +allocated memory by gem +----------------------------------- + 608.00 MB other + +allocated memory by file +----------------------------------- + 608.00 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb + +allocated memory by location +----------------------------------- + 529.28 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb:108 - это преобразование в json и сохранение в файле. + 26.36 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb:104 - добавление браузера к списку всех браузеров (предварительно преобразовывает строку в массив ) + + allocated memory by class +----------------------------------- + 542.83 MB String + 27.39 MB File + 18.76 MB Array + 10.69 MB Hash + + allocated objects by file +----------------------------------- + 1072646 /home/denio/thinknetika/rails-optimization-task2/task-2.rb + +allocated objects by location +----------------------------------- + 517087 /home/denio/thinknetika/rails-optimization-task2/task-2.rb:104 + 204294 /home/denio/thinknetika/rails-optimization-task2/task-2.rb:97 + +Чтобы стало еще более понятно, разделил строку File.write('result.json', "#{report.to_json}\n") на две +107 report_json = report.to_json +108 File.write('result.json', "#{report_json}\n") + +получил +allocated memory by location +----------------------------------- + 278.49 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb:108 + 250.79 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb:107 + +то есть здесь получается двойная генерация строк + +Проанализировав отчет, увидел, что report хоть и записывается на каждом круге, но остается полным в памяти и просто перезаписывает себя после парсера строки и сбора статистики. Это неправильное поведение. Если взять предельный случай, когда полные данные - это много уникальных пользователей, каждый из которых имеет одну сессию, то получим report размером больше входных данных. В нашем случае пакет более 130 МБ, значит мы точно не укладываемся в бюджет по памяти, если репорт будет висеть в памяти в полном объеме. Второе соображение, по другому предельному случаю - если весь входной пакет содержит информацию по сессиям одного пользователя. Значит класс User, содержащий все сессии пользователя, точно не позволит уложиться в бюджет. +- надо дорабатывать алгоритм, текущая реализация - только видимость потоковой обработки ) + +report состоит из двух частей: общая статистика и детализация по пользователям. Первая часть состоит из трех количественных показателей и одного списка уникальных браузеров(которых хоть и может быть много, но это довольно ограниченный список). Полагаю, эту часть можно держать в памяти во время всей работы метода. Вторая часть содержит информацию по пользователям и их сессиям, исходя из размышлений о множестве пользователей, имеющих по одной сессии, эту часть оставлять в памяти нельзя. +Варианты сохранения второй части: +- сохранять статистику по каждому отдельному пользователю в отдельный временный файл, и по завершению собрать из них и первой (общей) части report. +- по завершению сбора данных по пользователю записывать информацию в report, и по завершению всей работы дописать первую (общую) часть. + +Класс User не может содержать в себе все сессии (100 МБ данных данных с сессиями одного пользователя - уже не в бюджете). Так как в отчете требуется лишь накопительная статистика по сессиям, то и сам класс надо переписать на хранение статистики по сессиям. + +Итог: переписал в потоковом стиле. Выбрал вариант с записью в один файл, то есть информация по пользователю пишется сразу после обработки его "блока сессий". +Теперь метод в работе занимает 30-32 МБ, это проверял на 3250, 16250, 32500 и 100_000(проверял это запросом по памяти в каждой строке ()при 16250), но надо подтвердить в `valgrind massif visualizer`). +Выявилась проблема: "Полные тезки". На 100_000 итоговый json начал выдавать предупреждения о проблеме "Duplicate object key". Дело в том, что пользователи с одинаковыми именем и фамилией генерируют одинаковый ключ. Надо бы добавить id к ключу, чтоб решить эту проблему. Пример: `user,4011,Rico,Waneta,13`, `user,10412,Rico,Waneta,13` + +- 30 МБ +- MEMORY USAGE: 214 MB +Total allocated: 74.00 MB (941883 objects) +Total retained: 4.68 kB (9 objects) + +allocated memory by file +----------------------------------- + 74.00 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb + 40.00 B optim.rb + +allocated memory by location +----------------------------------- + 26.36 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb:87 + 15.25 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb:88 +allocated objects by location +----------------------------------- + 517087 /home/denio/thinknetika/rails-optimization-task2/task-2.rb:87 ### Ваша находка №2 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +- StackProf - +TOTAL (pct) SAMPLES (pct) FRAME + 4915710 (100.0%) 4279020 (87.0%) Object#work + 1693632 (34.5%) 480568 (9.8%) Object#collect_stats_from_user + 124038 (2.5%) 124038 (2.5%) Object#parse_session + 19744 (0.4%) 19744 (0.4%) Object#parse_user + 12340 (0.3%) 12340 (0.3%) Object#users_stats_initial +Внутри метода Object#work точка роста: + 2758207 (56.1%) / 2758207 (56.1%) | 87 | `all_browsers = report['allBrowsers'].split(',') << session['browser'].upcase` + MemoryPrfiler также указывал на это место. + +- Нет необходимости приводить report['allBrowsers'] к конечному виду (строка) на парсинге каждой строки (88: `report['allBrowsers'] = all_browsers.sort.uniq.join(',')`), достаточно сделать это один раз перед записью в файл. Соотвественно это позволит не преобразовывать строку в массив перед тем как добавить очередной браузер. +- Метрика снизилась до изменилась 30 МБ. Время работы на 16250 строках снизилось с 1.8 с до 1.2 с., понимаю, что это не метрика в данной задаче, но хоть по этому косвенному показателю видно некоторое улучшение. +- как изменился отчёт профилировщика StackProf +================================== + TOTAL (pct) SAMPLES (pct) FRAME + 2143723 (100.0%) 1507033 (70.3%) Object#work + 1693632 (79.0%) 480568 (22.4%) Object#collect_stats_from_user + 124038 (5.8%) 124038 (5.8%) Object#parse_session + 19744 (0.9%) 19744 (0.9%) Object#parse_user + + 13782 (0.6%) / 13782 (0.6%) | 87 | all_browsers = report['allBrowsers'] << session['browser'].upcase + 55126 (2.6%) / 55126 (2.6%) | 88 | report['allBrowsers'] = all_browsers.sort.uniq + + Количество объектов в методе 4915710 -> 2143723 снижение в 2.29 раза. И строка теперь явно не является источником проблем. + + +### Ваша находка №3 +- RubyProf () +Total: 2143724.000000 +Sort by: self_time -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика + %self total self wait child calls name location + 23.39 681725.000 501397.000 0.000 180328.000 55709 #parse + 22.60 1693632.000 484442.000 0.000 1209190.000 79272 Object#collect_stats_from_user /home/denio/thinknetika/rails-optimization-task2/task-2.rb:36 + 12.98 278273.000 278273.000 0.000 0.000 46282 String#split + +Graph и Stack тоже указывают на парсинг дат как следующую точку роста. + +- Даты в отчет записываются в виде строки из сортированных дат, чтобы сохранять возможность сортировки в ходе анализа сессий одного пользователя буду хранить даты в массиве. Сортировку и преобразование в строку выделю в одтельный метод и буду вызывать перед записью в файл. +- метрика осталась на прежнем уровне. У меня появилось подозрение, что я уже укладываюсь в бюджет. Проверил это с помощью valgrind, так и есть, программа выходит на уровень 43 МБ и дальше идет ровно. Хоть и есть мантра №1, но мой главный профит не в оптимизации метода, а в опробовании профайлеров. Так что еще пару итераций я сделаю +- Total: 1593510.000000 +Sort by: self_time + + %self total self wait child calls name location + 25.65 408700.000 408700.000 0.000 0.000 81740 Object#build_user_key /home/denio/thinknetika/rails-optimization-task2/task-2.rb:36 + 17.46 278273.000 278273.000 0.000 0.000 46282 String#split + 7.78 220512.000 124038.000 0.000 96474.000 13782 #parse + +Graph и Stack тоже указывают на то. что с датами стало все лучше. + +### Ваша находка №4 +- RubyProf CallTreePrinter (Callgrind) +следующая точка роста - генерация ключей. Ее влияение увеличил в ходе предыдущей правки +15_854_400 + 493_600 (весь метод work 63_646_680) +- буду создавать ключ сразу после User передавать его во необходимые методы. В генерации ключа удалена конкатенация +- метрика по памяти снова без особой реакции, но метрика по времени снизилась до 1с (предыдущая 1.2 с). valgrind не показал особых изменений. +- 493_600 (после изменения метода на использование и передачу ключа) -> 98_720 (удаление конкатенаций) (весь метод work 47 496 944) +вспомнил про баг "полные тезки" и добавил id в ключ, на отчете это не сказалось. 98_720 + +### Ваша находка №5 +- Интересная ситуация: MemoryProfiler vs RubyProf (Callgrind) +MemoryProfiler показывает, что точка роста в улучшении составления списка уникальных браузеров (там на самом деле так себе) +allocated memory by file +----------------------------------- + 32.28 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb + 40.00 B optim.rb + +allocated memory by location +----------------------------------- + 10.55 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb:98 - это составление списка уникальных браузеров. (Массив) + 4.33 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb:77 + +allocated memory by class +----------------------------------- + 13.09 MB Array + 7.60 MB String + +allocated objects by class +----------------------------------- + 160574 String + 27562 Array + +RubyProf выдал такой результат +work - 47_496_208 +foreach - 47_496_208 (self 3_562_200) +Object::collect_stats_from_user - 28_556_320 (3_523_280) +String::split 11_130_920 (11_130_920) +дальше даты, парсинг сессий.. + +то есть точка роста как будто скорее в string::split, что, в принципе, тоже верно, так как в данной оптимизации еще не исправленно двойное сплитование каждой строки + +Из любопытства прогнал еще через stackprof, получил еще один вариант ) + TOTAL (pct) SAMPLES (pct) FRAME + 1187423 (100.0%) 923686 (77.8%) Object#work + 124038 (10.4%) 124038 (10.4%) Object#parse_session + 713924 (60.1%) 84208 (7.1%) Object#collect_stats_from_user + +и он показвыват точку роста тут. И да, здесь тоже можно несложно оптимизировать + + | 141 | # Даты сессий через запятую в обратном порядке в формате iso8601 + 234310 (19.7%) | 142 | collect_stats_from_user(report, user, user_key) do |user| + | 143 | date_list = user.sessions_stats['dates'] || [] + 220528 (18.6%) / 220528 (18.6%) | 144 | date_list << Date.parse(session['date']) + | 145 | user.sessions_stats['dates'] = date_list + | 146 | user.sessions_stats + | 147 | end +хотя куча сплитований (если посидеть и поскладывать, тоже ялвяется по этому отчету точкой роста) + +- Решил оптимизировать по рекомендации MemoryProfiler. Изменил, так чтобы не создавались промежуточные переменные с массивами. +- Метрика 29, время снизилось до 0.7с (с 1с.) +- +allocated memory by file +----------------------------------- + 21.73 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb + +измененная строка ушла далеко с первой строки + 117.68 kB /home/denio/thinknetika/rails-optimization-task2/task-2.rb:97 + +### Ваша находка №6 +- RubyProf callgrind +String::split 11_130_920 (11_130_920) +- изменил методы парсинга на использование массива полей +- метрики практически не изменились +- String::split 6 679 640 + + +- MemoryProfiler +allocated memory by file +----------------------------------- + 20.19 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb + +allocated memory by class +----------------------------------- + 6.72 MB String + 4.30 MB File + +allocated objects by class +----------------------------------- + 138519 String + +- просто чтоюы использовать заведом полезную оптимизацию `# frozen_string_literal: true` +- Метрики не меняются значительно +- MemoryProfiler +allocated memory by file +----------------------------------- + 18.86 MB /home/denio/thinknetika/rails-optimization-task2/task-2.rb + + allocated memory by class +----------------------------------- + 5.38 MB String + 4.30 MB File + +allocated objects by class +----------------------------------- + 105113 String ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. -*Какими ещё результами можете поделиться* +Удалось улучшить метрику системы: +тестовый пакет 16250 строк +с MEMORY USAGE: 119 MB | Work time: 3.4953 +до MEMORY USAGE: 28 MB | Work time: 0.5687971760053188 +и уложиться в заданный бюджет. + +Если не поключать гемы, используемые лишь для разработки и тестов (pry, minitest), можно снизить потребление памяти на 5 МБ. -## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* +показатели при работе с полными данными (без тестовых гемов) +MEMORY USAGE: 24 MB +Work time: 144.6908641620248 diff --git a/task-2.rb b/task-2.rb index 34e09a3c..b5469d4b 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,22 +1,20 @@ -# Deoptimized version of homework task - +# frozen_string_literal: true require 'json' require 'pry' require 'date' require 'minitest/autorun' class User - attr_reader :attributes, :sessions + attr_reader :attributes, :sessions_stats - def initialize(attributes:, sessions:) + def initialize(attributes:, sessions_stats:) @attributes = attributes - @sessions = sessions + @sessions_stats = sessions_stats 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,9 +22,8 @@ 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], @@ -35,112 +32,134 @@ def parse_session(session) } 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 +def build_user_key(user) + "#{user.attributes['first_name']} #{user.attributes['last_name']} #{user.attributes['id']}" end -def work - file_lines = File.read('data.txt').split("\n") - - users = [] - sessions = [] +def prepare_dates_for_report_json(report, user, user_key) + report['usersStats'][user_key]['dates'] = report['usersStats'][user_key]['dates'].sort.reverse.map!(&:iso8601) +end - 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 +def collect_stats_from_user(report, user, user_key, &block) + report['usersStats'][user_key] ||= {} + report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) +end - # Отчёт в json - # - Сколько всего юзеров + - # - Сколько всего уникальных браузеров + - # - Сколько всего сессий + - # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + - # - # - По каждому пользователю - # - сколько всего сессий + - # - сколько всего времени + - # - самая длинная сессия + - # - браузеры через запятую + - # - Хоть раз использовал IE? + - # - Всегда использовал только Хром? + - # - даты сессий в порядке убывания через запятую + +def users_stats_initial + { + 'sessionsCount' => 0, + 'totalTime' => '0 min.', + 'longestSession' => '0 min.', + 'browsers' => '', + 'usedIE' => false, + 'alwaysUsedChrome' => true, + 'dates' => [] + } +end +def work(filename) report = {} - - report[:totalUsers] = users.count - - # Подсчёт количества уникальных браузеров + report[:totalUsers] = 0 + report['totalSessions'] = 0 + report['allBrowsers'] = [] 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(',') + user = 0 + user_key = '' - # Статистика по пользователям - users_objects = [] + File.write('result.json', '{"usersStats":{') - 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/ } } + File.foreach(filename, "\n") do |line| + cols = line.split(',') + + if cols[0] == 'user' + if report['usersStats'] + File.open('result.json', 'a') do |f| + f << ',' if report[:totalUsers] > 1 + prepare_dates_for_report_json(report, user, user_key) + f << report['usersStats'].to_json[1..-2] + end + end + report['usersStats'] = {} + + user = User.new(attributes: parse_user(cols), sessions_stats: users_stats_initial) + user_key = build_user_key(user) + report[:totalUsers] += 1 + end + if cols[0] == 'session' + session = parse_session(cols) + + # Collect total stats + report['totalSessions'] += 1 + uniqueBrowsers += [session['browser']] if uniqueBrowsers.all? { |b| b != session['browser'] } + report['uniqueBrowsersCount'] = uniqueBrowsers.count + + report['allBrowsers'] << session['browser'].upcase unless report['allBrowsers'].include?(session['browser'].upcase) + + #Collect user stats + collect_stats_from_user(report, user, user_key) do |user| + user.sessions_stats['sessionsCount'] = user.sessions_stats['sessionsCount'] + 1 + user.sessions_stats + end + + # Собираем количество времени по пользователям + collect_stats_from_user(report, user, user_key) do |user| + user.sessions_stats['totalTime'] = (user.sessions_stats['totalTime'].gsub(/\D/, '').to_i + session['time'].to_i).to_s + ' min.' + user.sessions_stats + end + + # Выбираем самую длинную сессию пользователя + collect_stats_from_user(report, user, user_key) do |user| + if user.sessions_stats['longestSession'].gsub(/\D/, '').to_i < session['time'].to_i + user.sessions_stats['longestSession'] = session['time'].to_s + ' min.' + end + user.sessions_stats + end + + # Браузеры пользователя через запятую + collect_stats_from_user(report, user, user_key) do |user| + user.sessions_stats['browsers'] = (user.sessions_stats['browsers'].split(',').map(&:strip) << session['browser'].upcase).sort.join(', ') + user.sessions_stats + end + + # Хоть раз использовал IE? + unless user.sessions_stats['usedIE'] + collect_stats_from_user(report, user, user_key) do |user| + user.sessions_stats['usedIE'] = session['browser'].upcase =~ /INTERNET EXPLORER/ ? true : false + user.sessions_stats + end + end + + # Всегда использовал только Chrome? + if user.sessions_stats['alwaysUsedChrome'] + collect_stats_from_user(report, user, user_key) do |user| + user.sessions_stats['alwaysUsedChrome'] = session['browser'].upcase =~ /CHROME/ ? true : false + user.sessions_stats + end + end + + # Даты сессий через запятую в обратном порядке в формате iso8601 + collect_stats_from_user(report, user, user_key) do |user| + date_list = user.sessions_stats['dates'] || [] + date_list << Date.parse(session['date']) + user.sessions_stats['dates'] = date_list + user.sessions_stats + end + # puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + end 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 } } + File.open('result.json', 'a') do |f| + f << ',' if report[:totalUsers] > 1 + if report['usersStats'].any? + prepare_dates_for_report_json(report, user, user_key) + f << report['usersStats'].to_json[1..-3] + end + report.delete('usersStats') + f << '}},' + + report['allBrowsers'] = report['allBrowsers'].sort.join(',') + f << report.to_json[1..-1] end - - File.write('result.json', "#{report.to_json}\n") puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end @@ -170,8 +189,8 @@ def setup 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"]}}}') + work('data.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 0":{"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 1":{"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 2":{"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/work_spec.rb b/work_spec.rb new file mode 100644 index 00000000..c2309d5d --- /dev/null +++ b/work_spec.rb @@ -0,0 +1,13 @@ +require 'rspec-benchmark' +require 'rspec' +require_relative 'task-2' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end + +describe '#work' do + it 'allocates less than 131,000 bytes in memory ' do + expect { work('data.txt') }.to perform_allocation(131000).bytes + end +end