From 31724fa633ea9533e6e15539e1abafee121669c8 Mon Sep 17 00:00:00 2001 From: Beglov Sergey Date: Thu, 2 May 2024 12:40:26 +0300 Subject: [PATCH 01/10] Streaming approach --- task-2.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/task-2.rb b/task-2.rb index 34e09a3c..bafeebd8 100644 --- a/task-2.rb +++ b/task-2.rb @@ -43,13 +43,13 @@ def collect_stats_from_users(report, users_objects, &block) end end -def work - file_lines = File.read('data.txt').split("\n") +def work(filename = 'data.txt') + users = [] sessions = [] - file_lines.each do |line| + File.foreach(filename) do |line| cols = line.split(',') users = users + [parse_user(line)] if cols[0] == 'user' sessions = sessions + [parse_session(line)] if cols[0] == 'session' @@ -175,3 +175,8 @@ def test_result assert_equal expected_result, JSON.parse(File.read('result.json')) end end + + +# work('data.txt') +work('data10000.txt') +# work('data40000.txt') \ No newline at end of file From 7c47c0029236137c69d66d3996672e1128dbdc81 Mon Sep 17 00:00:00 2001 From: Beglov Sergey Date: Thu, 2 May 2024 14:04:11 +0300 Subject: [PATCH 02/10] Construct sessions array --- case-study-template.md | 18 +++++++++--------- task-2.rb | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index c3279664..6a53d74b 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,26 +12,26 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: затрачиваемое количество мегабайт памяти на обработку файла в 10000 строк. ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за несколько секунд. -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +Вот как я построил `feedback_loop`: поскольку в программе имелся тест проверяющий корректность работы и выводилось количество потребляемой памяти, то фактически feedback_loop уже имелся, оставалось только подобрать для него подходящий объем данных (я остановился на значении 10000 строк). ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался memory_profiler. Вот какие проблемы удалось найти и решить -### Ваша находка №1 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +### Находка №1 - Не эффективное формирование массива с сессиями +- отчёт memory_profiler +- решил формировать массив путем простой вставки элемнета +- метрика снизилась с 89 MB до 47 MB +- указанная проблема перестала быть главной точкой роста ### Ваша находка №2 - какой отчёт показал главную точку роста diff --git a/task-2.rb b/task-2.rb index bafeebd8..74de44aa 100644 --- a/task-2.rb +++ b/task-2.rb @@ -3,6 +3,7 @@ require 'json' require 'pry' require 'date' +require 'memory_profiler' require 'minitest/autorun' class User @@ -44,15 +45,16 @@ def collect_stats_from_users(report, users_objects, &block) end def work(filename = 'data.txt') - - users = [] sessions = [] File.foreach(filename) do |line| cols = line.split(',') users = users + [parse_user(line)] if cols[0] == 'user' - sessions = sessions + [parse_session(line)] if cols[0] == 'session' + + if cols[0] == 'session' + sessions << parse_session(line) + end end # Отчёт в json @@ -179,4 +181,10 @@ def test_result # work('data.txt') work('data10000.txt') -# work('data40000.txt') \ No newline at end of file +# work('data40000.txt') + +### memory_profiler +# report = MemoryProfiler.report do +# work('data10000.txt') +# end +# report.pretty_print(scale_bytes: true) \ No newline at end of file From ded3c45bbfde897dbe9ddf1f85e65e43e24b95b7 Mon Sep 17 00:00:00 2001 From: Beglov Sergey Date: Sat, 4 May 2024 12:38:09 +0300 Subject: [PATCH 03/10] Construct user sessions array --- case-study-template.md | 10 +++++----- task-2.rb | 36 ++++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 6a53d74b..b9d6dace 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -33,11 +33,11 @@ - метрика снизилась с 89 MB до 47 MB - указанная проблема перестала быть главной точкой роста -### Ваша находка №2 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +### Ваша находка №2 - Создание для каждого пользователя нового массива с сессиями +- отчёт memory_profiler +- сформировать массив users_objects в цикле считывания данных из файла +- метрика снизилась с 47 MB до 34 MB +- указанная проблема перестала быть главной точкой роста ### Ваша находка №X - какой отчёт показал главную точку роста diff --git a/task-2.rb b/task-2.rb index 74de44aa..69c085a8 100644 --- a/task-2.rb +++ b/task-2.rb @@ -47,16 +47,38 @@ def collect_stats_from_users(report, users_objects, &block) def work(filename = 'data.txt') users = [] sessions = [] + users_objects = [] + user_sessions = [] + current_user = nil + prev_user = nil File.foreach(filename) do |line| cols = line.split(',') - users = users + [parse_user(line)] if cols[0] == 'user' + + if cols[0] == 'user' + user = parse_user(line) + + prev_user = current_user + current_user = user + + if prev_user != nil + users_objects << User.new(attributes: prev_user, sessions: user_sessions) + user_sessions = [] + end + + users << user + end if cols[0] == 'session' - sessions << parse_session(line) + session = parse_session(line) + + user_sessions << session + sessions << session end end + users_objects << User.new(attributes: current_user, sessions: user_sessions) + # Отчёт в json # - Сколько всего юзеров + # - Сколько всего уникальных браузеров + @@ -95,16 +117,6 @@ def work(filename = 'data.txt') .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'] = {} # Собираем количество сессий по пользователям From c78daf371c22e0ad5a41541a2e07074d71bd2ffc Mon Sep 17 00:00:00 2001 From: Beglov Sergey Date: Sat, 4 May 2024 13:17:31 +0300 Subject: [PATCH 04/10] Setup stackprof --- task-2.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/task-2.rb b/task-2.rb index 69c085a8..f1d086c7 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,8 +1,9 @@ -# Deoptimized version of homework task +# Optimized version of homework task require 'json' require 'pry' require 'date' +require 'stackprof' require 'memory_profiler' require 'minitest/autorun' @@ -192,11 +193,17 @@ def test_result # work('data.txt') -work('data10000.txt') +# work('data10000.txt') # work('data40000.txt') +# work('data500000.txt') ### memory_profiler # report = MemoryProfiler.report do # work('data10000.txt') # end -# report.pretty_print(scale_bytes: true) \ No newline at end of file +# report.pretty_print(scale_bytes: true) + +### stackprof +StackProf.run(mode: :object, out: 'stackprof_reports/stackprof.dump', raw: true) do + work('data10000.txt') +end \ No newline at end of file From 0d4089fd8bc86a14c9e8be0ac03c07c0abc2871f Mon Sep 17 00:00:00 2001 From: Beglov Sergey Date: Sat, 4 May 2024 13:30:25 +0300 Subject: [PATCH 05/10] Setup ruby-prof --- task-2.rb | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/task-2.rb b/task-2.rb index f1d086c7..cdd545fc 100644 --- a/task-2.rb +++ b/task-2.rb @@ -4,6 +4,7 @@ require 'pry' require 'date' require 'stackprof' +require 'ruby-prof' require 'memory_profiler' require 'minitest/autorun' @@ -204,6 +205,30 @@ def test_result # report.pretty_print(scale_bytes: true) ### stackprof -StackProf.run(mode: :object, out: 'stackprof_reports/stackprof.dump', raw: true) do +# StackProf.run(mode: :object, out: 'stackprof_reports/stackprof.dump', raw: true) do +# work('data10000.txt') +# end + +### ruby-prof +# RubyProf.measure_mode = RubyProf::ALLOCATIONS +# На этот раз профилируем не allocations, а объём памяти! +RubyProf.measure_mode = RubyProf::MEMORY + +result = RubyProf.profile do work('data10000.txt') -end \ No newline at end of file +end + +printer = RubyProf::FlatPrinter.new(result) +printer.print(File.open('ruby_prof_reports/flat.txt', 'w+')) + +# printer = RubyProf::DotPrinter.new(result) +# printer.print(File.open('ruby_prof_reports/graphviz.dot', 'w+')) + +printer = RubyProf::GraphHtmlPrinter.new(result) +printer.print(File.open('ruby_prof_reports/graph.html', 'w+')) + +printer = RubyProf::CallStackPrinter.new(result) +printer.print(File.open('ruby_prof_reports/callstack.html', 'w+')) + +printer = RubyProf::CallTreePrinter.new(result) +printer.print(path: 'ruby_prof_reports', profile: 'profile') From 46b37571cba964ee13806e46948406584b9176b9 Mon Sep 17 00:00:00 2001 From: Beglov Sergey Date: Sat, 4 May 2024 14:02:13 +0300 Subject: [PATCH 06/10] Fix split problem --- case-study-template.md | 8 ++++++- task-2.rb | 54 ++++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index b9d6dace..eaa745e9 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -23,7 +23,7 @@ Вот как я построил `feedback_loop`: поскольку в программе имелся тест проверяющий корректность работы и выводилось количество потребляемой памяти, то фактически feedback_loop уже имелся, оставалось только подобрать для него подходящий объем данных (я остановился на значении 10000 строк). ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался memory_profiler. +Для того, чтобы найти "точки роста" для оптимизации я воспользовался memory_profiler, stackprof и ruby-prof. Вот какие проблемы удалось найти и решить @@ -39,6 +39,12 @@ - метрика снизилась с 47 MB до 34 MB - указанная проблема перестала быть главной точкой роста +### Ваша находка №3 - Избыточный вызов метода split в методах parse_user и parse_session +- graph отчёт ruby-prof-а +- решил избавиться от лишних вызовов метода split +- метрика снизилась с 34 MB до 33 MB +- указанная проблема перестала быть главной точкой роста + ### Ваша находка №X - какой отчёт показал главную точку роста - как вы решили её оптимизировать diff --git a/task-2.rb b/task-2.rb index cdd545fc..b23995d1 100644 --- a/task-2.rb +++ b/task-2.rb @@ -17,9 +17,8 @@ def initialize(attributes:, sessions:) 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], @@ -27,9 +26,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], @@ -58,7 +56,7 @@ def work(filename = 'data.txt') cols = line.split(',') if cols[0] == 'user' - user = parse_user(line) + user = parse_user(cols) prev_user = current_user current_user = user @@ -72,7 +70,7 @@ def work(filename = 'data.txt') end if cols[0] == 'session' - session = parse_session(line) + session = parse_session(cols) user_sessions << session sessions << session @@ -212,23 +210,23 @@ def test_result ### ruby-prof # RubyProf.measure_mode = RubyProf::ALLOCATIONS # На этот раз профилируем не allocations, а объём памяти! -RubyProf.measure_mode = RubyProf::MEMORY - -result = RubyProf.profile do - work('data10000.txt') -end - -printer = RubyProf::FlatPrinter.new(result) -printer.print(File.open('ruby_prof_reports/flat.txt', 'w+')) - -# printer = RubyProf::DotPrinter.new(result) -# printer.print(File.open('ruby_prof_reports/graphviz.dot', 'w+')) - -printer = RubyProf::GraphHtmlPrinter.new(result) -printer.print(File.open('ruby_prof_reports/graph.html', 'w+')) - -printer = RubyProf::CallStackPrinter.new(result) -printer.print(File.open('ruby_prof_reports/callstack.html', 'w+')) - -printer = RubyProf::CallTreePrinter.new(result) -printer.print(path: 'ruby_prof_reports', profile: 'profile') +# RubyProf.measure_mode = RubyProf::MEMORY +# +# result = RubyProf.profile do +# work('data10000.txt') +# end +# +# printer = RubyProf::FlatPrinter.new(result) +# printer.print(File.open('ruby_prof_reports/flat.txt', 'w+')) +# +# # printer = RubyProf::DotPrinter.new(result) +# # printer.print(File.open('ruby_prof_reports/graphviz.dot', 'w+')) +# +# printer = RubyProf::GraphHtmlPrinter.new(result) +# printer.print(File.open('ruby_prof_reports/graph.html', 'w+')) +# +# printer = RubyProf::CallStackPrinter.new(result) +# printer.print(File.open('ruby_prof_reports/callstack.html', 'w+')) +# +# # printer = RubyProf::CallTreePrinter.new(result) +# # printer.print(path: 'ruby_prof_reports', profile: 'profile') From 9a306295b16231cc7ddbc728488ec7aad7851922 Mon Sep 17 00:00:00 2001 From: Beglov Sergey Date: Sat, 4 May 2024 16:00:14 +0300 Subject: [PATCH 07/10] Fix Date#parse problem --- case-study-template.md | 6 +++ task-2.rb | 86 ++++++++++++++++++++++++++---------------- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index eaa745e9..3ea46114 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -45,6 +45,12 @@ - метрика снизилась с 34 MB до 33 MB - указанная проблема перестала быть главной точкой роста +### Ваша находка №4 - Вызов `Date#parse` +- flat отчёт ruby-prof-а +- от парсинга даты можно вообще избавиться +- метрика снизилась с 33 MB до 31 MB +- указанная проблема перестала быть главной точкой роста + ### Ваша находка №X - какой отчёт показал главную точку роста - как вы решили её оптимизировать diff --git a/task-2.rb b/task-2.rb index b23995d1..bfb4fa56 100644 --- a/task-2.rb +++ b/task-2.rb @@ -15,6 +15,44 @@ def initialize(attributes:, sessions:) @attributes = attributes @sessions = sessions end + + def sessions_count + @sessions_count ||= @sessions.size + end + + def total_time + @total_time ||= sessions_time.sum.to_s + ' min.' + end + + def longest_session + @longest_session ||= sessions_time.max.to_s + ' min.' + end + + def browsers + @browsers ||= upcase_browsers.sort.join(', ') + end + + def used_ie? + @used_ie ||= upcase_browsers.any? { |b| b =~ /INTERNET EXPLORER/ } + end + + def always_used_chrome? + @always_used_chrome ||= upcase_browsers.all? { |b| b =~ /CHROME/ } + end + + def dates + @dates ||= @sessions.map { |s| s['date'].chomp }.sort!.reverse! + end + + private + + def sessions_time + @sessions_time ||= @sessions.map { |s| s['time'].to_i } + end + + def upcase_browsers + @upcase_browsers ||= @sessions.map { |s| s['browser'].upcase } + end end def parse_user(fields) @@ -119,39 +157,23 @@ def work(filename = 'data.txt') report['usersStats'] = {} - # Собираем количество сессий по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } - end - - # Собираем количество времени по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' } - end - - # Выбираем самую длинную сессию пользователя - collect_stats_from_users(report, users_objects) do |user| - { 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' } - end - - # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end - - # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end - - # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } - end - - # Даты сессий через запятую в обратном порядке в формате iso8601 collect_stats_from_users(report, users_objects) do |user| - { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } + { + # Собираем количество сессий по пользователям + 'sessionsCount' => user.sessions_count, + # Собираем количество времени по пользователям + 'totalTime' => user.total_time, + # Выбираем самую длинную сессию пользователя + 'longestSession' => user.longest_session, + # Браузеры пользователя через запятую + 'browsers' => user.browsers, + # Хоть раз использовал IE? + 'usedIE' => user.used_ie?, + # Всегда использовал только Chrome? + 'alwaysUsedChrome' => user.always_used_chrome?, + # Даты сессий через запятую в обратном порядке в формате iso8601 + 'dates' => user.dates + } end File.write('result.json', "#{report.to_json}\n") From 833ff24c13d2d070c12ad8496079470c5fe4feb8 Mon Sep 17 00:00:00 2001 From: Beglov Sergey Date: Sat, 4 May 2024 19:43:56 +0300 Subject: [PATCH 08/10] Streaming approach --- task-2.rb | 159 +++++++++++++++++++++++++++--------------------------- 1 file changed, 79 insertions(+), 80 deletions(-) diff --git a/task-2.rb b/task-2.rb index bfb4fa56..7a9c54ba 100644 --- a/task-2.rb +++ b/task-2.rb @@ -74,49 +74,29 @@ def parse_session(fields) } 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 write_user_stat(file, user) + user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" + file.write('"' + user_key + '": ') + json_item = { + # Собираем количество сессий по пользователям + 'sessionsCount' => user.sessions_count, + # Собираем количество времени по пользователям + 'totalTime' => user.total_time, + # Выбираем самую длинную сессию пользователя + 'longestSession' => user.longest_session, + # Браузеры пользователя через запятую + 'browsers' => user.browsers, + # Хоть раз использовал IE? + 'usedIE' => user.used_ie?, + # Всегда использовал только Chrome? + 'alwaysUsedChrome' => user.always_used_chrome?, + # Даты сессий через запятую в обратном порядке в формате iso8601 + 'dates' => user.dates + }.to_json + file.write(json_item) end def work(filename = 'data.txt') - users = [] - sessions = [] - users_objects = [] - user_sessions = [] - current_user = nil - prev_user = nil - - File.foreach(filename) do |line| - cols = line.split(',') - - if cols[0] == 'user' - user = parse_user(cols) - - prev_user = current_user - current_user = user - - if prev_user != nil - users_objects << User.new(attributes: prev_user, sessions: user_sessions) - user_sessions = [] - end - - users << user - end - - if cols[0] == 'session' - session = parse_session(cols) - - user_sessions << session - sessions << session - end - end - - users_objects << User.new(attributes: current_user, sessions: user_sessions) - # Отчёт в json # - Сколько всего юзеров + # - Сколько всего уникальных браузеров + @@ -131,52 +111,71 @@ def work(filename = 'data.txt') # - Хоть раз использовал IE? + # - Всегда использовал только Хром? + # - даты сессий в порядке убывания через запятую + + report = { + totalUsers: 0, + totalSessions: 0 + } + users = [] + sessions = [] + user_sessions = [] + uniqueBrowsers = Hash.new(0) + current_user = nil + prev_user = nil - report = {} + File.open('result.json', 'w') do |file| + file.puts('{') + file.puts('"usersStats": {') - report[:totalUsers] = users.count + File.foreach(filename) do |line| + cols = line.split(',') - # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end + if cols[0] == 'user' + report[:totalUsers] += 1 + user = parse_user(cols) + + prev_user = current_user + current_user = user + + if prev_user != nil + user = User.new(attributes: prev_user, sessions: user_sessions) + write_user_stat(file, user) + file.puts(",") + user_sessions = [] + end + + users << user + end + + if cols[0] == 'session' + report[:totalSessions] += 1 + session = parse_session(cols) + + browser = session['browser'] + uniqueBrowsers[browser] += 1 + + user_sessions << session + sessions << session + end + end + + user = User.new(attributes: current_user, sessions: user_sessions) + write_user_stat(file, user) + file.puts("\n},") + + ################################################# + file.puts(%Q("totalUsers": #{report[:totalUsers]},)) + file.puts(%Q("uniqueBrowsersCount": #{uniqueBrowsers.keys.size},)) + file.puts(%Q("totalSessions": #{report[:totalSessions]},)) + allBrowsers = uniqueBrowsers.keys + .map { |b| b.upcase } + .sort + .uniq + .join(',') + file.puts(%Q("allBrowsers": "#{allBrowsers}")) - report['uniqueBrowsersCount'] = uniqueBrowsers.count - - report['totalSessions'] = sessions.count - - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') - - report['usersStats'] = {} - - collect_stats_from_users(report, users_objects) do |user| - { - # Собираем количество сессий по пользователям - 'sessionsCount' => user.sessions_count, - # Собираем количество времени по пользователям - 'totalTime' => user.total_time, - # Выбираем самую длинную сессию пользователя - 'longestSession' => user.longest_session, - # Браузеры пользователя через запятую - 'browsers' => user.browsers, - # Хоть раз использовал IE? - 'usedIE' => user.used_ie?, - # Всегда использовал только Chrome? - 'alwaysUsedChrome' => user.always_used_chrome?, - # Даты сессий через запятую в обратном порядке в формате iso8601 - 'dates' => user.dates - } + file.puts("}") end - File.write('result.json', "#{report.to_json}\n") puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end From 8fd4e8e23db8825b581a387ea739488cdb0ae60f Mon Sep 17 00:00:00 2001 From: Beglov Sergey Date: Mon, 6 May 2024 16:25:11 +0300 Subject: [PATCH 09/10] Fix split problem --- case-study-template.md | 6 +++++ task-2.rb | 58 ++++++++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 3ea46114..8141e9b0 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -51,6 +51,12 @@ - метрика снизилась с 33 MB до 31 MB - указанная проблема перестала быть главной точкой роста +### Ваша находка №5 - Вызов split при чтении строк из файла +- все отчеты указывают на это +- не присваивать переменной результат вызова метода, а передать ему блок +- метрика снизилась с 31 MB до 23 MB +- указанная проблема перестала быть главной точкой роста + ### Ваша находка №X - какой отчёт показал главную точку роста - как вы решили её оптимизировать diff --git a/task-2.rb b/task-2.rb index 7a9c54ba..48cc25f9 100644 --- a/task-2.rb +++ b/task-2.rb @@ -55,23 +55,32 @@ def upcase_browsers end end -def parse_user(fields) - { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], - } +USER_COLUMNS = { + 1 => 'id', 2 => 'first_name', 3 => 'last_name', 4 => 'age' +}.freeze + +SESSION_COLUMNS = { + 1 => 'user_id', 2 => 'session_id', 3 => 'browser', 4 => 'time', 5 => 'date' +}.freeze + +def parse_user(line) + user = {} + line.split(',').each_with_index do |item, i| + next if i == 0 + + user[USER_COLUMNS[i]] = item + end + user end -def parse_session(fields) - { - 'user_id' => fields[1], - 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[5], - } +def parse_session(line) + session = {} + line.split(',').each_with_index do |item, i| + next if i == 0 + + session[SESSION_COLUMNS[i]] = item + end + session end def write_user_stat(file, user) @@ -115,8 +124,6 @@ def work(filename = 'data.txt') totalUsers: 0, totalSessions: 0 } - users = [] - sessions = [] user_sessions = [] uniqueBrowsers = Hash.new(0) current_user = nil @@ -127,11 +134,9 @@ def work(filename = 'data.txt') file.puts('"usersStats": {') File.foreach(filename) do |line| - cols = line.split(',') - - if cols[0] == 'user' + if line.start_with?('user') report[:totalUsers] += 1 - user = parse_user(cols) + user = parse_user(line) prev_user = current_user current_user = user @@ -142,19 +147,16 @@ def work(filename = 'data.txt') file.puts(",") user_sessions = [] end - - users << user end - if cols[0] == 'session' + if line.start_with?('session') report[:totalSessions] += 1 - session = parse_session(cols) + session = parse_session(line) browser = session['browser'] uniqueBrowsers[browser] += 1 user_sessions << session - sessions << session end end @@ -219,7 +221,7 @@ def test_result ### memory_profiler # report = MemoryProfiler.report do -# work('data10000.txt') +# work('data40000.txt') # end # report.pretty_print(scale_bytes: true) @@ -234,7 +236,7 @@ def test_result # RubyProf.measure_mode = RubyProf::MEMORY # # result = RubyProf.profile do -# work('data10000.txt') +# work('data40000.txt') # end # # printer = RubyProf::FlatPrinter.new(result) From f6a7d374fbf29ea41e670e1e87de514da216d086 Mon Sep 17 00:00:00 2001 From: Beglov Sergey Date: Mon, 6 May 2024 16:49:08 +0300 Subject: [PATCH 10/10] Mission complete! --- case-study-template.md | 22 ++++++------------- task-2.rb | 48 ++++++++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 8141e9b0..3d105fb6 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -20,7 +20,7 @@ ## Feedback-Loop Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за несколько секунд. -Вот как я построил `feedback_loop`: поскольку в программе имелся тест проверяющий корректность работы и выводилось количество потребляемой памяти, то фактически feedback_loop уже имелся, оставалось только подобрать для него подходящий объем данных (я остановился на значении 10000 строк). +Вот как я построил `feedback_loop`: поскольку в программе имелся тест проверяющий корректность работы и выводилось количество потребляемой памяти, то фактически оставалось только подобрать подходящий объем данных, чтобы не приходилось долго ждать выполнения программы (я остановился на значении 10000 строк). ## Вникаем в детали системы, чтобы найти главные точки роста Для того, чтобы найти "точки роста" для оптимизации я воспользовался memory_profiler, stackprof и ruby-prof. @@ -33,41 +33,33 @@ - метрика снизилась с 89 MB до 47 MB - указанная проблема перестала быть главной точкой роста -### Ваша находка №2 - Создание для каждого пользователя нового массива с сессиями +### Находка №2 - Создание для каждого пользователя нового массива с сессиями - отчёт memory_profiler - сформировать массив users_objects в цикле считывания данных из файла - метрика снизилась с 47 MB до 34 MB - указанная проблема перестала быть главной точкой роста -### Ваша находка №3 - Избыточный вызов метода split в методах parse_user и parse_session +### Находка №3 - Избыточный вызов метода split в методах parse_user и parse_session - graph отчёт ruby-prof-а - решил избавиться от лишних вызовов метода split - метрика снизилась с 34 MB до 33 MB - указанная проблема перестала быть главной точкой роста -### Ваша находка №4 - Вызов `Date#parse` +### Находка №4 - Вызов `Date#parse` - flat отчёт ruby-prof-а - от парсинга даты можно вообще избавиться - метрика снизилась с 33 MB до 31 MB - указанная проблема перестала быть главной точкой роста -### Ваша находка №5 - Вызов split при чтении строк из файла +### Находка №5 - Вызов split при чтении строк из файла - все отчеты указывают на это - не присваивать переменной результат вызова метода, а передать ему блок - метрика снизилась с 31 MB до 23 MB - указанная проблема перестала быть главной точкой роста -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. - -*Какими ещё результами можете поделиться* +Удалось улучшить метрику системы до 23 MB при обработке файла любого размера и уложиться в заданный бюджет. ## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы я написал тест который проверяет что программа потребляет не более 23 MB памяти. diff --git a/task-2.rb b/task-2.rb index 48cc25f9..373fc20a 100644 --- a/task-2.rb +++ b/task-2.rb @@ -3,6 +3,7 @@ require 'json' require 'pry' require 'date' +require 'benchmark' require 'stackprof' require 'ruby-prof' require 'memory_profiler' @@ -86,7 +87,7 @@ def parse_session(line) def write_user_stat(file, user) user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" file.write('"' + user_key + '": ') - json_item = { + user_stat = { # Собираем количество сессий по пользователям 'sessionsCount' => user.sessions_count, # Собираем количество времени по пользователям @@ -101,8 +102,8 @@ def write_user_stat(file, user) 'alwaysUsedChrome' => user.always_used_chrome?, # Даты сессий через запятую в обратном порядке в формате iso8601 'dates' => user.dates - }.to_json - file.write(json_item) + } + file.write(user_stat.to_json) end def work(filename = 'data.txt') @@ -120,14 +121,12 @@ def work(filename = 'data.txt') # - Хоть раз использовал IE? + # - Всегда использовал только Хром? + # - даты сессий в порядке убывания через запятую + - report = { - totalUsers: 0, - totalSessions: 0 - } - user_sessions = [] - uniqueBrowsers = Hash.new(0) + total_users = 0 + total_sessions = 0 + unique_browsers = Hash.new(0) current_user = nil prev_user = nil + user_sessions = [] File.open('result.json', 'w') do |file| file.puts('{') @@ -135,7 +134,7 @@ def work(filename = 'data.txt') File.foreach(filename) do |line| if line.start_with?('user') - report[:totalUsers] += 1 + total_users += 1 user = parse_user(line) prev_user = current_user @@ -150,11 +149,10 @@ def work(filename = 'data.txt') end if line.start_with?('session') - report[:totalSessions] += 1 + total_sessions += 1 session = parse_session(line) - browser = session['browser'] - uniqueBrowsers[browser] += 1 + unique_browsers[session['browser']] += 1 user_sessions << session end @@ -165,15 +163,15 @@ def work(filename = 'data.txt') file.puts("\n},") ################################################# - file.puts(%Q("totalUsers": #{report[:totalUsers]},)) - file.puts(%Q("uniqueBrowsersCount": #{uniqueBrowsers.keys.size},)) - file.puts(%Q("totalSessions": #{report[:totalSessions]},)) - allBrowsers = uniqueBrowsers.keys - .map { |b| b.upcase } - .sort + file.puts(%Q("totalUsers": #{total_users},)) + file.puts(%Q("uniqueBrowsersCount": #{unique_browsers.keys.size},)) + file.puts(%Q("totalSessions": #{total_sessions},)) + all_browsers = unique_browsers.keys + .map! { |b| b.upcase } + .sort! .uniq .join(',') - file.puts(%Q("allBrowsers": "#{allBrowsers}")) + file.puts(%Q("allBrowsers": "#{all_browsers}")) file.puts("}") end @@ -211,6 +209,12 @@ def test_result 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 + + def test_memory + work + memory = `ps -o rss= -p #{Process.pid}`.to_i / 1024 + assert memory <= 23, "The Ruby method took more than 23 MB memory" + end end @@ -218,6 +222,10 @@ def test_result # work('data10000.txt') # work('data40000.txt') # work('data500000.txt') +# time = Benchmark.realtime do +# work('data_large.txt') +# end +# puts "Work data_large finish in #{time.round(2)}" ### memory_profiler # report = MemoryProfiler.report do