From 8ba34cf7b3cdde8ac78f126b9ff289c7b59ec4e5 Mon Sep 17 00:00:00 2001 From: anna Date: Wed, 29 Jan 2025 18:43:50 +0300 Subject: [PATCH 1/8] First measurement --- .ruby-version | 1 + case-study.md | 65 +++++++++++++++++++++ task-2.rb | 154 ++++++++++++++++++++------------------------------ 3 files changed, 128 insertions(+), 92 deletions(-) create mode 100644 .ruby-version create mode 100644 case-study.md diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..9c25013d --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.6 diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..c3373d37 --- /dev/null +++ b/case-study.md @@ -0,0 +1,65 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумала использовать такую метрику: + +Измерила (вариант с изменениями из первого задания) с помощью предложенной команды: + +`puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)` + +Результат в начале: + +``` +MEMORY USAGE: 2444 MB +``` + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: *как вы построили feedback_loop* + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика + +### Ваша находка №2 +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика + +### Ваша находка №X +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. + +*Какими ещё результами можете поделиться* + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* diff --git a/task-2.rb b/task-2.rb index 34e09a3c..ab0c0544 100644 --- a/task-2.rb +++ b/task-2.rb @@ -14,45 +14,49 @@ def initialize(attributes:, sessions:) end end -def parse_user(user) - fields = user.split(',') - parsed_result = { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], +def parse_user(cols) + _, id, first_name, last_name, age = cols.split(',') + { + 'id' => id, + 'first_name' => first_name, + 'last_name' => last_name, + 'age' => age, } end -def parse_session(session) - fields = session.split(',') - parsed_result = { - 'user_id' => fields[1], - 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[5], +def parse_session(cols) + _, user_id, session_id, browser, time, date = cols.split(',') + { + 'user_id' => user_id, + 'session_id' => session_id, + 'browser' => browser, + 'time' => time, + 'date' => date, } end -def collect_stats_from_users(report, users_objects, &block) - users_objects.each do |user| - user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" - report['usersStats'][user_key] ||= {} - report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) - end -end - -def work - file_lines = File.read('data.txt').split("\n") +def work(filename = 'data.txt') + report = {} - users = [] - sessions = [] + current_user = nil + uniqueBrowsers = Set.new + totalSessions = 0 + user_object = nil + users_objects = [] - file_lines.each do |line| + File.readlines(filename, chomp: true).each 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] == 'user' + current_user = parse_user(line) + user_object = User.new(attributes: current_user, sessions: []) + users_objects.push user_object + elsif cols[0] == 'session' + session = parse_session(line) + user_object.sessions.push session + + totalSessions += 1 + uniqueBrowsers.add(session['browser'].upcase) + end end # Отчёт в json @@ -70,80 +74,44 @@ def work # - Всегда использовал только Хром? + # - даты сессий в порядке убывания через запятую + - report = {} - - report[:totalUsers] = users.count + report['totalUsers'] = users_objects.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['totalSessions'] = totalSessions + report['allBrowsers'] = uniqueBrowsers.sort.join(',') 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 + cached_dates = {} - # Хоть раз использовал 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 } } + users_objects.each do |user| + user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + + times = user.sessions.map { |s| s['time'].to_i } + browsers = user.sessions.map { |s| s['browser'].upcase } + + dates = user.sessions.map do |session| + cached_dates[session['date']] ||= Date.parse(session['date']) + cached_dates[session['date']] + end + + report['usersStats'][user_key] = { + 'sessionsCount' => user.sessions.count, + 'totalTime' => "#{times.sum.to_s} min.", + 'longestSession' => "#{times.max.to_s} min.", + 'browsers' => browsers.sort.join(', '), + 'usedIE' => browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, + 'alwaysUsedChrome' => browsers.all? { |b| b =~ /CHROME/ }, + 'dates' => dates.sort.reverse.map { |d| d.iso8601 } + } end File.write('result.json', "#{report.to_json}\n") - puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end +work('data_large.txt') + class TestMe < Minitest::Test def setup File.write('result.json', '') @@ -175,3 +143,5 @@ def test_result assert_equal expected_result, JSON.parse(File.read('result.json')) end end + +puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) From 65b1df8df4aa9c8ac63ddcf6effffc6824f664ae Mon Sep 17 00:00:00 2001 From: anna Date: Sat, 1 Feb 2025 17:26:25 +0300 Subject: [PATCH 2/8] Optimize to stream json --- .gitignore | 3 ++ Gemfile | 5 +++ Gemfile.lock | 23 +++++++++++ case-study.md | 36 ++++++++++++++++-- task-2.rb | 103 +++++++++++++++++++++++++++++++------------------- 5 files changed, 128 insertions(+), 42 deletions(-) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 Gemfile.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..56e30085 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +data_large.txt +*.json +test.rb diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..473cefa0 --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +ruby '3.3.6' +source "https://rubygems.org" + +gem 'minitest' +gem 'ruby-prof' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..10b5ab0b --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,23 @@ +GEM + remote: https://rubygems.org/ + specs: + json-write-stream (2.0.0) + json_pure (~> 1.8.0) + json_pure (1.8.6) + minitest (5.25.4) + ruby-prof (1.7.1) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + json-write-stream + minitest + ruby-prof + +RUBY VERSION + ruby 3.3.6p108 + +BUNDLED WITH + 2.5.22 diff --git a/case-study.md b/case-study.md index c3373d37..1c0512c9 100644 --- a/case-study.md +++ b/case-study.md @@ -12,7 +12,7 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумала использовать такую метрику: +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумала использовать такую метрику: Измерила (вариант с изменениями из первого задания) с помощью предложенной команды: @@ -21,22 +21,52 @@ Результат в начале: ``` +# с GC + MEMORY USAGE: 2444 MB + +# без GC + +MEMORY USAGE: 5924 MB ``` ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроила эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 35 секунд. + + +Вот как я построил `feedback_loop`: + +Прописала, чтобы оценивать кол-во памяти: +```ruby +puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) +``` + +На этапе отладки потокового варианта использовала sample, к-й позволял оценить работоспособность в пределах пары секунд. +Далее использовала простой вывод, чтобы оценивать в пределах 35 секунд, либо sample, чтобы делать это быстрее. -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* ## Вникаем в детали системы, чтобы найти главные точки роста Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* Вот какие проблемы удалось найти и решить +### Ваша находка №0 + +Переход на потоковую обработку. + +По времени программа стала работать медленнее (25с => 33-34c , с gc) + +По памяти произошло улучшение: + +`MEMORY USAGE: 2444 MB` +=> +`MEMORY USAGE: 332 MB` + +Думала использовать `gem 'json-write-stream'` , но неудобно писать вложенный json для `usersStats`, поэтому решила напрямую + ### Ваша находка №1 - какой отчёт показал главную точку роста - как вы решили её оптимизировать diff --git a/task-2.rb b/task-2.rb index ab0c0544..2c61034a 100644 --- a/task-2.rb +++ b/task-2.rb @@ -2,7 +2,6 @@ require 'json' require 'pry' -require 'date' require 'minitest/autorun' class User @@ -35,30 +34,74 @@ def parse_session(cols) } end -def work(filename = 'data.txt') +def write_user_to_json(file, user, first_user: false) + # cached_dates = {} + + user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + + times = user.sessions.map { |s| s['time'].to_i } + browsers = user.sessions.map { |s| s['browser'].upcase } + + dates = user.sessions.map { |s| s['date'] } + + File.open file, "a" do |f| + f.write ',' unless first_user + str = <<-JSON + \"#{user_key}\": { + \"sessionsCount\": #{user.sessions.count}, + \"totalTime\": "#{times.sum.to_s} min.", + \"longestSession\": "#{times.max.to_s} min.", + \"browsers\": "#{browsers.sort.join(', ')}", + \"usedIE\": #{browsers.any? { |b| b =~ /INTERNET EXPLORER/ }}, + \"alwaysUsedChrome\": #{browsers.all? { |b| b =~ /CHROME/ }}, + \"dates\": #{dates.sort.reverse} + } + JSON + + f.write str + end + +end + +def work(filename = 'data.txt', gc: true, result: 'result.json') + GC.disable unless gc + report = {} current_user = nil uniqueBrowsers = Set.new totalSessions = 0 user_object = nil - users_objects = [] + totalUsers = 0 + # to see if we need a comma + first_user = true + + File.open(result, 'a') { |file| file.write("{ \"usersStats\":{") } File.readlines(filename, chomp: true).each do |line| cols = line.split(',') if cols[0] == 'user' - current_user = parse_user(line) - user_object = User.new(attributes: current_user, sessions: []) - users_objects.push user_object + # write previous user + if current_user + write_user_to_json(result, current_user, first_user: first_user) + first_user = false + end + parsed_user = parse_user(line) + current_user = User.new(attributes: parsed_user, sessions: []) + totalUsers += 1 elsif cols[0] == 'session' session = parse_session(line) - user_object.sessions.push session + current_user.sessions.push session totalSessions += 1 uniqueBrowsers.add(session['browser'].upcase) end end + write_user_to_json(result, current_user, first_user: false) + + File.open(result, 'a') { |file| file.write("},") } + # Отчёт в json # - Сколько всего юзеров + # - Сколько всего уникальных браузеров + @@ -74,43 +117,26 @@ def work(filename = 'data.txt') # - Всегда использовал только Хром? + # - даты сессий в порядке убывания через запятую + - report['totalUsers'] = users_objects.count - # Подсчёт количества уникальных браузеров - report['uniqueBrowsersCount'] = uniqueBrowsers.count - report['totalSessions'] = totalSessions - report['allBrowsers'] = uniqueBrowsers.sort.join(',') - - report['usersStats'] = {} - cached_dates = {} - - users_objects.each do |user| - user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + File.open result, "a" do |f| + f.write "\"uniqueBrowsersCount\": #{uniqueBrowsers.count}," + f.write "\"totalSessions\": #{totalSessions}," + f.write "\"allBrowsers\": \"#{uniqueBrowsers.sort.join(',')}\"," + f.write "\"totalUsers\": #{totalUsers}" + f.write("}") + end +end - times = user.sessions.map { |s| s['time'].to_i } - browsers = user.sessions.map { |s| s['browser'].upcase } +time = Time.now - dates = user.sessions.map do |session| - cached_dates[session['date']] ||= Date.parse(session['date']) - cached_dates[session['date']] - end +work('data_large.txt', gc: true) - report['usersStats'][user_key] = { - 'sessionsCount' => user.sessions.count, - 'totalTime' => "#{times.sum.to_s} min.", - 'longestSession' => "#{times.max.to_s} min.", - 'browsers' => browsers.sort.join(', '), - 'usedIE' => browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, - 'alwaysUsedChrome' => browsers.all? { |b| b =~ /CHROME/ }, - 'dates' => dates.sort.reverse.map { |d| d.iso8601 } - } - end +after = Time.now - File.write('result.json', "#{report.to_json}\n") -end +p after - time -work('data_large.txt') +puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) class TestMe < Minitest::Test def setup @@ -140,8 +166,7 @@ def setup def test_result work expected_result = JSON.parse('{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}') + res = JSON.parse(File.read('result.json')) assert_equal expected_result, JSON.parse(File.read('result.json')) end end - -puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) From 1ac9afd5a65d434fda849532e1f8bb4dcdd7e05f Mon Sep 17 00:00:00 2001 From: anna Date: Sun, 2 Feb 2025 17:53:08 +0300 Subject: [PATCH 3/8] Tried to profile and optimize memory --- Gemfile | 2 + Gemfile.lock | 12 ++-- README.md | 1 - case-study.md | 56 +++++++++++++++- task-2.rb | 181 ++++++++------------------------------------------ work.rb | 124 ++++++++++++++++++++++++++++++++++ 6 files changed, 215 insertions(+), 161 deletions(-) create mode 100644 work.rb diff --git a/Gemfile b/Gemfile index 473cefa0..abe4f17e 100644 --- a/Gemfile +++ b/Gemfile @@ -3,3 +3,5 @@ source "https://rubygems.org" gem 'minitest' gem 'ruby-prof' +gem 'memory_profiler' +gem 'pry' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 10b5ab0b..22bd5fe0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,13 @@ GEM remote: https://rubygems.org/ specs: - json-write-stream (2.0.0) - json_pure (~> 1.8.0) - json_pure (1.8.6) + coderay (1.1.3) + memory_profiler (1.1.0) + method_source (1.1.0) minitest (5.25.4) + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) ruby-prof (1.7.1) PLATFORMS @@ -12,8 +15,9 @@ PLATFORMS x86_64-linux DEPENDENCIES - json-write-stream + memory_profiler minitest + pry ruby-prof RUBY VERSION diff --git a/README.md b/README.md index d73dc702..03037cf3 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ Можем считать, что все сессии юзера всегда идут одним непрерывным куском. Нет такого, что сначала идёт часть сессий юзера, потом сессии другого юзера, и потом снова сессии первого. - ## План работы В этот раз переработка потребуется кардинальная, так что нужно сделать два этапа diff --git a/case-study.md b/case-study.md index 1c0512c9..bfc96034 100644 --- a/case-study.md +++ b/case-study.md @@ -45,8 +45,13 @@ puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) ``` На этапе отладки потокового варианта использовала sample, к-й позволял оценить работоспособность в пределах пары секунд. -Далее использовала простой вывод, чтобы оценивать в пределах 35 секунд, либо sample, чтобы делать это быстрее. +Далее подобрала sample, с к-м фидбек можно было получить за n секунд. + + +Далее использовала простой вывод, чтобы оценивать в пределах 35 секунд. + +Либо sample на 1_000_000 строк, тогда в пределах 7 секунд. ## Вникаем в детали системы, чтобы найти главные точки роста Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* @@ -68,6 +73,55 @@ puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) Думала использовать `gem 'json-write-stream'` , но неудобно писать вложенный json для `usersStats`, поэтому решила напрямую ### Ваша находка №1 + +memory profiler показал ,что File очень много ест памяти, но хз, как от него избавиться! + +Ещё String, попробую от неё избавиться. Тут ничего не изменилось. + +Также посмотрела qcachegrind (у меня kcachegrind) - много отнимают `write_user_to_json` , `Array#each`. + + +Также создала второй тред, вот его отчёт: + +``` +MEMORY USAGE: 29 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +22.006778048 +``` + + + + +Подумала, раз `File`, то не открывать/закрывать его для каждого юзера, а открыть на запись 1 раз. + +Переписала - по памяти ничего не поменялось, но программа стала быстрее отрабатывать по рвемени. + + + + + + + - какой отчёт показал главную точку роста - как вы решили её оптимизировать - как изменилась метрика diff --git a/task-2.rb b/task-2.rb index 2c61034a..118bfb91 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,172 +1,43 @@ # Deoptimized version of homework task require 'json' +# require 'minitest/autorun' +require 'memory_profiler' +require 'ruby-prof' +require './work' require 'pry' -require 'minitest/autorun' -class User - attr_reader :attributes, :sessions - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions - end -end - -def parse_user(cols) - _, id, first_name, last_name, age = cols.split(',') - { - 'id' => id, - 'first_name' => first_name, - 'last_name' => last_name, - 'age' => age, - } -end - -def parse_session(cols) - _, user_id, session_id, browser, time, date = cols.split(',') - { - 'user_id' => user_id, - 'session_id' => session_id, - 'browser' => browser, - 'time' => time, - 'date' => date, - } -end - -def write_user_to_json(file, user, first_user: false) - # cached_dates = {} +# result = RubyProf.profile do +# end - user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" +# На этот раз профилируем не allocations, а объём памяти! +# RubyProf.measure_mode = RubyProf::MEMORY - times = user.sessions.map { |s| s['time'].to_i } - browsers = user.sessions.map { |s| s['browser'].upcase } +# printer = RubyProf::CallTreePrinter.new(result) +# printer.print(path: 'ruby_prof_reports', profile: 'profile') - dates = user.sessions.map { |s| s['date'] } - - File.open file, "a" do |f| - f.write ',' unless first_user - str = <<-JSON - \"#{user_key}\": { - \"sessionsCount\": #{user.sessions.count}, - \"totalTime\": "#{times.sum.to_s} min.", - \"longestSession\": "#{times.max.to_s} min.", - \"browsers\": "#{browsers.sort.join(', ')}", - \"usedIE\": #{browsers.any? { |b| b =~ /INTERNET EXPLORER/ }}, - \"alwaysUsedChrome\": #{browsers.all? { |b| b =~ /CHROME/ }}, - \"dates\": #{dates.sort.reverse} - } - JSON - - f.write str - end +# report = MemoryProfiler.report do +# work('data_large_sample.txt', gc: true) +# end +# report.pretty_print(scale_bytes: true) +# work('data_large.txt', gc: true) +thread1 = Thread.new do + p "start" + time = Time.now + work('data_large.txt', gc: true) + after = Time.now + p after - time end -def work(filename = 'data.txt', gc: true, result: 'result.json') - GC.disable unless gc - - report = {} - - current_user = nil - uniqueBrowsers = Set.new - totalSessions = 0 - user_object = nil - totalUsers = 0 - # to see if we need a comma - first_user = true - - File.open(result, 'a') { |file| file.write("{ \"usersStats\":{") } - - File.readlines(filename, chomp: true).each do |line| - cols = line.split(',') - if cols[0] == 'user' - # write previous user - if current_user - write_user_to_json(result, current_user, first_user: first_user) - first_user = false - end - parsed_user = parse_user(line) - current_user = User.new(attributes: parsed_user, sessions: []) - totalUsers += 1 - elsif cols[0] == 'session' - session = parse_session(line) - current_user.sessions.push session - - totalSessions += 1 - uniqueBrowsers.add(session['browser'].upcase) - end - end - - write_user_to_json(result, current_user, first_user: false) - - File.open(result, 'a') { |file| file.write("},") } - - # Отчёт в json - # - Сколько всего юзеров + - # - Сколько всего уникальных браузеров + - # - Сколько всего сессий + - # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + - # - # - По каждому пользователю - # - сколько всего сессий + - # - сколько всего времени + - # - самая длинная сессия + - # - браузеры через запятую + - # - Хоть раз использовал IE? + - # - Всегда использовал только Хром? + - # - даты сессий в порядке убывания через запятую + - - # Подсчёт количества уникальных браузеров - - File.open result, "a" do |f| - f.write "\"uniqueBrowsersCount\": #{uniqueBrowsers.count}," - f.write "\"totalSessions\": #{totalSessions}," - f.write "\"allBrowsers\": \"#{uniqueBrowsers.sort.join(',')}\"," - f.write "\"totalUsers\": #{totalUsers}" - f.write("}") +Thread.new do + loop do + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + sleep 1 end end -time = Time.now +thread1.join -work('data_large.txt', gc: true) -after = Time.now - -p after - time - -puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) - -class TestMe < Minitest::Test - def setup - File.write('result.json', '') - File.write('data.txt', -'user,0,Leida,Cira,0 -session,0,0,Safari 29,87,2016-10-23 -session,0,1,Firefox 12,118,2017-02-27 -session,0,2,Internet Explorer 28,31,2017-03-28 -session,0,3,Internet Explorer 28,109,2016-09-15 -session,0,4,Safari 39,104,2017-09-27 -session,0,5,Internet Explorer 35,6,2016-09-01 -user,1,Palmer,Katrina,65 -session,1,0,Safari 17,12,2016-10-21 -session,1,1,Firefox 32,3,2016-12-20 -session,1,2,Chrome 6,59,2016-11-11 -session,1,3,Internet Explorer 10,28,2017-04-29 -session,1,4,Chrome 13,116,2016-12-28 -user,2,Gregory,Santos,86 -session,2,0,Chrome 35,6,2018-09-21 -session,2,1,Safari 49,85,2017-05-22 -session,2,2,Firefox 47,17,2018-02-02 -session,2,3,Chrome 20,84,2016-11-25 -') - end - - def test_result - work - expected_result = 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"]}}}') - res = JSON.parse(File.read('result.json')) - assert_equal expected_result, JSON.parse(File.read('result.json')) - end -end diff --git a/work.rb b/work.rb new file mode 100644 index 00000000..16ff1508 --- /dev/null +++ b/work.rb @@ -0,0 +1,124 @@ +class User + attr_reader :attributes, :sessions + + def initialize(attributes:, sessions:) + @attributes = attributes + @sessions = sessions + end +end + +def parse_user(cols) + _, id, first_name, last_name, age = cols.split(',') + { + 'id' => id, + 'first_name' => first_name, + 'last_name' => last_name, + 'age' => age, + } +end + +def parse_session(cols) + _, user_id, session_id, browser, time, date = cols.split(',') + { + 'user_id' => user_id, + 'session_id' => session_id, + 'browser' => browser, + 'time' => time, + 'date' => date, + } +end + +def write_user_to_json(f, user, first_user: false) + user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + + times = user.sessions.map { |s| s['time'].to_i } + browsers = user.sessions.map { |s| s['browser'].upcase } + + dates = user.sessions.map { |s| s['date'] } + + # File.open file, "a" do |f| + f.write ',' unless first_user + f.write <<-JSON + \"#{user_key}\": { + \"sessionsCount\": #{user.sessions.count}, + \"totalTime\": "#{times.sum.to_s} min.", + \"longestSession\": "#{times.max.to_s} min.", + \"browsers\": "#{browsers.sort.join(', ')}", + \"usedIE\": #{browsers.any? { |b| b =~ /INTERNET EXPLORER/ }}, + \"alwaysUsedChrome\": #{browsers.all? { |b| b =~ /CHROME/ }}, + \"dates\": #{dates.sort.reverse} + } + JSON + # end +end + +def work(filename = 'data.txt', gc: true, result: 'result.json') + GC.disable unless gc + + current_user = nil + uniqueBrowsers = Set.new + totalSessions = 0 + totalUsers = 0 + # to see if we need a comma + first_user = true + + File.open(result, 'a') do |f| + f.write("{ \"usersStats\":{") + + File.readlines(filename, chomp: true).each do |line| + cols = line.split(',') + if cols[0] == 'user' + # write previous user + if current_user + write_user_to_json(f, current_user, first_user: first_user) + first_user = false + end + parsed_user = parse_user(line) + current_user = User.new(attributes: parsed_user, sessions: []) + totalUsers += 1 + elsif cols[0] == 'session' + session = parse_session(line) + current_user.sessions.push session + + totalSessions += 1 + uniqueBrowsers.add(session['browser'].upcase) + end + end + write_user_to_json(f, current_user, first_user: false) + + f.write("},") + + f.write "\"uniqueBrowsersCount\": #{uniqueBrowsers.count}," + f.write "\"totalSessions\": #{totalSessions}," + f.write "\"allBrowsers\": \"#{uniqueBrowsers.sort.join(',')}\"," + f.write "\"totalUsers\": #{totalUsers}" + f.write("}") + end + + + + # Отчёт в json + # - Сколько всего юзеров + + # - Сколько всего уникальных браузеров + + # - Сколько всего сессий + + # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + + # + # - По каждому пользователю + # - сколько всего сессий + + # - сколько всего времени + + # - самая длинная сессия + + # - браузеры через запятую + + # - Хоть раз использовал IE? + + # - Всегда использовал только Хром? + + # - даты сессий в порядке убывания через запятую + + + # Подсчёт количества уникальных браузеров + + # File.open result, "a" do |f| + # f.write "\"uniqueBrowsersCount\": #{uniqueBrowsers.count}," + # f.write "\"totalSessions\": #{totalSessions}," + # f.write "\"allBrowsers\": \"#{uniqueBrowsers.sort.join(',')}\"," + # f.write "\"totalUsers\": #{totalUsers}" + # f.write("}") + # end +end \ No newline at end of file From 34cc87c8c5d064e7cdd74116beb713f269905a27 Mon Sep 17 00:00:00 2001 From: anna Date: Fri, 7 Feb 2025 08:55:32 +0300 Subject: [PATCH 4/8] Optimize memory by minimizing split --- case-study.md | 69 ++++++++++++++++++++++++------- draft.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++ work.rb | 68 +++++++++++++++++++------------ 3 files changed, 209 insertions(+), 39 deletions(-) create mode 100644 draft.md diff --git a/case-study.md b/case-study.md index bfc96034..aec5986d 100644 --- a/case-study.md +++ b/case-study.md @@ -76,11 +76,8 @@ puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) memory profiler показал ,что File очень много ест памяти, но хз, как от него избавиться! -Ещё String, попробую от неё избавиться. Тут ничего не изменилось. - Также посмотрела qcachegrind (у меня kcachegrind) - много отнимают `write_user_to_json` , `Array#each`. - Также создала второй тред, вот его отчёт: ``` @@ -110,28 +107,72 @@ MEMORY USAGE: 334 MB ``` +Подумала, раз `File`, то не открывать/закрывать его для каждого юзера, а открыть на запись 1 раз. +Переписала - по памяти ничего не поменялось, но программа стала быстрее отрабатывать (27 => 21 секунд, если проверять на большом файле без профилирования) -Подумала, раз `File`, то не открывать/закрывать его для каждого юзера, а открыть на запись 1 раз. +### Ваша находка №2 -Переписала - по памяти ничего не поменялось, но программа стала быстрее отрабатывать по рвемени. +Далее снова посмотрела с помощью memory_profiler: +Увидела, что String ест много памяти, причём на месте `split(',')` +Заметила, что при проверке строки "юзер" или "сессия" вообще необязательно split делать. Но также заметила, что split делается 2 раза для каждой строки: для проверки user / session и для парсинга юзера/сессии. +Сначала переписала на разовый `split` и передачу `cols` в `parse_session` и `parse_user`. +```ruby +cols = line.split(',') +... +parse_user(cols) +``` +Использование памяти на sample снизилось с: +``` +Total allocated: 179.51 MB (2546193 objects) +``` +До: +``` +Total allocated: 131.29 MB (1761624 objects) +``` -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +Далее избавилась от массива в `split` (у меня это уже было в предыдущем варианте, заметила, что вариант с набором переменных быстрее отрабатывает). ++ избавилась от методов пока что `parse_session`, `parse_user`. + +```ruby +_, user_id, session_id, browser, time, date = cols.split(',') + +session = { + 'user_id' => user_id, + 'session_id' => session_id, + 'browser' => browser, + 'time' => time, + 'date' => date, +} +``` + +Использование памяти на сэмпле снизилось до: +``` +Total allocated: 51.71 MB (854936 objects) +``` + +Проверяю основную метрику на большом файле: + +``` +MEMORY USAGE: 33 MB +MEMORY USAGE: 327 MB +MEMORY USAGE: 327 MB +MEMORY USAGE: 327 MB +MEMORY USAGE: 327 MB +MEMORY USAGE: 327 MB +MEMORY USAGE: 327 MB +MEMORY USAGE: 327 MB +7.241316391 +``` + +Общий объём использованной памяти немного снизился + программа стала отрабатывать намного быстрее! -### Ваша находка №2 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика ### Ваша находка №X - какой отчёт показал главную точку роста diff --git a/draft.md b/draft.md new file mode 100644 index 00000000..bb09006b --- /dev/null +++ b/draft.md @@ -0,0 +1,111 @@ + + + +На sample до оптимизации split: + +Total allocated: 179.51 MB (2546193 objects) +Total retained: 0 B (0 objects) + +cols + +---------- + +после передачи cols: + +Total allocated: 131.29 MB (1761624 objects) +Total retained: 0 B (0 objects) + + + +-------------------- +Перепишу без массива (набор переменных): + +anna@composaurus:~/apps/rails-optimization/rails-optimization-task2$ be ruby task-2.rb +Total allocated: 51.71 MB (854936 objects) +Total retained: 0 B (0 objects) + +О, круто! + + +------------------------------- +Из первого отчёта (по 100_000 строк) + + +Total allocated: 312.22 MB (2609218 objects) +Total retained: 6.58 kB (48 objects) + + +Вот хз, Array#each да readlines много занимают. + +Io.open ещё. + + +allocated memory by gem +----------------------------------- + 312.10 MB other + 121.74 kB set + 295.00 B bundled_gems + +allocated memory by file +----------------------------------- + 312.10 MB task-2.rb + 121.74 kB /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb + 295.00 B /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/bundled_gems.rb + +allocated memory by location +----------------------------------- + 131.47 MB task-2.rb:47 + 48.22 MB task-2.rb:79 + 41.43 MB task-2.rb:27 + 13.53 MB task-2.rb:28 + 9.45 MB task-2.rb:57 + 9.32 MB task-2.rb:49 + 9.03 MB task-2.rb:78 + 7.89 MB task-2.rb:87 + 6.79 MB task-2.rb:17 + 5.52 MB task-2.rb:43 + 5.26 MB task-2.rb:54 + 4.22 MB task-2.rb:94 + 4.00 MB task-2.rb:80 + 3.38 MB task-2.rb:89 + 2.70 MB task-2.rb:55 + 2.47 MB task-2.rb:18 + 1.73 MB task-2.rb:48 + 1.30 MB task-2.rb:42 + 1.30 MB task-2.rb:45 + 1.03 MB task-2.rb:56 + 747.60 kB task-2.rb:40 + 617.24 kB task-2.rb:52 + 617.24 kB task-2.rb:53 + 100.15 kB /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb:0 + 61.56 kB task-2.rb:51 + 10.00 kB /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb:512 + 8.67 kB task-2.rb:76 + 8.63 kB task-2.rb:100 + 8.52 kB task-2.rb:119 + 7.33 kB /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb:244 + 5.08 kB task-2.rb:122 + 3.86 kB /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb:218 + 400.00 B /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/set.rb:852 + 295.00 B /home/anna/.rbenv/versions/3.3.6/lib/ruby/3.3.0/bundled_gems.rb:69 + 192.00 B task-2.rb:120 + 120.00 B task-2.rb:121 + 120.00 B task-2.rb:123 + 112.00 B task-2.rb:70 + 80.00 B task-2.rb:131 + 72.00 B task-2.rb:61 + 40.00 B task-2.rb:124 + 40.00 B task-2.rb:66 + +allocated memory by class +----------------------------------- + 130.28 MB File + 102.99 MB String + 53.61 MB Array + 20.95 MB Hash + 2.66 MB MatchData + 1.11 MB Thread::Mutex + 617.24 kB User + 3.86 kB Class + 40.00 B Range + 40.00 B Set diff --git a/work.rb b/work.rb index 16ff1508..49884d70 100644 --- a/work.rb +++ b/work.rb @@ -7,26 +7,26 @@ def initialize(attributes:, sessions:) end end -def parse_user(cols) - _, id, first_name, last_name, age = cols.split(',') - { - 'id' => id, - 'first_name' => first_name, - 'last_name' => last_name, - 'age' => age, - } -end - -def parse_session(cols) - _, user_id, session_id, browser, time, date = cols.split(',') - { - 'user_id' => user_id, - 'session_id' => session_id, - 'browser' => browser, - 'time' => time, - 'date' => date, - } -end +# def parse_user(cols) +# # _, id, first_name, last_name, age = cols.split(',') +# { +# 'id' => cols[1], +# 'first_name' => cols[2], +# 'last_name' => cols[3], +# 'age' => cols[4], +# } +# end + +# def parse_session(cols) +# # _, user_id, session_id, browser, time, date = cols.split(',') +# { +# 'user_id' => cols[1], +# 'session_id' => cols[2], +# 'browser' => cols[3], +# 'time' => cols[4], +# 'date' => cols[5], +# } +# end def write_user_to_json(f, user, first_user: false) user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" @@ -66,18 +66,36 @@ def work(filename = 'data.txt', gc: true, result: 'result.json') f.write("{ \"usersStats\":{") File.readlines(filename, chomp: true).each do |line| - cols = line.split(',') - if cols[0] == 'user' + if line[0,4] == 'user' + _, id, first_name, last_name, age = line.split(',') + # write previous user if current_user write_user_to_json(f, current_user, first_user: first_user) first_user = false end - parsed_user = parse_user(line) + # parsed_user = parse_user(cols) + parsed_user = { + 'id' => id, + 'first_name' => first_name, + 'last_name' => last_name, + 'age' => age, + } + current_user = User.new(attributes: parsed_user, sessions: []) totalUsers += 1 - elsif cols[0] == 'session' - session = parse_session(line) + elsif line[0,6] == 'session' + _, user_id, session_id, browser, time, date = cols.split(',') + + session = { + 'user_id' => user_id, + 'session_id' => session_id, + 'browser' => browser, + 'time' => time, + 'date' => date, + } + + # session = parse_session(cols) current_user.sessions.push session totalSessions += 1 From cd2f2ed3d7ebd55a3448b904a9848ebea00f1992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=BD=D0=B0=20=D0=91=D1=83=D1=8F=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0?= Date: Fri, 7 Feb 2025 23:08:50 +0300 Subject: [PATCH 5/8] Kind of optimize... --- case-study.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++- measure.rb | 55 ++++++++++++++++++++++++++++++++ task-2.rb | 56 ++++++++++++++++++++------------ work.rb | 88 ++++++++++++++++----------------------------------- 4 files changed, 198 insertions(+), 82 deletions(-) create mode 100644 measure.rb diff --git a/case-study.md b/case-study.md index aec5986d..7533f3ff 100644 --- a/case-study.md +++ b/case-study.md @@ -173,8 +173,87 @@ MEMORY USAGE: 327 MB Общий объём использованной памяти немного снизился + программа стала отрабатывать намного быстрее! +### Ваша находка №3 + +Теперь ест на: + +`line[0,4] == 'user'` + +`line[0,7] == 'session'` + +Создала соотв. переменные заранее и сравниваю с ними. + +Решила проверить на сэмпле на 300_000 строк. + +До: +``` +Total allocated: 155.18 MB (2560691 objects) +``` + +После: + +``` +Total allocated: 133.02 MB (2006807 objects) +``` + +Ну пусть будет. + + +### Ваша находка номер 4 + +Теперь (кроме File): + +``` +current_user = User.new(attributes: parsed_user, sessions: []) +``` + + +Можно оптимизировать `User` => `OptimizedUser` ++ увидела, что не используются id и age, их убираем из объекта. + +Заменила на оптимизированного юзера без лишних полей: +``` +OptimizedUser = Struct.new(:full_name, :sessions, keyword_init: true) + +current_user = OptimizedUser.new(full_name: "#{first_name} #{last_name}", sessions: []) +``` + +Стало: + +``` +Total allocated: 126.35 MB (1730762 objects) +``` + +### Находка 5 + +Увидела, что у меня browser.upcase 2 раза, решила сделать 1. + +``` +Total allocated: 122.13 MB (1646193 objects) +``` + +### 6 + +Сделала сессию массивом. + +``` +Total allocated: 112.82 MB (1646193 objects) +``` + ++ upcase + +``` +Total allocated: 104.38 MB (1477055 objects) +``` + + + + + + +Упс, тест надо вернуть! + -### Ваша находка №X - какой отчёт показал главную точку роста - как вы решили её оптимизировать - как изменилась метрика diff --git a/measure.rb b/measure.rb new file mode 100644 index 00000000..9cd8e722 --- /dev/null +++ b/measure.rb @@ -0,0 +1,55 @@ + +require 'json' +require 'minitest/autorun' +require './work' +require 'pry' + +thread1 = Thread.new do + p "start" + time = Time.now + work('data_large.txt', gc: true) + after = Time.now + p after - time +end + +Thread.new do + loop do + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + sleep 1 + end +end + +thread1.join + +class TestMe < Minitest::Test + def setup + File.write('result.json', '') + File.write('data.txt', +'user,0,Leida,Cira,0 +session,0,0,Safari 29,87,2016-10-23 +session,0,1,Firefox 12,118,2017-02-27 +session,0,2,Internet Explorer 28,31,2017-03-28 +session,0,3,Internet Explorer 28,109,2016-09-15 +session,0,4,Safari 39,104,2017-09-27 +session,0,5,Internet Explorer 35,6,2016-09-01 +user,1,Palmer,Katrina,65 +session,1,0,Safari 17,12,2016-10-21 +session,1,1,Firefox 32,3,2016-12-20 +session,1,2,Chrome 6,59,2016-11-11 +session,1,3,Internet Explorer 10,28,2017-04-29 +session,1,4,Chrome 13,116,2016-12-28 +user,2,Gregory,Santos,86 +session,2,0,Chrome 35,6,2018-09-21 +session,2,1,Safari 49,85,2017-05-22 +session,2,2,Firefox 47,17,2018-02-02 +session,2,3,Chrome 20,84,2016-11-25 +') + end + + def test_result + work + expected_result = 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"]}}}') + res = JSON.parse(File.read('result.json')) + assert_equal expected_result, res + end +end diff --git a/task-2.rb b/task-2.rb index 118bfb91..f7820da5 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,7 +1,7 @@ # Deoptimized version of homework task require 'json' -# require 'minitest/autorun' +require 'minitest/autorun' require 'memory_profiler' require 'ruby-prof' require './work' @@ -17,27 +17,41 @@ # printer = RubyProf::CallTreePrinter.new(result) # printer.print(path: 'ruby_prof_reports', profile: 'profile') -# report = MemoryProfiler.report do -# work('data_large_sample.txt', gc: true) -# end -# report.pretty_print(scale_bytes: true) -# work('data_large.txt', gc: true) - -thread1 = Thread.new do - p "start" - time = Time.now - work('data_large.txt', gc: true) - after = Time.now - p after - time +report = MemoryProfiler.report do + # work('data_large_sample.txt', gc: true) + work('data_average_sample.txt', gc: true) end +report.pretty_print(scale_bytes: true) + +class TestMe < Minitest::Test + def setup + File.write('result.json', '') + File.write('data.txt', +'user,0,Leida,Cira,0 +session,0,0,Safari 29,87,2016-10-23 +session,0,1,Firefox 12,118,2017-02-27 +session,0,2,Internet Explorer 28,31,2017-03-28 +session,0,3,Internet Explorer 28,109,2016-09-15 +session,0,4,Safari 39,104,2017-09-27 +session,0,5,Internet Explorer 35,6,2016-09-01 +user,1,Palmer,Katrina,65 +session,1,0,Safari 17,12,2016-10-21 +session,1,1,Firefox 32,3,2016-12-20 +session,1,2,Chrome 6,59,2016-11-11 +session,1,3,Internet Explorer 10,28,2017-04-29 +session,1,4,Chrome 13,116,2016-12-28 +user,2,Gregory,Santos,86 +session,2,0,Chrome 35,6,2018-09-21 +session,2,1,Safari 49,85,2017-05-22 +session,2,2,Firefox 47,17,2018-02-02 +session,2,3,Chrome 20,84,2016-11-25 +') + end -Thread.new do - loop do - puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) - sleep 1 + def test_result + work + expected_result = JSON.parse('{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}') + res = JSON.parse(File.read('result.json')) + assert_equal expected_result, res end end - -thread1.join - - diff --git a/work.rb b/work.rb index 49884d70..ee9f258d 100644 --- a/work.rb +++ b/work.rb @@ -1,45 +1,14 @@ -class User - attr_reader :attributes, :sessions - - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions - end -end - -# def parse_user(cols) -# # _, id, first_name, last_name, age = cols.split(',') -# { -# 'id' => cols[1], -# 'first_name' => cols[2], -# 'last_name' => cols[3], -# 'age' => cols[4], -# } -# end - -# def parse_session(cols) -# # _, user_id, session_id, browser, time, date = cols.split(',') -# { -# 'user_id' => cols[1], -# 'session_id' => cols[2], -# 'browser' => cols[3], -# 'time' => cols[4], -# 'date' => cols[5], -# } -# end +OptimizedUser = Struct.new(:full_name, :sessions, keyword_init: true) def write_user_to_json(f, user, first_user: false) - user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" - - times = user.sessions.map { |s| s['time'].to_i } - browsers = user.sessions.map { |s| s['browser'].upcase } - - dates = user.sessions.map { |s| s['date'] } + times = user.sessions.map { |s| s[4].to_i } + browsers = user.sessions.map { |s| s[3] } + dates = user.sessions.map { |s| s[5] } # File.open file, "a" do |f| f.write ',' unless first_user f.write <<-JSON - \"#{user_key}\": { + \"#{user.full_name}\": { \"sessionsCount\": #{user.sessions.count}, \"totalTime\": "#{times.sum.to_s} min.", \"longestSession\": "#{times.max.to_s} min.", @@ -61,45 +30,46 @@ def work(filename = 'data.txt', gc: true, result: 'result.json') totalUsers = 0 # to see if we need a comma first_user = true + user_label = 'user'.freeze + session_label = 'session'.freeze File.open(result, 'a') do |f| f.write("{ \"usersStats\":{") File.readlines(filename, chomp: true).each do |line| - if line[0,4] == 'user' - _, id, first_name, last_name, age = line.split(',') + if line[0,4] == user_label + _, _, first_name, last_name, _ = line.split(',') # write previous user if current_user write_user_to_json(f, current_user, first_user: first_user) first_user = false end - # parsed_user = parse_user(cols) - parsed_user = { - 'id' => id, - 'first_name' => first_name, - 'last_name' => last_name, - 'age' => age, - } - current_user = User.new(attributes: parsed_user, sessions: []) + current_user = OptimizedUser.new(full_name: "#{first_name} #{last_name}", sessions: []) totalUsers += 1 - elsif line[0,6] == 'session' - _, user_id, session_id, browser, time, date = cols.split(',') - - session = { - 'user_id' => user_id, - 'session_id' => session_id, - 'browser' => browser, - 'time' => time, - 'date' => date, - } + elsif line[0,7] == session_label + # _, user_id, session_id, browser, time, date = line.split(',') + + # OptimizedSession.new(user_id: user_id, session_id: session_id, browser: browser, time:) + + session = line.split(',') + session[3].upcase! + # session = { + # 'user_id' => user_id, + # 'session_id' => session_id, + # 'browser' => browser.upcase, + # 'time' => time, + # 'date' => date, + # } # session = parse_session(cols) current_user.sessions.push session totalSessions += 1 - uniqueBrowsers.add(session['browser'].upcase) + # uniqueBrowsers.add(session['browser']) + uniqueBrowsers.add(session[3]) + end end write_user_to_json(f, current_user, first_user: false) @@ -113,8 +83,6 @@ def work(filename = 'data.txt', gc: true, result: 'result.json') f.write("}") end - - # Отчёт в json # - Сколько всего юзеров + # - Сколько всего уникальных браузеров + @@ -139,4 +107,4 @@ def work(filename = 'data.txt', gc: true, result: 'result.json') # f.write "\"totalUsers\": #{totalUsers}" # f.write("}") # end -end \ No newline at end of file +end From 2049aac24cda8a56ba803e42f1b0a250ba0df527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=BD=D0=B0=20=D0=91=D1=83=D1=8F=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0?= Date: Fri, 7 Feb 2025 23:51:05 +0300 Subject: [PATCH 6/8] Couple of tiny optimizations --- case-study.md | 35 ++++++++++++++++++++++++ work.rb | 74 +++++++++++++++++++++++---------------------------- 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/case-study.md b/case-study.md index 7533f3ff..7c860752 100644 --- a/case-study.md +++ b/case-study.md @@ -246,6 +246,41 @@ Total allocated: 112.82 MB (1646193 objects) Total allocated: 104.38 MB (1477055 objects) ``` +### 7 + +Сделала один split: + +``` +Total allocated: 97.00 MB (1292486 objects) +``` +### 8 + +Увидела много объектов `','`, завела константу `DELIMITER = ','` (omg, чем я занимаюсь) + +``` +Total allocated: 92.38 MB (1177056 objects) +# и ещё одну +Total allocated: 91.76 MB (1161626 objects) +``` + +### 9 + +Сделала `user_or_session.shift` для получения первого эл-та массива: +Ну такое: + +``` +Total allocated: 91.15 MB (1155726 objects) +``` + + +### 10 + +Убрала `Struct`, сделала обычный класс + +``` +Total allocated: 88.68 MB (1140295 objects)s +``` + diff --git a/work.rb b/work.rb index ee9f258d..e8da2651 100644 --- a/work.rb +++ b/work.rb @@ -1,20 +1,34 @@ -OptimizedUser = Struct.new(:full_name, :sessions, keyword_init: true) +# OptimizedUser = .new(:full_name, :sessions, keyword_init: true) + +class OptimizedUser + def initialize(full_name, sessions = []) + @full_name = full_name + @sessions = sessions + end + + attr_accessor :full_name, :sessions +end + +DELIMITER = ','.freeze +COMMA = ', '.freeze def write_user_to_json(f, user, first_user: false) - times = user.sessions.map { |s| s[4].to_i } - browsers = user.sessions.map { |s| s[3] } - dates = user.sessions.map { |s| s[5] } + times = user.sessions.map { |s| s[3].to_i } + browsers = user.sessions.map { |s| s[2] } + dates = user.sessions.map { |s| s[4] } + ie = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } + chrome = !ie && browsers.all? { |b| b =~ /CHROME/ } # File.open file, "a" do |f| - f.write ',' unless first_user + f.write DELIMITER unless first_user f.write <<-JSON \"#{user.full_name}\": { \"sessionsCount\": #{user.sessions.count}, \"totalTime\": "#{times.sum.to_s} min.", \"longestSession\": "#{times.max.to_s} min.", - \"browsers\": "#{browsers.sort.join(', ')}", - \"usedIE\": #{browsers.any? { |b| b =~ /INTERNET EXPLORER/ }}, - \"alwaysUsedChrome\": #{browsers.all? { |b| b =~ /CHROME/ }}, + \"browsers\": "#{browsers.sort.join(COMMA)}", + \"usedIE\": #{ie}, + \"alwaysUsedChrome\": #{chrome}, \"dates\": #{dates.sort.reverse} } JSON @@ -37,8 +51,11 @@ def work(filename = 'data.txt', gc: true, result: 'result.json') f.write("{ \"usersStats\":{") File.readlines(filename, chomp: true).each do |line| - if line[0,4] == user_label - _, _, first_name, last_name, _ = line.split(',') + session_or_user = line.split(DELIMITER) + line_type = session_or_user.shift + + if line_type == user_label + full_name = "#{session_or_user[1]} #{session_or_user[2]}" # write previous user if current_user @@ -46,30 +63,13 @@ def work(filename = 'data.txt', gc: true, result: 'result.json') first_user = false end - current_user = OptimizedUser.new(full_name: "#{first_name} #{last_name}", sessions: []) + current_user = OptimizedUser.new(full_name) totalUsers += 1 - elsif line[0,7] == session_label - # _, user_id, session_id, browser, time, date = line.split(',') - - # OptimizedSession.new(user_id: user_id, session_id: session_id, browser: browser, time:) - - session = line.split(',') - session[3].upcase! - # session = { - # 'user_id' => user_id, - # 'session_id' => session_id, - # 'browser' => browser.upcase, - # 'time' => time, - # 'date' => date, - # } - - # session = parse_session(cols) - current_user.sessions.push session - + elsif line_type == session_label + session_or_user[2].upcase! + current_user.sessions.push session_or_user totalSessions += 1 - # uniqueBrowsers.add(session['browser']) - uniqueBrowsers.add(session[3]) - + uniqueBrowsers.add(session_or_user[2]) end end write_user_to_json(f, current_user, first_user: false) @@ -78,7 +78,7 @@ def work(filename = 'data.txt', gc: true, result: 'result.json') f.write "\"uniqueBrowsersCount\": #{uniqueBrowsers.count}," f.write "\"totalSessions\": #{totalSessions}," - f.write "\"allBrowsers\": \"#{uniqueBrowsers.sort.join(',')}\"," + f.write "\"allBrowsers\": \"#{uniqueBrowsers.sort.join(DELIMITER)}\"," f.write "\"totalUsers\": #{totalUsers}" f.write("}") end @@ -99,12 +99,4 @@ def work(filename = 'data.txt', gc: true, result: 'result.json') # - даты сессий в порядке убывания через запятую + # Подсчёт количества уникальных браузеров - - # File.open result, "a" do |f| - # f.write "\"uniqueBrowsersCount\": #{uniqueBrowsers.count}," - # f.write "\"totalSessions\": #{totalSessions}," - # f.write "\"allBrowsers\": \"#{uniqueBrowsers.sort.join(',')}\"," - # f.write "\"totalUsers\": #{totalUsers}" - # f.write("}") - # end end From 97c301f9cc59696106b3c71dfd8c3e6d5bbd8e51 Mon Sep 17 00:00:00 2001 From: anna Date: Sat, 8 Feb 2025 12:46:52 +0300 Subject: [PATCH 7/8] Attempts to optimize --- Gemfile | 3 ++- Gemfile.lock | 2 ++ case-study.md | 51 ++++++++++++++++++++++++++++++++++++- measure.rb | 69 +++++++++++++++++++++++++-------------------------- task-2.rb | 10 ++++++-- work.rb | 65 ++++++++++++++++++++---------------------------- 6 files changed, 123 insertions(+), 77 deletions(-) diff --git a/Gemfile b/Gemfile index abe4f17e..3f66fec7 100644 --- a/Gemfile +++ b/Gemfile @@ -4,4 +4,5 @@ source "https://rubygems.org" gem 'minitest' gem 'ruby-prof' gem 'memory_profiler' -gem 'pry' \ No newline at end of file +gem 'pry' +gem 'stackprof' diff --git a/Gemfile.lock b/Gemfile.lock index 22bd5fe0..f4ec4b57 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,6 +9,7 @@ GEM coderay (~> 1.1) method_source (~> 1.0) ruby-prof (1.7.1) + stackprof (0.2.27) PLATFORMS ruby @@ -19,6 +20,7 @@ DEPENDENCIES minitest pry ruby-prof + stackprof RUBY VERSION ruby 3.3.6p108 diff --git a/case-study.md b/case-study.md index 7c860752..f0e9f8a8 100644 --- a/case-study.md +++ b/case-study.md @@ -281,12 +281,61 @@ Total allocated: 91.15 MB (1155726 objects) Total allocated: 88.68 MB (1140295 objects)s ``` +### 11 + +Убрала объект `User` - тут не особо что-то изменилось. + +``` +Total allocated: 88.32 MB (1124864 objects) +``` + +### 12 + +Поменяла режим записи в файл: + +``` +Total allocated: 87.90 MB (1124625 objects) +``` + +### Тупик + +Далее тупик, остальные ухищрения делают только хуже. + +При этом, при замере на большом файле использование памяти практически не изменилось (после замены на потоковую обработку): + +``` +MEMORY USAGE: 27 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +10.641142708 +``` + +Проблемные места: + +```ruby +line = line.split(DELIMITER) + +dates.sort.reverse + +File.readlines +``` + +Много строк "session" создаётся при `split` + много строк типа "0", "1" и т.д. + +Посмотрела stackprof, примерно то же показывает. -Упс, тест надо вернуть! - какой отчёт показал главную точку роста diff --git a/measure.rb b/measure.rb index 9cd8e722..df687dea 100644 --- a/measure.rb +++ b/measure.rb @@ -1,11 +1,10 @@ -require 'json' -require 'minitest/autorun' +# require 'json' +# require 'minitest/autorun' require './work' -require 'pry' +# require 'pry' thread1 = Thread.new do - p "start" time = Time.now work('data_large.txt', gc: true) after = Time.now @@ -21,35 +20,35 @@ thread1.join -class TestMe < Minitest::Test - def setup - File.write('result.json', '') - File.write('data.txt', -'user,0,Leida,Cira,0 -session,0,0,Safari 29,87,2016-10-23 -session,0,1,Firefox 12,118,2017-02-27 -session,0,2,Internet Explorer 28,31,2017-03-28 -session,0,3,Internet Explorer 28,109,2016-09-15 -session,0,4,Safari 39,104,2017-09-27 -session,0,5,Internet Explorer 35,6,2016-09-01 -user,1,Palmer,Katrina,65 -session,1,0,Safari 17,12,2016-10-21 -session,1,1,Firefox 32,3,2016-12-20 -session,1,2,Chrome 6,59,2016-11-11 -session,1,3,Internet Explorer 10,28,2017-04-29 -session,1,4,Chrome 13,116,2016-12-28 -user,2,Gregory,Santos,86 -session,2,0,Chrome 35,6,2018-09-21 -session,2,1,Safari 49,85,2017-05-22 -session,2,2,Firefox 47,17,2018-02-02 -session,2,3,Chrome 20,84,2016-11-25 -') - end +# class TestMe < Minitest::Test +# def setup +# File.write('result.json', '') +# File.write('data.txt', +# 'user,0,Leida,Cira,0 +# session,0,0,Safari 29,87,2016-10-23 +# session,0,1,Firefox 12,118,2017-02-27 +# session,0,2,Internet Explorer 28,31,2017-03-28 +# session,0,3,Internet Explorer 28,109,2016-09-15 +# session,0,4,Safari 39,104,2017-09-27 +# session,0,5,Internet Explorer 35,6,2016-09-01 +# user,1,Palmer,Katrina,65 +# session,1,0,Safari 17,12,2016-10-21 +# session,1,1,Firefox 32,3,2016-12-20 +# session,1,2,Chrome 6,59,2016-11-11 +# session,1,3,Internet Explorer 10,28,2017-04-29 +# session,1,4,Chrome 13,116,2016-12-28 +# user,2,Gregory,Santos,86 +# session,2,0,Chrome 35,6,2018-09-21 +# session,2,1,Safari 49,85,2017-05-22 +# session,2,2,Firefox 47,17,2018-02-02 +# session,2,3,Chrome 20,84,2016-11-25 +# ') +# end - def test_result - work - expected_result = 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"]}}}') - res = JSON.parse(File.read('result.json')) - assert_equal expected_result, res - end -end +# def test_result +# work +# expected_result = JSON.parse('{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}') +# res = JSON.parse(File.read('result.json')) +# assert_equal expected_result, res +# end +# end diff --git a/task-2.rb b/task-2.rb index f7820da5..b3881f89 100644 --- a/task-2.rb +++ b/task-2.rb @@ -6,6 +6,7 @@ require 'ruby-prof' require './work' require 'pry' +require 'stackprof' # result = RubyProf.profile do @@ -18,11 +19,16 @@ # printer.print(path: 'ruby_prof_reports', profile: 'profile') report = MemoryProfiler.report do - # work('data_large_sample.txt', gc: true) - work('data_average_sample.txt', gc: true) + work('data_large_sample.txt', gc: true) end report.pretty_print(scale_bytes: true) + +# StackProf.run(mode: :object,raw: true, out: 'stackprof.dump', interval: 1) do +# work('data_large_sample.txt', gc: true) +# end + + class TestMe < Minitest::Test def setup File.write('result.json', '') diff --git a/work.rb b/work.rb index e8da2651..cb7acefc 100644 --- a/work.rb +++ b/work.rb @@ -1,38 +1,26 @@ -# OptimizedUser = .new(:full_name, :sessions, keyword_init: true) - -class OptimizedUser - def initialize(full_name, sessions = []) - @full_name = full_name - @sessions = sessions - end - - attr_accessor :full_name, :sessions -end - DELIMITER = ','.freeze COMMA = ', '.freeze def write_user_to_json(f, user, first_user: false) - times = user.sessions.map { |s| s[3].to_i } - browsers = user.sessions.map { |s| s[2] } - dates = user.sessions.map { |s| s[4] } + name = user.shift + times = user.map { |s| s[3].to_i } + browsers = user.map { |s| s[2] } + dates = user.map { |s| s[4] } ie = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } chrome = !ie && browsers.all? { |b| b =~ /CHROME/ } - # File.open file, "a" do |f| - f.write DELIMITER unless first_user - f.write <<-JSON - \"#{user.full_name}\": { - \"sessionsCount\": #{user.sessions.count}, - \"totalTime\": "#{times.sum.to_s} min.", - \"longestSession\": "#{times.max.to_s} min.", - \"browsers\": "#{browsers.sort.join(COMMA)}", - \"usedIE\": #{ie}, - \"alwaysUsedChrome\": #{chrome}, - \"dates\": #{dates.sort.reverse} - } - JSON - # end + f.write DELIMITER unless first_user + f.write <<-JSON + \"#{name}\": { + \"sessionsCount\": #{user.count}, + \"totalTime\": "#{times.sum} min.", + \"longestSession\": "#{times.max} min.", + \"browsers\": "#{browsers.sort.join(COMMA)}", + \"usedIE\": #{ie}, + \"alwaysUsedChrome\": #{chrome}, + \"dates\": #{dates.sort.reverse} + } + JSON end def work(filename = 'data.txt', gc: true, result: 'result.json') @@ -47,30 +35,31 @@ def work(filename = 'data.txt', gc: true, result: 'result.json') user_label = 'user'.freeze session_label = 'session'.freeze - File.open(result, 'a') do |f| + File.open(result, 'w') do |f| f.write("{ \"usersStats\":{") File.readlines(filename, chomp: true).each do |line| - session_or_user = line.split(DELIMITER) - line_type = session_or_user.shift + line = line.split(DELIMITER) + line_type = line.shift if line_type == user_label - full_name = "#{session_or_user[1]} #{session_or_user[2]}" - + full_name = "#{line[1]} #{line[2]}" # write previous user if current_user write_user_to_json(f, current_user, first_user: first_user) first_user = false end - - current_user = OptimizedUser.new(full_name) + current_user = [full_name] totalUsers += 1 elsif line_type == session_label - session_or_user[2].upcase! - current_user.sessions.push session_or_user + line[2].upcase! + current_user << line totalSessions += 1 - uniqueBrowsers.add(session_or_user[2]) + uniqueBrowsers.add(line[2]) end + # if totalUsers % 50000 == 0 + # puts "записали 50000 юзеров" + # end end write_user_to_json(f, current_user, first_user: false) From 7f4aa76ed18dd7090a48e91b1c3a3a6b92287a64 Mon Sep 17 00:00:00 2001 From: anna Date: Sat, 8 Feb 2025 14:23:13 +0300 Subject: [PATCH 8/8] File optimization, case study --- .gitignore | 1 - Gemfile | 1 + Gemfile.lock | 23 ++++++++ case-study.md | 154 +++++++++++++++++++++++--------------------------- measure.rb | 38 +------------ spec/spec.rb | 21 +++++++ task-2.rb | 16 ------ work.rb | 76 +++++++++++++++---------- 8 files changed, 162 insertions(+), 168 deletions(-) create mode 100644 spec/spec.rb diff --git a/.gitignore b/.gitignore index 56e30085..ad41dc5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ data_large.txt *.json -test.rb diff --git a/Gemfile b/Gemfile index 3f66fec7..8062e8a4 100644 --- a/Gemfile +++ b/Gemfile @@ -6,3 +6,4 @@ gem 'ruby-prof' gem 'memory_profiler' gem 'pry' gem 'stackprof' +gem 'rspec-benchmark' diff --git a/Gemfile.lock b/Gemfile.lock index f4ec4b57..61b136e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,35 @@ GEM remote: https://rubygems.org/ specs: + benchmark-malloc (0.2.0) + benchmark-perf (0.6.0) + benchmark-trend (0.4.0) coderay (1.1.3) + diff-lcs (1.5.1) memory_profiler (1.1.0) method_source (1.1.0) minitest (5.25.4) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-benchmark (0.6.0) + benchmark-malloc (~> 0.2) + benchmark-perf (~> 0.6) + benchmark-trend (~> 0.4) + rspec (>= 3.0) + rspec-core (3.13.3) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.2) ruby-prof (1.7.1) stackprof (0.2.27) @@ -19,6 +41,7 @@ DEPENDENCIES memory_profiler minitest pry + rspec-benchmark ruby-prof stackprof diff --git a/case-study.md b/case-study.md index f0e9f8a8..22841db8 100644 --- a/case-study.md +++ b/case-study.md @@ -36,8 +36,7 @@ MEMORY USAGE: 5924 MB ## Feedback-Loop Для того, чтобы иметь возможность быстро проверять гипотезы я выстроила эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 35 секунд. - -Вот как я построил `feedback_loop`: +Вот как я построила `feedback_loop`: Прописала, чтобы оценивать кол-во памяти: ```ruby @@ -46,15 +45,10 @@ puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) На этапе отладки потокового варианта использовала sample, к-й позволял оценить работоспособность в пределах пары секунд. -Далее подобрала sample, с к-м фидбек можно было получить за n секунд. - - -Далее использовала простой вывод, чтобы оценивать в пределах 35 секунд. - -Либо sample на 1_000_000 строк, тогда в пределах 7 секунд. +Далее подобрала sample, с к-м фидбек можно было получить за 5-20 секунд. ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовалась `ruby-prof`. Пробовала другие (stackprof, kcachgrind (да, у меня он с k)), ничего особо нового из них не узнала. Вот какие проблемы удалось найти и решить @@ -74,7 +68,7 @@ puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) ### Ваша находка №1 -memory profiler показал ,что File очень много ест памяти, но хз, как от него избавиться! +memory profiler показал ,что File очень много ест памяти, но непонятно, как от него избавиться. Также посмотрела qcachegrind (у меня kcachegrind) - много отнимают `write_user_to_json` , `Array#each`. @@ -106,7 +100,6 @@ MEMORY USAGE: 334 MB 22.006778048 ``` - Подумала, раз `File`, то не открывать/закрывать его для каждого юзера, а открыть на запись 1 раз. Переписала - по памяти ничего не поменялось, но программа стала быстрее отрабатывать (27 => 21 секунд, если проверять на большом файле без профилирования) @@ -137,82 +130,24 @@ Total allocated: 179.51 MB (2546193 objects) Total allocated: 131.29 MB (1761624 objects) ``` -Далее избавилась от массива в `split` (у меня это уже было в предыдущем варианте, заметила, что вариант с набором переменных быстрее отрабатывает). -+ избавилась от методов пока что `parse_session`, `parse_user`. - -```ruby -_, user_id, session_id, browser, time, date = cols.split(',') - -session = { - 'user_id' => user_id, - 'session_id' => session_id, - 'browser' => browser, - 'time' => time, - 'date' => date, -} -``` - -Использование памяти на сэмпле снизилось до: -``` -Total allocated: 51.71 MB (854936 objects) -``` - -Проверяю основную метрику на большом файле: - -``` -MEMORY USAGE: 33 MB -MEMORY USAGE: 327 MB -MEMORY USAGE: 327 MB -MEMORY USAGE: 327 MB -MEMORY USAGE: 327 MB -MEMORY USAGE: 327 MB -MEMORY USAGE: 327 MB -MEMORY USAGE: 327 MB -7.241316391 -``` - -Общий объём использованной памяти немного снизился + программа стала отрабатывать намного быстрее! - -### Ваша находка №3 - -Теперь ест на: - -`line[0,4] == 'user'` - -`line[0,7] == 'session'` - -Создала соотв. переменные заранее и сравниваю с ними. - -Решила проверить на сэмпле на 300_000 строк. - -До: -``` -Total allocated: 155.18 MB (2560691 objects) -``` - -После: - -``` -Total allocated: 133.02 MB (2006807 objects) -``` - -Ну пусть будет. +### Предупреждение об особенностях работы +[Тут было ещё несколько находок и итераций, но где-то закралась ошибка, поэтому убрала их] +[+ в целом было несколько заходов туда-сюда, оставила лучший из вариантов] ### Ваша находка номер 4 Теперь (кроме File): -``` +```ruby current_user = User.new(attributes: parsed_user, sessions: []) ``` - Можно оптимизировать `User` => `OptimizedUser` + увидела, что не используются id и age, их убираем из объекта. Заменила на оптимизированного юзера без лишних полей: -``` +```ruby OptimizedUser = Struct.new(:full_name, :sessions, keyword_init: true) current_user = OptimizedUser.new(full_name: "#{first_name} #{last_name}", sessions: []) @@ -272,7 +207,6 @@ Total allocated: 91.76 MB (1161626 objects) Total allocated: 91.15 MB (1155726 objects) ``` - ### 10 Убрала `Struct`, сделала обычный класс @@ -332,22 +266,74 @@ File.readlines Посмотрела stackprof, примерно то же показывает. +## 12 +Попробовала переписать ещё более потоково - не накапливать сессии в рамках каждого юзера в массиве, а считать по мере прохождения по строкам в переменных и писать сразу по позможности, юзера писать сразу. +Результат минимальный (на 100_000 строк): +``` +Total allocated: 86.70 MB (1099665 objects) +``` +Замеры (память и время): +На файле `data_large.txt`: +``` +MEMORY USAGE: 27 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +MEMORY USAGE: 334 MB +9.926471411 +``` -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +На 100_000 строк: +``` +MEMORY USAGE: 27 MB +0.243047239 +``` -## Результаты -В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. +На 300_000 строк: +``` +MEMORY USAGE: 27 MB +MEMORY USAGE: 122 MB +MEMORY USAGE: 122 MB +MEMORY USAGE: 122 MB +3.195018119 +``` + +Далее можно использовать `oj` , но т.к. он не касается проблемных мест, то не стала использовать. + +## Находка 13 + +Решила дополнительно поресёрчить, можно ли более оптимально прочитать файл. +Просто я была уверена, что `readlines` это и есть `foreach`. Да и `open` , к-й был, сработал бы. + +Заменила `readlines` на `foreach`: + +Результат на data_large.txt: + +``` +MEMORY USAGE: 27 MB +MEMORY USAGE: 29 MB +MEMORY USAGE: 29 MB +MEMORY USAGE: 29 MB +MEMORY USAGE: 29 MB +MEMORY USAGE: 29 MB +MEMORY USAGE: 29 MB +Time: 6.599629822 seconds +TOTAL MEMORY USAGE: 29 MB +``` -*Какими ещё результами можете поделиться* +## Результаты +В результате проделанной оптимизации удалось обработать файл с данными. +Удалось улучшить метрику системы с использования 2444 MB и 30с до обработки целевого файла за 6-7 секунд и 29mb памяти, но не удалось уложиться в заданный бюджет. ## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написана парочка performance-тестов. diff --git a/measure.rb b/measure.rb index df687dea..fc04ef90 100644 --- a/measure.rb +++ b/measure.rb @@ -1,14 +1,11 @@ -# require 'json' -# require 'minitest/autorun' require './work' -# require 'pry' thread1 = Thread.new do time = Time.now work('data_large.txt', gc: true) after = Time.now - p after - time + puts "Time: #{after - time} seconds" end Thread.new do @@ -20,35 +17,4 @@ thread1.join -# class TestMe < Minitest::Test -# def setup -# File.write('result.json', '') -# File.write('data.txt', -# 'user,0,Leida,Cira,0 -# session,0,0,Safari 29,87,2016-10-23 -# session,0,1,Firefox 12,118,2017-02-27 -# session,0,2,Internet Explorer 28,31,2017-03-28 -# session,0,3,Internet Explorer 28,109,2016-09-15 -# session,0,4,Safari 39,104,2017-09-27 -# session,0,5,Internet Explorer 35,6,2016-09-01 -# user,1,Palmer,Katrina,65 -# session,1,0,Safari 17,12,2016-10-21 -# session,1,1,Firefox 32,3,2016-12-20 -# session,1,2,Chrome 6,59,2016-11-11 -# session,1,3,Internet Explorer 10,28,2017-04-29 -# session,1,4,Chrome 13,116,2016-12-28 -# user,2,Gregory,Santos,86 -# session,2,0,Chrome 35,6,2018-09-21 -# session,2,1,Safari 49,85,2017-05-22 -# session,2,2,Firefox 47,17,2018-02-02 -# session,2,3,Chrome 20,84,2016-11-25 -# ') -# end - -# def test_result -# work -# expected_result = 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"]}}}') -# res = JSON.parse(File.read('result.json')) -# assert_equal expected_result, res -# end -# end +puts "TOTAL MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) diff --git a/spec/spec.rb b/spec/spec.rb new file mode 100644 index 00000000..9dd06de7 --- /dev/null +++ b/spec/spec.rb @@ -0,0 +1,21 @@ + +require 'rspec-benchmark' +require_relative '../work' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end + +describe 'Performance' do + it 'uses under 30MB of memory' do + expect do + work('test_data.txt') + end.to perform_allocation(31457280).bytes + end + + it 'performs under 7 seconds' do + expect do + work('data_large_sample.txt') + end.to perform_under(7).sec + end +end \ No newline at end of file diff --git a/task-2.rb b/task-2.rb index b3881f89..6811ee11 100644 --- a/task-2.rb +++ b/task-2.rb @@ -8,27 +8,11 @@ require 'pry' require 'stackprof' - -# result = RubyProf.profile do -# end - -# На этот раз профилируем не allocations, а объём памяти! -# RubyProf.measure_mode = RubyProf::MEMORY - -# printer = RubyProf::CallTreePrinter.new(result) -# printer.print(path: 'ruby_prof_reports', profile: 'profile') - report = MemoryProfiler.report do work('data_large_sample.txt', gc: true) end report.pretty_print(scale_bytes: true) - -# StackProf.run(mode: :object,raw: true, out: 'stackprof.dump', interval: 1) do -# work('data_large_sample.txt', gc: true) -# end - - class TestMe < Minitest::Test def setup File.write('result.json', '') diff --git a/work.rb b/work.rb index cb7acefc..0d2d3b5b 100644 --- a/work.rb +++ b/work.rb @@ -1,20 +1,11 @@ DELIMITER = ','.freeze COMMA = ', '.freeze -def write_user_to_json(f, user, first_user: false) - name = user.shift - times = user.map { |s| s[3].to_i } - browsers = user.map { |s| s[2] } - dates = user.map { |s| s[4] } - ie = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } - chrome = !ie && browsers.all? { |b| b =~ /CHROME/ } - - f.write DELIMITER unless first_user +def write_sessions(f, cnt, time_sum, time_max, browsers, dates, ie, chrome) f.write <<-JSON - \"#{name}\": { - \"sessionsCount\": #{user.count}, - \"totalTime\": "#{times.sum} min.", - \"longestSession\": "#{times.max} min.", + \"sessionsCount\": #{cnt}, + \"totalTime\": "#{time_sum} min.", + \"longestSession\": "#{time_max} min.", \"browsers\": "#{browsers.sort.join(COMMA)}", \"usedIE\": #{ie}, \"alwaysUsedChrome\": #{chrome}, @@ -26,7 +17,6 @@ def write_user_to_json(f, user, first_user: false) def work(filename = 'data.txt', gc: true, result: 'result.json') GC.disable unless gc - current_user = nil uniqueBrowsers = Set.new totalSessions = 0 totalUsers = 0 @@ -35,36 +25,60 @@ def work(filename = 'data.txt', gc: true, result: 'result.json') user_label = 'user'.freeze session_label = 'session'.freeze + time_sum = 0 + time_max = 0 + browsers = [] + dates = [] + ie = false + chrome = true + sessions_cnt = 0 + File.open(result, 'w') do |f| f.write("{ \"usersStats\":{") - File.readlines(filename, chomp: true).each do |line| - line = line.split(DELIMITER) - line_type = line.shift + File.foreach(filename, chomp: true).each do |line| + line_type, _, second, third, fourth, fifth = line.split(DELIMITER) if line_type == user_label - full_name = "#{line[1]} #{line[2]}" - # write previous user - if current_user - write_user_to_json(f, current_user, first_user: first_user) - first_user = false + unless first_user + write_sessions(f, sessions_cnt, time_sum, time_max, browsers, dates, ie, chrome) + f.write DELIMITER end - current_user = [full_name] + + f.write "\"#{second} #{third}\": {" + first_user = false + + time_sum = 0 + time_max = 0 + browsers = [] + dates = [] + ie = false + chrome = true + sessions_cnt = 0 + totalUsers += 1 elsif line_type == session_label - line[2].upcase! - current_user << line + third.upcase! # browser + ctime = fourth.to_i + + time_sum += ctime + time_max = ctime if ctime > time_max + browsers << third + unless ie + ie = true if third =~ /INTERNET EXPLORER/ + end + if chrome + chrome = false unless third =~ /CHROME/ + end + dates << fifth + sessions_cnt += 1 totalSessions += 1 - uniqueBrowsers.add(line[2]) + uniqueBrowsers.add(third) end - # if totalUsers % 50000 == 0 - # puts "записали 50000 юзеров" - # end end - write_user_to_json(f, current_user, first_user: false) + write_sessions(f, sessions_cnt, time_sum, time_max, browsers, dates, ie, chrome) f.write("},") - f.write "\"uniqueBrowsersCount\": #{uniqueBrowsers.count}," f.write "\"totalSessions\": #{totalSessions}," f.write "\"allBrowsers\": \"#{uniqueBrowsers.sort.join(DELIMITER)}\","