From 31871a4e03c1bbdeeb356704202d7d54eceda1c2 Mon Sep 17 00:00:00 2001 From: Alexey Kirkov Date: Tue, 12 Mar 2019 12:04:57 +0400 Subject: [PATCH 1/2] init --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc77a11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +data.txt +data_1MB.txt +data_20MB.txt +data_5MB.txt +data_large.txt +result.json From 0f5f7d3fabb1c5bef53296da8d26a92804b6b8a2 Mon Sep 17 00:00:00 2001 From: Alexey Kirkov Date: Tue, 12 Mar 2019 12:07:02 +0400 Subject: [PATCH 2/2] Added changes --- case-study-template.md | 115 ++++++++++++++++++++++++++++++++---- task-1.rb | 129 ++++++++++++++++++----------------------- 2 files changed, 162 insertions(+), 82 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index e0eef00..10743bb 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,35 +12,128 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: + +- Измерять общее время выполнения скрипта с помощью `Benchmark.realtime` +- Использовать профилировщие памяти `memory_profiler` + +Для оптимизации был использован тестовый файл 25000 строк (объемом около 1МБ), перед началом оптимизации были следующие показатели: + +- Расход памяти: 1655 MB +- Время выполнения: 25.61 sec ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за время меньшее чем 30 секунд + +Вот как я построил `feedback_loop`: -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +- подготовка тестового файла на 25 000 строк +- замеры метрик +- анализ метрик +- исправление самого узкого места +- оценка полученного результата ## Вникаем в детали системы, чтобы найти 20% точек роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался `benchmark` и `memory_profiler` Вот какие проблемы удалось найти и решить ### Ваша находка №1 -О вашей находке №1 +Большего всего памяти расходовалось при формировании массивов `users` и `sessions` +Изменил формирование на модификации массивов. +Это позволило снизить расход памяти и немного уменьшить время выполнения. + +``` shellsession +rss after concatenation: 1655 MB +Finish in 25.61 + +rss after concatenation: 491 MB +Finish in 18.09 + +``` ### Ваша находка №2 -О вашей находке №2 +Следующим узким местом был сам принцип формирования массивов. +Сделал рефакторинг убрав максимально возможное количество временных переменных. +Это позволило сократить время выполения. + +``` shellsession +rss after concatenation: 491 MB +Finish in 18.09 + +rss after concatenation: 418 MB +Finish in 4.57 + +``` + +### Ваша находка №3 +Следующее узкое место формирование статистики юзера. +Объединил формирование в статистики в один цикл. +Уменьшилось количество потребляемой памяти и время выполнения + +``` shellsession +rss after concatenation: 418 MB +Finish in 4.57 + +rss after concatenation: 370 MB +Finish in 3.68 -### Ваша находка №X -О вашей находке №X +``` + +### Ваша находка №4 +По метрикам и по моему имеющемуся опыту следущее узкое место это парсинг дат. +Отрефакторил заменив `Date.parse` на `Date.strptime` +В итоге уменьшилось количество потребляемой памяти и время выполнения + +``` shellsession +rss after concatenation: 370 MB +Finish in 3.68 + +rss after concatenation: 314 MB +Finish in 2.87 + +``` + +### Ваша находка №5 +По метрикам было видно что чаще всего используются стринги ` ` и `, `. +Вынес их в константы, уменьшив потребление памяти и времени. + +``` shellsession +rss after concatenation: 314 MB +Finish in 2.87 + +rss after concatenation: 308 MB +Finish in 2.67 + +``` ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* +Удалось улучшить метрику системы с 25 секунд до 0.26 секунды, что позволило завершить обработку исходного файла. + + +``` shellsession +Start +rss before concatenation: 24 MB +rss after concatenation: 50 MB +Finish in 0.26 + +``` + +Файл `data_large.txt` в итоге стал обрабатываться за время 46.93 секунды + +``` shellsession +Start +rss before concatenation: 23 MB +rss after concatenation: 2530 MB +Finish in 46.93 -*Какими ещё результами можете поделиться* +``` +Также есть дальнейший путь оптимизации, к примеру замена регулярок на `match`, +изменить формирование хешей с `stringify keys` на `symbolize keys` ## Защита от регресса производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сохрнял среднее значение минимального времени выполения и потребления памяти. diff --git a/task-1.rb b/task-1.rb index 778672d..3e8aa40 100644 --- a/task-1.rb +++ b/task-1.rb @@ -4,6 +4,8 @@ require 'pry' require 'date' require 'minitest/autorun' +require 'benchmark' +require 'memory_profiler' class User attr_reader :attributes, :sessions @@ -14,45 +16,54 @@ def initialize(attributes:, sessions:) end end +IE_REG = /INTERNET EXPLORER/.freeze +CHROME_REG = /CHROME/.freeze +COMMA = ','.freeze + def parse_user(user) - fields = user.split(',') - parsed_result = { + fields = user.split(COMMA) + { 'id' => fields[1], 'first_name' => fields[2], 'last_name' => fields[3], - 'age' => fields[4], + 'age' => fields[4] } end def parse_session(session) - fields = session.split(',') - parsed_result = { + fields = session.split(COMMA) + { 'user_id' => fields[1], 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[5], + 'browser' => fields[3].upcase, + 'time' => fields[4].to_i, + 'date' => Date.strptime(fields[5]).iso8601 } 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']}" + 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)) + report['usersStats'][user_key].merge!(block.call(user)) end end -def work - file_lines = File.read('data.txt').split("\n") +def process_user_data(line) + User.new(attributes: parse_user(line), sessions: []) +end + +def work(file = 'data.txt') + unique_browsers = [] + users_objects = [] - users = [] - sessions = [] + File.read(file).split("\n").each do |line| + next users_objects << process_user_data(line) if line.start_with?('user') - 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' + users_objects.last.sessions << parse_session(line) + next if unique_browsers.include?(users_objects.last.sessions.last['browser']) + + unique_browsers << users_objects.last.sessions.last['browser'] end # Отчёт в json @@ -72,77 +83,53 @@ def work report = {} - report[:totalUsers] = users.count - - # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end - - report['uniqueBrowsersCount'] = uniqueBrowsers.count - - report['totalSessions'] = sessions.count + report[:totalUsers] = users_objects.count - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') + report['uniqueBrowsersCount'] = unique_browsers.count - # Статистика по пользователям - users_objects = [] + report['totalSessions'] = users_objects.flat_map(&:sessions).count - 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['allBrowsers'] = unique_browsers.sort.join(',') report['usersStats'] = {} - # Собираем количество сессий по пользователям collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } + tme = user.sessions.map { |s| s['time'] } + brw = user.sessions.map { |s| s['browser'] } + { + 'sessionsCount' => user.sessions.count, + 'totalTime' => tme.sum.to_s << ' min.', + 'longestSession' => tme.max.to_s << ' min.', + 'browsers' => brw.sort.join(', '), + 'usedIE' => brw.any? { |b| b =~ IE_REG }, + 'alwaysUsedChrome' => brw.all? { |b| b =~ CHROME_REG }, + 'dates' => user.sessions.map! { |s| s['date'] }.sort.reverse + } 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 + File.write('result.json', "#{report.to_json}\n") +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 +puts 'Start' - # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end +def print_memory_usage + "%d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) +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 +time = Benchmark.realtime do + puts "rss before concatenation: #{print_memory_usage}" - # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } + report = MemoryProfiler.report do + work('data_1MB.txt') 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 + report.pretty_print(detailed_report: true) - File.write('result.json', "#{report.to_json}\n") + puts "rss after concatenation: #{print_memory_usage}" end +puts "Finish in #{time.round(2)}" + class TestMe < Minitest::Test def setup File.write('result.json', '')