From 4616760300c62817d892479b23f3023ab6691022 Mon Sep 17 00:00:00 2001 From: denio-rus Date: Wed, 28 Apr 2021 11:11:28 +0300 Subject: [PATCH 01/13] Change to a streaming work --- task-2.rb | 166 ++++++++++++++++++++++-------------------------------- 1 file changed, 66 insertions(+), 100 deletions(-) diff --git a/task-2.rb b/task-2.rb index 34e09a3c..da4a766d 100644 --- a/task-2.rb +++ b/task-2.rb @@ -35,114 +35,80 @@ 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 collect_stats_from_user(report, user, &block) + 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 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'] = {} + report[:totalUsers] = 0 + report['totalSessions'] = 0 + uniqueBrowsers = [] + report['allBrowsers'] = '' + user = 0 - # Собираем количество сессий по пользователям - 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 } } + File.foreach('data.txt', "\n") do |line| + cols = line.split(',') + if cols[0] == 'user' + user = User.new(attributes: parse_user(line), sessions: []) + report[:totalUsers] += 1 + end + if cols[0] == 'session' + session = parse_session(line) + user.sessions << session + report['totalSessions'] += 1 + end + + collect_stats_from_user(report, user) do |user| + { 'sessionsCount' => user.sessions.count } + end + + # Собираем количество времени по пользователям + collect_stats_from_user(report, user) do |user| + { 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' } + end + + # Выбираем самую длинную сессию пользователя + collect_stats_from_user(report, user) do |user| + { 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' } + end + + # Браузеры пользователя через запятую + collect_stats_from_user(report, user) do |user| + { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } + end + + # Хоть раз использовал IE? + collect_stats_from_user(report, user) do |user| + { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } + end + + # Всегда использовал только Chrome? + collect_stats_from_user(report, user) do |user| + { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } + end + + # Даты сессий через запятую в обратном порядке в формате iso8601 + collect_stats_from_user(report, user) do |user| + { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } + end + + if session + uniqueBrowsers += [session['browser']] if uniqueBrowsers.all? { |b| b != session['browser'] } + report['uniqueBrowsersCount'] = uniqueBrowsers.count + + all_browsers = report['allBrowsers'].split(',') << session['browser'].upcase + report['allBrowsers'] = all_browsers.sort.uniq.join(',') + end + + File.write('result.json', "#{report.to_json}\n") + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end - - File.write('result.json', "#{report.to_json}\n") - puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end +puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) class TestMe < Minitest::Test def setup From 299d9c64a5ea76128b1bf4b8df06657e37e16cb5 Mon Sep 17 00:00:00 2001 From: denio-rus Date: Wed, 28 Apr 2021 21:53:50 +0300 Subject: [PATCH 02/13] Before optimization --- case-study-template.md | 9 ++++++--- task-2.rb | 11 ++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index c3279664..bdc3e6f5 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,15 +12,18 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: объем памяти использыуемый приложением на конец выполнения. И время выполнения для сравнения с оптимизациями по 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. ## Вникаем в детали системы, чтобы найти главные точки роста Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* diff --git a/task-2.rb b/task-2.rb index da4a766d..13565aa2 100644 --- a/task-2.rb +++ b/task-2.rb @@ -41,7 +41,8 @@ def collect_stats_from_user(report, user, &block) report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) end -def work +def work(filename) + line_num = 0 report = {} report['usersStats'] = {} report[:totalUsers] = 0 @@ -50,7 +51,7 @@ def work report['allBrowsers'] = '' user = 0 - File.foreach('data.txt', "\n") do |line| + File.foreach(filename, "\n") do |line| cols = line.split(',') if cols[0] == 'user' user = User.new(attributes: parse_user(line), sessions: []) @@ -105,10 +106,10 @@ def work end File.write('result.json', "#{report.to_json}\n") - puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + # puts "Line #{line_num += 1} MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end -puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) class TestMe < Minitest::Test def setup @@ -136,7 +137,7 @@ def setup end def test_result - work + 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":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}') assert_equal expected_result, JSON.parse(File.read('result.json')) end From 6b18465f5fd35997f0e6f04bce09ada53f4db584 Mon Sep 17 00:00:00 2001 From: denio-rus Date: Sat, 1 May 2021 12:14:44 +0300 Subject: [PATCH 03/13] Abort mission -no real streaming found --- case-study-template.md | 77 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index bdc3e6f5..80d49bb9 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -24,15 +24,86 @@ Показатели: MEMORY USAGE: 170 MB Work time: 40.490744165999786. Попробовал запустить метод с опросом по памяти в конце обработки каждой строки. Максимальное значение, что увидел, примерное такое же (169) +/- погрешность. Но до 10000 строк значение составляли до 80 МВ, и только после выросло выше 100. Либо данные так скомпонованы, либо что-то накапливается. В любом случае, на первом этапе этот размер данных полагаю подходящим. Все измерения на ноуте с питанием от сети, CPU в режиме perfomance. +update: далее быстро выяснил, что запуск с профилировщиком и таким объемом данных это нерабочая схема. Для метрики можно использовать такой пакет, но для профилирования возьму 3250 строк. ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался: MemoryProfiler Вот какие проблемы удалось найти и решить ### Ваша находка №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, содержащий все сессии пользователя, точно не позволит уложиться в бюджет. +- надо дорабатывать алгоритм, текущая реализация - только видимость потоковой обработки ) + - как изменилась метрика - как изменился отчёт профилировщика From 102dc6d80510e6a9993f9ac5b26323c94dbb21de Mon Sep 17 00:00:00 2001 From: denio-rus Date: Sun, 2 May 2021 12:29:15 +0300 Subject: [PATCH 04/13] Fix json building --- task-2.rb | 139 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 89 insertions(+), 50 deletions(-) diff --git a/task-2.rb b/task-2.rb index 13565aa2..f6098b24 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,16 +1,14 @@ -# Deoptimized version of homework task - 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 @@ -41,72 +39,113 @@ def collect_stats_from_user(report, user, &block) report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) end +def users_stats_initial + { + 'sessionsCount' => 0, + 'totalTime' => '0 min.', + 'longestSession' => '0 min.', + 'browsers' => '', + 'usedIE' => false, + 'alwaysUsedChrome' => true, + 'dates' => [] + } +end + def work(filename) - line_num = 0 report = {} - report['usersStats'] = {} report[:totalUsers] = 0 report['totalSessions'] = 0 - uniqueBrowsers = [] report['allBrowsers'] = '' + uniqueBrowsers = [] user = 0 + File.write('result.json', '{"usersStats":{') + File.foreach(filename, "\n") do |line| cols = line.split(',') + if cols[0] == 'user' - user = User.new(attributes: parse_user(line), sessions: []) + if report['usersStats'] + File.open('result.json', 'a') do |f| + f << ',' if report[:totalUsers] > 1 + f << report['usersStats'].to_json[1..-2] + end + end + report['usersStats'] = {} + + user = User.new(attributes: parse_user(line), sessions_stats: users_stats_initial) report[:totalUsers] += 1 end if cols[0] == 'session' session = parse_session(line) - user.sessions << session - report['totalSessions'] += 1 - end - collect_stats_from_user(report, user) do |user| - { 'sessionsCount' => user.sessions.count } - end - - # Собираем количество времени по пользователям - collect_stats_from_user(report, user) do |user| - { 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' } - end - - # Выбираем самую длинную сессию пользователя - collect_stats_from_user(report, user) do |user| - { 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' } - end - - # Браузеры пользователя через запятую - collect_stats_from_user(report, user) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end - - # Хоть раз использовал IE? - collect_stats_from_user(report, user) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end - - # Всегда использовал только Chrome? - collect_stats_from_user(report, user) do |user| - { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } - end - - # Даты сессий через запятую в обратном порядке в формате iso8601 - collect_stats_from_user(report, user) do |user| - { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } - end - - if session + # Collect total stats + report['totalSessions'] += 1 uniqueBrowsers += [session['browser']] if uniqueBrowsers.all? { |b| b != session['browser'] } report['uniqueBrowsersCount'] = uniqueBrowsers.count all_browsers = report['allBrowsers'].split(',') << session['browser'].upcase report['allBrowsers'] = all_browsers.sort.uniq.join(',') + + #Collect user stats + collect_stats_from_user(report, user) do |user| + user.sessions_stats['sessionsCount'] = user.sessions_stats['sessionsCount'] + 1 + user.sessions_stats + end + + # Собираем количество времени по пользователям + collect_stats_from_user(report, user) 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) 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) 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) 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) do |user| + user.sessions_stats['alwaysUsedChrome'] = session['browser'].upcase =~ /CHROME/ ? true : false + user.sessions_stats + end + end + + # Даты сессий через запятую в обратном порядке в формате iso8601 + collect_stats_from_user(report, user) do |user| + date_list = user.sessions_stats['dates']&.map { |d| Date.parse(d) } || [] + date_list << Date.parse(session['date']) + user.sessions_stats['dates'] = date_list.sort.reverse.map { |d| d.iso8601 } + user.sessions_stats + end + # puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end - - File.write('result.json', "#{report.to_json}\n") - # puts "Line #{line_num += 1} MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + end + + File.open('result.json', 'a') do |f| + f << ',' if report[:totalUsers] > 1 + f << report['usersStats'].to_json[1..-3] if report['usersStats'].any? + report.delete('usersStats') + f << '}},' + f << report.to_json[1..-1] end puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end From 668bba321b2d08570180c2b26ec1bd5c0ad90674 Mon Sep 17 00:00:00 2001 From: denio-rus Date: Sun, 2 May 2021 12:29:43 +0300 Subject: [PATCH 05/13] Otimization - iteration 1 Change method work to streaming style work --- case-study-template.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 80d49bb9..c2c562a2 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -104,8 +104,34 @@ allocated memory by location Проанализировав отчет, увидел, что report хоть и записывается на каждом круге, но остается полным в памяти и просто перезаписывает себя после парсера строки и сбора статистики. Это неправильное поведение. Если взять предельный случай, когда полные данные - это много уникальных пользователей, каждый из которых имеет одну сессию, то получим report размером больше входных данных. В нашем случае пакет более 130 МБ, значит мы точно не укладываемся в бюджет по памяти, если репорт будет висеть в памяти в полном объеме. Второе соображение, по другому предельному случаю - если весь входной пакет содержит информацию по сессиям одного пользователя. Значит класс User, содержащий все сессии пользователя, точно не позволит уложиться в бюджет. - надо дорабатывать алгоритм, текущая реализация - только видимость потоковой обработки ) -- как изменилась метрика -- как изменился отчёт профилировщика +report состоит из двух частей: общая статистика и детализация по пользователям. Первая часть состоит из трех количественных показателей и одного списка уникальных браузеров(которых хоть и может быть много, но это довольно ограниченный список). Полагаю, эту часть можно держать в памяти во время всей работы метода. Вторая часть содержит информацию по пользователям и их сессиям, исходя из размышлений о множестве пользователей, имеющих по одной сессии, эту часть оставлять в памяти нельзя. +Варианты сохранения второй части: +- сохранять статистику по каждому отдельному пользователю в отдельный временный файл, и по завершению собрать из них и первой (общей) части report. +- по завершению сбора данных по пользователю записывать информацию в report, и по завершению всей работы дописать первую (общую) часть. + +Класс User не может содержать в себе все сессии (100 МБ данных данных с сессиями одного пользователя - уже не в бюджете). Так как в отчете требуется лишь накопительная статистика по сессиям, то и сам класс надо переписать на хранение статистики по сессиям. + +Итог: переписал в потоковом стиле. Выбрал вариант с записью в один файл, то есть информация по пользователю пишется сразу после обработки его "блока сессий". +Теперь метод в работе занимает 29-30 МБ, это проверял на 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 - какой отчёт показал главную точку роста From 59377a0303b00d792ab4aecab3a4d16b04b8316d Mon Sep 17 00:00:00 2001 From: denio-rus Date: Sun, 2 May 2021 15:06:46 +0300 Subject: [PATCH 06/13] Optimization iteration 2 improve collecting allBrowsers stats --- case-study-template.md | 30 +++++++++++++++++++++++++++++- task-2.rb | 9 +++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index c2c562a2..c63195f4 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -112,7 +112,7 @@ report состоит из двух частей: общая статистик Класс User не может содержать в себе все сессии (100 МБ данных данных с сессиями одного пользователя - уже не в бюджете). Так как в отчете требуется лишь накопительная статистика по сессиям, то и сам класс надо переписать на хранение статистики по сессиям. Итог: переписал в потоковом стиле. Выбрал вариант с записью в один файл, то есть информация по пользователю пишется сразу после обработки его "блока сессий". -Теперь метод в работе занимает 29-30 МБ, это проверял на 3250, 16250, 32500 и 100_000(проверял это запросом по памяти в каждой строке ()при 16250), но надо подтвердить в `valgrind massif visualizer`). +Теперь метод в работе занимает 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 МБ @@ -134,6 +134,34 @@ 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 раза. И строка теперь ясно не является источником проблем. + + +### Ваша находка №X - какой отчёт показал главную точку роста - как вы решили её оптимизировать - как изменилась метрика diff --git a/task-2.rb b/task-2.rb index f6098b24..bcbacc0d 100644 --- a/task-2.rb +++ b/task-2.rb @@ -55,7 +55,7 @@ def work(filename) report = {} report[:totalUsers] = 0 report['totalSessions'] = 0 - report['allBrowsers'] = '' + report['allBrowsers'] = [] uniqueBrowsers = [] user = 0 @@ -84,9 +84,8 @@ def work(filename) uniqueBrowsers += [session['browser']] if uniqueBrowsers.all? { |b| b != session['browser'] } report['uniqueBrowsersCount'] = uniqueBrowsers.count - all_browsers = report['allBrowsers'].split(',') << session['browser'].upcase - report['allBrowsers'] = all_browsers.sort.uniq.join(',') - + all_browsers = report['allBrowsers'] << session['browser'].upcase + report['allBrowsers'] = all_browsers.sort.uniq #Collect user stats collect_stats_from_user(report, user) do |user| user.sessions_stats['sessionsCount'] = user.sessions_stats['sessionsCount'] + 1 @@ -145,6 +144,8 @@ def work(filename) f << report['usersStats'].to_json[1..-3] if report['usersStats'].any? report.delete('usersStats') f << '}},' + + report['allBrowsers'] = report['allBrowsers'].join(',') f << report.to_json[1..-1] end puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) From 9ab4c03e537a38887b869652a5c19e4d8edf7cc1 Mon Sep 17 00:00:00 2001 From: denio-rus Date: Tue, 4 May 2021 12:27:24 +0300 Subject: [PATCH 07/13] Optimization iteration 3 improve Date parsing --- case-study-template.md | 28 ++++++++++++++++++++++++++-- task-2.rb | 21 +++++++++++++++++---- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index c63195f4..115f7f8c 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -27,7 +27,7 @@ update: далее быстро выяснил, что запуск с профилировщиком и таким объемом данных это нерабочая схема. Для метрики можно использовать такой пакет, но для профилирования возьму 3250 строк. ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался: MemoryProfiler +Для того, чтобы найти "точки роста" для оптимизации я воспользовался: MemoryProfiler, StackProf, RubyProf(flat, graph, stack) Вот какие проблемы удалось найти и решить @@ -161,7 +161,31 @@ TOTAL (pct) SAMPLES (pct) FRAME Количество объектов в методе 4915710 -> 2143723 снижение в 2.29 раза. И строка теперь ясно не является источником проблем. -### Ваша находка №X +### Ваша находка №3 +- RubyProf () +Total: 2143724.000000 +Sort by: self_time + + %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 - какой отчёт показал главную точку роста - как вы решили её оптимизировать - как изменилась метрика diff --git a/task-2.rb b/task-2.rb index bcbacc0d..9ff32993 100644 --- a/task-2.rb +++ b/task-2.rb @@ -33,8 +33,17 @@ def parse_session(session) } end +def build_user_key(user) + "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" +end + +def prepare_dates_for_report_json(report, user) + key = build_user_key(user) + report['usersStats'][key]['dates'] = report['usersStats'][key]['dates'].sort.reverse.map!(&:iso8601) +end + def collect_stats_from_user(report, user, &block) - user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" + user_key = build_user_key(user) report['usersStats'][user_key] ||= {} report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) end @@ -68,6 +77,7 @@ def work(filename) if report['usersStats'] File.open('result.json', 'a') do |f| f << ',' if report[:totalUsers] > 1 + prepare_dates_for_report_json(report, user) f << report['usersStats'].to_json[1..-2] end end @@ -130,9 +140,9 @@ def work(filename) # Даты сессий через запятую в обратном порядке в формате iso8601 collect_stats_from_user(report, user) do |user| - date_list = user.sessions_stats['dates']&.map { |d| Date.parse(d) } || [] + date_list = user.sessions_stats['dates'] || [] date_list << Date.parse(session['date']) - user.sessions_stats['dates'] = date_list.sort.reverse.map { |d| d.iso8601 } + user.sessions_stats['dates'] = date_list user.sessions_stats end # puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) @@ -141,7 +151,10 @@ def work(filename) File.open('result.json', 'a') do |f| f << ',' if report[:totalUsers] > 1 - f << report['usersStats'].to_json[1..-3] if report['usersStats'].any? + if report['usersStats'].any? + prepare_dates_for_report_json(report, user) + f << report['usersStats'].to_json[1..-3] + end report.delete('usersStats') f << '}},' From 0cb4ae95db8c28c00c92fb6f4f545f056bf4cdff Mon Sep 17 00:00:00 2001 From: denio-rus Date: Tue, 4 May 2021 13:15:36 +0300 Subject: [PATCH 08/13] Optimization iteration 4 user key (RubyProf Callgrind) --- case-study-template.md | 13 ++++++++----- task-2.rb | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 115f7f8c..f1f72dcb 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -158,7 +158,7 @@ TOTAL (pct) SAMPLES (pct) FRAME 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 раза. И строка теперь ясно не является источником проблем. + Количество объектов в методе 4915710 -> 2143723 снижение в 2.29 раза. И строка теперь явно не является источником проблем. ### Ваша находка №3 @@ -186,10 +186,13 @@ Sort by: self_time 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 ### Ваша находка №X - какой отчёт показал главную точку роста diff --git a/task-2.rb b/task-2.rb index 9ff32993..083b27ba 100644 --- a/task-2.rb +++ b/task-2.rb @@ -34,16 +34,14 @@ def parse_session(session) end def build_user_key(user) - "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" + "#{user.attributes['first_name']} #{user.attributes['last_name']} #{user.attributes['id']}" end -def prepare_dates_for_report_json(report, user) - key = build_user_key(user) - report['usersStats'][key]['dates'] = report['usersStats'][key]['dates'].sort.reverse.map!(&:iso8601) +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 -def collect_stats_from_user(report, user, &block) - user_key = build_user_key(user) +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 @@ -67,6 +65,7 @@ def work(filename) report['allBrowsers'] = [] uniqueBrowsers = [] user = 0 + user_key = '' File.write('result.json', '{"usersStats":{') @@ -77,13 +76,14 @@ def work(filename) if report['usersStats'] File.open('result.json', 'a') do |f| f << ',' if report[:totalUsers] > 1 - prepare_dates_for_report_json(report, user) + 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(line), sessions_stats: users_stats_initial) + user_key = build_user_key(user) report[:totalUsers] += 1 end if cols[0] == 'session' @@ -97,19 +97,19 @@ def work(filename) all_browsers = report['allBrowsers'] << session['browser'].upcase report['allBrowsers'] = all_browsers.sort.uniq #Collect user stats - collect_stats_from_user(report, user) do |user| + 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) do |user| + 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) do |user| + 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 @@ -117,14 +117,14 @@ def work(filename) end # Браузеры пользователя через запятую - collect_stats_from_user(report, user) do |user| + 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) do |user| + 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 @@ -132,14 +132,14 @@ def work(filename) # Всегда использовал только Chrome? if user.sessions_stats['alwaysUsedChrome'] - collect_stats_from_user(report, user) do |user| + 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) do |user| + 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 @@ -152,7 +152,7 @@ def work(filename) File.open('result.json', 'a') do |f| f << ',' if report[:totalUsers] > 1 if report['usersStats'].any? - prepare_dates_for_report_json(report, user) + prepare_dates_for_report_json(report, user, user_key) f << report['usersStats'].to_json[1..-3] end report.delete('usersStats') @@ -191,7 +191,7 @@ def setup def test_result 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":{"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"]}}}') + 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 From dc29d7f1985e9f2c6222dcefc4b05835b7244456 Mon Sep 17 00:00:00 2001 From: denio-rus Date: Tue, 4 May 2021 14:25:33 +0300 Subject: [PATCH 09/13] Optimization iteration 5 improve collection of allBrowsers --- case-study-template.md | 65 ++++++++++++++++++++++++++++++++++++++---- task-2.rb | 6 ++-- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index f1f72dcb..8c8bee98 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -27,7 +27,7 @@ update: далее быстро выяснил, что запуск с профилировщиком и таким объемом данных это нерабочая схема. Для метрики можно использовать такой пакет, но для профилирования возьму 3250 строк. ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался: MemoryProfiler, StackProf, RubyProf(flat, graph, stack) +Для того, чтобы найти "точки роста" для оптимизации я воспользовался: MemoryProfiler, StackProf, RubyProf(flat, graph, stack, callgrind) Вот какие проблемы удалось найти и решить @@ -194,11 +194,64 @@ Graph и Stack тоже указывают на то. что с датами с - 493_600 (после изменения метода на использование и передачу ключа) -> 98_720 (удаление конкатенаций) (весь метод work 47 496 944) вспомнил про баг "полные тезки" и добавил id в ключ, на отчете это не сказалось. 98_720 -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +### Ваша находка №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 ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. diff --git a/task-2.rb b/task-2.rb index 083b27ba..8b3b751f 100644 --- a/task-2.rb +++ b/task-2.rb @@ -94,8 +94,8 @@ def work(filename) uniqueBrowsers += [session['browser']] if uniqueBrowsers.all? { |b| b != session['browser'] } report['uniqueBrowsersCount'] = uniqueBrowsers.count - all_browsers = report['allBrowsers'] << session['browser'].upcase - report['allBrowsers'] = all_browsers.sort.uniq + 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 @@ -158,7 +158,7 @@ def work(filename) report.delete('usersStats') f << '}},' - report['allBrowsers'] = report['allBrowsers'].join(',') + report['allBrowsers'] = report['allBrowsers'].sort.join(',') f << report.to_json[1..-1] end puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) From 21da6e9274b9e02bcf7f4ecebad54ca7496be58f Mon Sep 17 00:00:00 2001 From: denio-rus Date: Tue, 4 May 2021 14:37:46 +0300 Subject: [PATCH 10/13] Optimization iteration 6 remove double file_line split --- case-study-template.md | 8 ++++++++ task-2.rb | 14 ++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 8c8bee98..946b5822 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -253,6 +253,14 @@ allocated memory by file измененная строка ушла далеко с первой строки 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 + + ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. diff --git a/task-2.rb b/task-2.rb index 8b3b751f..9578fd0b 100644 --- a/task-2.rb +++ b/task-2.rb @@ -12,9 +12,8 @@ def initialize(attributes:, 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], @@ -22,9 +21,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], @@ -82,12 +80,12 @@ def work(filename) end report['usersStats'] = {} - user = User.new(attributes: parse_user(line), sessions_stats: users_stats_initial) + 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(line) + session = parse_session(cols) # Collect total stats report['totalSessions'] += 1 From c6087f4923f9b2707085b6e2eb46a15caf831cd9 Mon Sep 17 00:00:00 2001 From: denio-rus Date: Tue, 4 May 2021 15:25:30 +0300 Subject: [PATCH 11/13] Optimiation - Frozen strings --- case-study-template.md | 30 ++++++++++++++++++++++++++++++ task-2.rb | 1 + 2 files changed, 31 insertions(+) diff --git a/case-study-template.md b/case-study-template.md index 946b5822..c4ac144e 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -261,6 +261,36 @@ 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 + ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. diff --git a/task-2.rb b/task-2.rb index 9578fd0b..b5469d4b 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'json' require 'pry' require 'date' From c1ac1712db5615e7708cdc3ab745d90e09589d91 Mon Sep 17 00:00:00 2001 From: denio-rus Date: Tue, 4 May 2021 15:49:58 +0300 Subject: [PATCH 12/13] Final --- case-study-template.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index c4ac144e..658183bb 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -293,9 +293,15 @@ allocated objects by class ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. -*Какими ещё результами можете поделиться* +Удалось улучшить метрику системы: +тестовый пакет 16250 строк +с MEMORY USAGE: 119 MB | Work time: 3.4953 +до MEMORY USAGE: 28 MB | Work time: 0.5687971760053188 +и уложиться в заданный бюджет. -## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* +Если не поключать гемы, используемые лишь для разработки и тестов (pry, minitest), можно снизить потребление памяти на 5 МБ. + +показатели при работе с полными данными (без тестовых гемов) +MEMORY USAGE: 24 MB +Work time: 144.6908641620248 From f9701cece3f4a51f446ded1cd38ac9eec06d06fb Mon Sep 17 00:00:00 2001 From: denio-rus Date: Tue, 4 May 2021 16:11:05 +0300 Subject: [PATCH 13/13] Add spec --- work_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 work_spec.rb 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