From 1222a7cbbe71f426678a0cb91251d5bd9d85ef57 Mon Sep 17 00:00:00 2001 From: Oleg Platonov Date: Tue, 28 Jan 2025 19:00:24 +0500 Subject: [PATCH 1/3] Develop first stage. Now it faster, but not enough --- .gitignore | 13 +++++ case-study.md | 87 ++++++++++++++++++++++++++++ task-1.rb | 156 +++++++++++++++++++++++++++++++++----------------- 3 files changed, 205 insertions(+), 51 deletions(-) create mode 100644 .gitignore create mode 100644 case-study.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..37c05c14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.idea +.idea/ +.vscode/ + +# ignore file data +data_large.txt +data.json +data_small.txt +result.json + +# ignore directory +ruby_prof_reports +stackprof_reports diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..aca769ec --- /dev/null +++ b/case-study.md @@ -0,0 +1,87 @@ +# Case-study оптимизации + +## Checklist +- [x] Прикинуть зависимость времени работы программы от размера обрабатываемого файла +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`; +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`; +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`; +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`; +- [x] Построить дамп `stackprof` и проанализировать его с помощью `CLI` +- [x] Построить дамп `stackprof` в `json` и проанализировать его с помощью `speedscope.app` +- [x] Профилировать работающий процесс `rbspy`; +- [x] Добавить в программу `ProgressBar`; +- [ ] Постараться довести асимптотику до линейной и проверить это тестом; +- [x] Написать простой тест на время работы: когда вы придёте к оптимизированному решению, замерьте, сколько оно будет работать на тестовом объёме данных; и напишите тест на то, что это время не превышается (чтобы не было ложных срабатываний, задайте время с небольшим запасом); + + +## Актуальная проблема +Запуск программы с данными в 12 500 срок показало работу в 2.3 секунд. +Запуск программы с данными в 25 000 срок показало работу в 8 секунд. +Запуск программы с данными в 50 000 срок показало работу в 55 секунд. +Запуск программы с данными в 100 000 срок показало работу в 257 секунд. + +Тут видно, что с увеличением строк в файле в два раза, процесс обработки увеличивается более чем в 5 раз. + +То есть если файл будет с 3 250 940 строк, то этот файл может обрабатываться примерно неделю. + +Моя первая цель ускорить обработку файла в 25 000 срок до 2 секунд. Это примерная оценка из "потолка". + +## Формирование метрики +Для понимания процесса обработки данных я использовал метрику *Benchmark*. Так я могу видеть время работы программы + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. + +Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +Еще добавлен тест на время выполнения теста. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: *как вы построили feedback_loop* + +- Для начала я запустил Rbspy что бы обнаружить точку роста. 99% времени работа происходит в этом блоке кода block in work - task-1.rb:63 + +- Ruby-prof в режиме Flat показал точку роста в вызове метода Array#select + +- Ruby-prof в режиме Graph так же показал точку роста в Array#select и вызывался из Array#each + +- В Ruby-prof в режиме callgrind получил красивый отчет, так же показан рост в Array#select + +- StackProf показал основное время работы в Object#work + +- StackProf в формате json на много интереснее получается и так же информативно + + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- Ruby-prof в режиме Graph так же показал точку роста в Array#select +- Самое главное что я сделал, это первую обработку данных. Что бы все было структурировано и была некая связь между пользователем и его сессиями. После этого внес правки в весь код, что бы тесты начали выполняться +- Метрика стала на много лучше . Запуск программы с данными в 25 000 срок показало работу в 0.8 секунд. Это укладывается в мой первоначальный бюджет. +- Теперь профилировщик показывает две точки роста. Это 71% Object#collect_stats_from_users и 50% Array#map + +### Ваша находка №2 +- Ruby-prof в режиме Graph так же показал две точки роста 71% в Object#collect_stats_from_users и 50% в Array#map +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? + +### Ваша находка №X +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. + +*Какими ещё результами можете поделиться* + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* \ No newline at end of file diff --git a/task-1.rb b/task-1.rb index 778672df..9141c150 100644 --- a/task-1.rb +++ b/task-1.rb @@ -4,6 +4,16 @@ require 'pry' require 'date' require 'minitest/autorun' +require 'benchmark' +require 'ruby-progressbar' + +require 'ruby-prof' +require 'stackprof' + +# FILE_NAME = 'data_large.txt' +# FILE_NAME = 'data_small.txt' +FILE_NAME = 'data.txt' + class User attr_reader :attributes, :sessions @@ -14,45 +24,66 @@ 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], - } -end +def parse_data(line, result) + fields = line.split(',') + + if fields[0] == 'user' + id = fields[1] + + result[id] = { + first_name: fields[2], + last_name: fields[3], + age: fields[4], + sessions: {} + } + + return result + end + + user_id = fields[1] + session_id = fields[2] + + user = result[user_id] + + sessions = user[:sessions] -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], + sessions[session_id] = { + browser: fields[3], + time: fields[4], + date: fields[5] } + + result end def collect_stats_from_users(report, users_objects, &block) users_objects.each do |user| - user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" + user_key = "#{user.attributes[:first_name]}" + ' ' + "#{user.attributes[:last_name]}" report['usersStats'][user_key] ||= {} report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) end end -def work - file_lines = File.read('data.txt').split("\n") +def progress_bar(parts_of_work) + ProgressBar.create( + total: parts_of_work, + format: '%a, %J, %E %B' # elapsed time, percent complete, estimate, bar + # output: File.open(File::NULL, 'w') # IN TEST ENV + ) +end + +def work(disable_gc: false, enable_pb: false) + GC.disable if disable_gc + + file_lines = File.read(FILE_NAME).split("\n") - users = [] - sessions = [] + progressbar = progress_bar(file_lines.count) + + users = {} file_lines.each do |line| - cols = line.split(',') - users = users + [parse_user(line)] if cols[0] == 'user' - sessions = sessions + [parse_session(line)] if cols[0] == 'session' + progressbar.increment if enable_pb + users = parse_data(line, users) end # Отчёт в json @@ -75,32 +106,29 @@ def work report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end + unique_browsers = users.values.flat_map do |user| + user[:sessions].values.flat_map { |session| session[:browser] } + end.uniq - report['uniqueBrowsersCount'] = uniqueBrowsers.count + report['uniqueBrowsersCount'] = unique_browsers.count - report['totalSessions'] = sessions.count + report['totalSessions'] = users.values.sum { |user| user[:sessions].size } - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') + all_browsers = users.values.flat_map do |user| + user[:sessions].values.map { |session| session[:browser].upcase } + end.sort.uniq.join(',') + + report['allBrowsers'] = all_browsers # Статистика по пользователям 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] + users.each_value do |user| + sessions = user.delete(:sessions) + + user_object = User.new(attributes: user, sessions: sessions) + + users_objects.append(user_object) end report['usersStats'] = {} @@ -112,32 +140,32 @@ def work # Собираем количество времени по пользователям 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.' } + { 'totalTime' => user.sessions.values.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.' } + { 'longestSession' => user.sessions.values.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(', ') } + { 'browsers' => user.sessions.values.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/ } } + { 'usedIE' => user.sessions.values.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/ } } + { 'alwaysUsedChrome' => user.sessions.values.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 } } + { 'dates' => user.sessions.values.map{|s| s[:date]}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } end File.write('result.json', "#{report.to_json}\n") @@ -169,8 +197,34 @@ def setup end def test_result - work + elapsed_time = Benchmark.realtime { work } + + available_time = 2 + + msg = "Execution time exceeded: #{elapsed_time} seconds. + The available time to complete the test is #{available_time} seconds." + + assert elapsed_time <= available_time, msg + + # assert_equal File.read('data.json'), File.read('result.json') + expected_result = '{"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"]}}}' + "\n" + assert_equal expected_result, File.read('result.json') end + + def test_profile + result = RubyProf.profile { work(disable_gc: true, enable_pb: true) } + + printer = RubyProf::GraphHtmlPrinter.new(result) + printer.print(File.open("ruby_prof_reports/graph.html", "w+")) + end + + # def test_stackprof + # profile = StackProf.run(mode: :wall, raw: true) do + # work(disable_gc: true) + # end + # + # File.write('stackprof_reports/stackprof.json', JSON.generate(profile)) + # end end From 7d2aa71aeb61bebdc1588dd5fb6095123b8acd06 Mon Sep 17 00:00:00 2001 From: Oleg Platonov Date: Thu, 30 Jan 2025 13:21:58 +0500 Subject: [PATCH 2/3] develop code faster --- app/bar.rb | 15 ++++ app/parsing.rb | 61 +++++++++++++ app/report.rb | 96 +++++++++++++++++++++ app/task-1.rb | 91 +++++++++++++++++++ app/user.rb | 8 ++ task-1.rb | 230 ------------------------------------------------- 6 files changed, 271 insertions(+), 230 deletions(-) create mode 100644 app/bar.rb create mode 100644 app/parsing.rb create mode 100644 app/report.rb create mode 100644 app/task-1.rb create mode 100644 app/user.rb delete mode 100644 task-1.rb diff --git a/app/bar.rb b/app/bar.rb new file mode 100644 index 00000000..6f8d52f4 --- /dev/null +++ b/app/bar.rb @@ -0,0 +1,15 @@ +class Bar + attr_reader :parts_of_work + + def initialize(parts_of_work) + @parts_of_work = parts_of_work + end + + def progress + ProgressBar.create( + total: parts_of_work, + format: '%a, %J, %E %B' # elapsed time, percent complete, estimate, bar + # output: File.open(File::NULL, 'w') # IN TEST ENV + ) + end +end diff --git a/app/parsing.rb b/app/parsing.rb new file mode 100644 index 00000000..989ecdfe --- /dev/null +++ b/app/parsing.rb @@ -0,0 +1,61 @@ +class Parsing + attr_reader :file_name, :disable_bar + + def initialize(file_name, disable_bar) + @file_name = file_name + @disable_bar = disable_bar + end + + def call + file_lines = File.read(file_name).split("\n") + bar = Bar.new(file_lines.count).progress unless disable_bar + + result = {} + + file_lines.each do |line| + bar.increment unless disable_bar + + fields = line.split(',') + + if fields[0] == 'user' + id = fields[1] + + result[id] = { + first_name: fields[2], + last_name: fields[3], + age: fields[4], + sessions: {} + } + + next + end + + user_id = fields[1] + browser = fields[3].upcase + time = fields[4].to_i + date = fields[5] + sessions = result[user_id][:sessions] + sessions[:items] ||= {} + + sessions[:total_time] ||= 0 + sessions[:total_time] += time + + sessions[:long_session] ||= 0 + sessions[:long_session] = time if time > sessions[:long_session] + + sessions[:browsers] ||= [] + sessions[:browsers].push(browser) + + sessions[:dates] ||= [] + sessions[:dates].push(date) + + sessions[:items][fields[2]] = { + browser: browser, + time: time, + date: date + } + end + + result + end +end diff --git a/app/report.rb b/app/report.rb new file mode 100644 index 00000000..dd4d19c8 --- /dev/null +++ b/app/report.rb @@ -0,0 +1,96 @@ +class Report + attr_reader :users, :disable_bar + + def initialize(users, disable_bar) + @users = users + @disable_bar = disable_bar + end + + def call + # Отчёт в json + # - Сколько всего юзеров + + # - Сколько всего уникальных браузеров + + # - Сколько всего сессий + + # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + + # + # - По каждому пользователю + # - сколько всего сессий + + # - сколько всего времени + + # - самая длинная сессия + + # - браузеры через запятую + + # - Хоть раз использовал IE? + + # - Всегда использовал только Хром? + + # - даты сессий в порядке убывания через запятую + + + report = {} + + report[:totalUsers] = users.count + + # Подсчёт количества уникальных браузеров + unique_browsers = users.values.flat_map do |user| + user[:sessions][:items].values.flat_map { |session| session[:browser] } + end.compact.uniq + + report['uniqueBrowsersCount'] = unique_browsers.count + + report['totalSessions'] = users.values.sum { |user| user[:sessions].size } + + all_browsers = users.values.flat_map do |user| + user[:sessions][:items].values.map { |session| session[:browser] } + end.compact.sort.uniq.join(',') + + report['allBrowsers'] = all_browsers + + # Статистика по пользователям + users_objects = [] + + users.each_value do |user| + sessions = user.delete(:sessions) + + user_object = User.new(attributes: user, sessions: sessions) + + users_objects.append(user_object) + end + + report['usersStats'] = {} + + collect_data(report, users_objects) + end + + def collect_data(report, users_objects) + bar = Bar.new(users_objects.count).progress unless disable_bar + + report['usersStats'] = {} + + users_objects.each do |user| + bar.increment unless disable_bar + + user_key = "#{user.attributes[:first_name]} #{user.attributes[:last_name]}" + + report['usersStats'][user_key] ||= {} + + # Собираем количество сессий по пользователям + report['usersStats'][user_key][:sessionsCount] = user.sessions[:items].count + + # Собираем количество времени по пользователям + report['usersStats'][user_key][:totalTime] = "#{user.sessions[:total_time]} min." + + # Выбираем самую длинную сессию пользователя + report['usersStats'][user_key][:longestSession] = "#{user.sessions[:long_session]} min." + + # Браузеры пользователя через запятую + report['usersStats'][user_key][:browsers] = user.sessions[:browsers].sort.join(', ') + + # Хоть раз использовал IE? + report['usersStats'][user_key][:usedIE] = user.sessions[:browsers].any? { |b| b =~ /INTERNET EXPLORER/ } + + # Всегда использовал только Chrome? + report['usersStats'][user_key][:alwaysUsedChrome] = user.sessions[:browsers].all? { |b| b =~ /CHROME/ } + + # Даты сессий через запятую в обратном порядке в формате iso8601 + report['usersStats'][user_key][:dates] = user.sessions[:dates].sort.reverse + end + + report + end +end diff --git a/app/task-1.rb b/app/task-1.rb new file mode 100644 index 00000000..71085072 --- /dev/null +++ b/app/task-1.rb @@ -0,0 +1,91 @@ +# Deoptimized version of homework task + +require 'json' +require 'pry' +require 'date' +require 'minitest/autorun' +require 'benchmark' +require 'ruby-progressbar' + +require 'ruby-prof' +require 'stackprof' + +# additional classes +require_relative 'user' +require_relative 'parsing' +require_relative 'report' +require_relative 'bar' + +# FILE_NAME = '../data_large.txt' +FILE_NAME = '../data_small.txt' +# FILE_NAME = '../data.txt' + +def work(disable_gc: false, disable_bar: false) + GC.disable if disable_gc + + users = Parsing.new(FILE_NAME, disable_bar).call + report = Report.new(users, disable_bar).call + + File.write('../result.json', "#{report.to_json}\n") +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 + elapsed_time = Benchmark.realtime { work(disable_bar: true) } + + available_time = 30 + + msg = "Execution time exceeded: #{elapsed_time} seconds. + The available time to complete the test is #{available_time} seconds." + + assert elapsed_time <= available_time, msg + + # assert_equal File.read('data.json'), File.read('result.json') + + # expected_result = '{"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"]}}}' + "\n" + + # assert_equal expected_result, File.read('../result.json') + end + + # def test_profile + # result = RubyProf::Profile.profile { work(disable_gc: true, disable_bar: true) } + # + # printer = RubyProf::CallStackPrinter.new(result) + # printer.print(File.open('../ruby_prof_reports/callstack.html', 'w+')) + # + # printer = RubyProf::GraphHtmlPrinter.new(result) + # printer.print(File.open("../ruby_prof_reports/graph.html", "w+")) + # end + + # def test_stackprof + # profile = StackProf.run(mode: :wall, raw: true) do + # work(disable_gc: true) + # end + # + # File.write('stackprof_reports/stackprof.json', JSON.generate(profile)) + # end +end diff --git a/app/user.rb b/app/user.rb new file mode 100644 index 00000000..c0a90d89 --- /dev/null +++ b/app/user.rb @@ -0,0 +1,8 @@ +class User + attr_reader :attributes, :sessions + + def initialize(attributes:, sessions:) + @attributes = attributes + @sessions = sessions + end +end diff --git a/task-1.rb b/task-1.rb deleted file mode 100644 index 9141c150..00000000 --- a/task-1.rb +++ /dev/null @@ -1,230 +0,0 @@ -# Deoptimized version of homework task - -require 'json' -require 'pry' -require 'date' -require 'minitest/autorun' -require 'benchmark' -require 'ruby-progressbar' - -require 'ruby-prof' -require 'stackprof' - -# FILE_NAME = 'data_large.txt' -# FILE_NAME = 'data_small.txt' -FILE_NAME = 'data.txt' - - -class User - attr_reader :attributes, :sessions - - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions - end -end - -def parse_data(line, result) - fields = line.split(',') - - if fields[0] == 'user' - id = fields[1] - - result[id] = { - first_name: fields[2], - last_name: fields[3], - age: fields[4], - sessions: {} - } - - return result - end - - user_id = fields[1] - session_id = fields[2] - - user = result[user_id] - - sessions = user[:sessions] - - sessions[session_id] = { - browser: fields[3], - time: fields[4], - date: fields[5] - } - - result -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 progress_bar(parts_of_work) - ProgressBar.create( - total: parts_of_work, - format: '%a, %J, %E %B' # elapsed time, percent complete, estimate, bar - # output: File.open(File::NULL, 'w') # IN TEST ENV - ) -end - -def work(disable_gc: false, enable_pb: false) - GC.disable if disable_gc - - file_lines = File.read(FILE_NAME).split("\n") - - progressbar = progress_bar(file_lines.count) - - users = {} - - file_lines.each do |line| - progressbar.increment if enable_pb - users = parse_data(line, users) - end - - # Отчёт в json - # - Сколько всего юзеров + - # - Сколько всего уникальных браузеров + - # - Сколько всего сессий + - # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + - # - # - По каждому пользователю - # - сколько всего сессий + - # - сколько всего времени + - # - самая длинная сессия + - # - браузеры через запятую + - # - Хоть раз использовал IE? + - # - Всегда использовал только Хром? + - # - даты сессий в порядке убывания через запятую + - - report = {} - - report[:totalUsers] = users.count - - # Подсчёт количества уникальных браузеров - unique_browsers = users.values.flat_map do |user| - user[:sessions].values.flat_map { |session| session[:browser] } - end.uniq - - report['uniqueBrowsersCount'] = unique_browsers.count - - report['totalSessions'] = users.values.sum { |user| user[:sessions].size } - - all_browsers = users.values.flat_map do |user| - user[:sessions].values.map { |session| session[:browser].upcase } - end.sort.uniq.join(',') - - report['allBrowsers'] = all_browsers - - # Статистика по пользователям - users_objects = [] - - users.each_value do |user| - sessions = user.delete(:sessions) - - user_object = User.new(attributes: user, sessions: sessions) - - users_objects.append(user_object) - end - - report['usersStats'] = {} - - # Собираем количество сессий по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } - end - - # Собираем количество времени по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'totalTime' => user.sessions.values.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.values.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.values.map {|s| s[:browser]}.map {|b| b.upcase}.sort.join(', ') } - end - - # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.values.map{|s| s[:browser]}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end - - # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.values.map{|s| s[:browser]}.all? { |b| b.upcase =~ /CHROME/ } } - end - - # Даты сессий через запятую в обратном порядке в формате iso8601 - collect_stats_from_users(report, users_objects) do |user| - { 'dates' => user.sessions.values.map{|s| s[:date]}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } - end - - File.write('result.json', "#{report.to_json}\n") -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 - elapsed_time = Benchmark.realtime { work } - - available_time = 2 - - msg = "Execution time exceeded: #{elapsed_time} seconds. - The available time to complete the test is #{available_time} seconds." - - assert elapsed_time <= available_time, msg - - # assert_equal File.read('data.json'), File.read('result.json') - - expected_result = '{"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"]}}}' + "\n" - - assert_equal expected_result, File.read('result.json') - end - - def test_profile - result = RubyProf.profile { work(disable_gc: true, enable_pb: true) } - - printer = RubyProf::GraphHtmlPrinter.new(result) - printer.print(File.open("ruby_prof_reports/graph.html", "w+")) - end - - # def test_stackprof - # profile = StackProf.run(mode: :wall, raw: true) do - # work(disable_gc: true) - # end - # - # File.write('stackprof_reports/stackprof.json', JSON.generate(profile)) - # end -end From 0ff30adbc63416bdd60cf471c80e6c90fa4886b8 Mon Sep 17 00:00:00 2001 From: Oleg Platonov Date: Thu, 30 Jan 2025 17:26:33 +0500 Subject: [PATCH 3/3] upload case-study.rb --- case-study.md | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/case-study.md b/case-study.md index aca769ec..fb236d01 100644 --- a/case-study.md +++ b/case-study.md @@ -59,29 +59,27 @@ Вот какие проблемы удалось найти и решить -### Ваша находка №1 +### Моя находка №1 - Ruby-prof в режиме Graph так же показал точку роста в Array#select - Самое главное что я сделал, это первую обработку данных. Что бы все было структурировано и была некая связь между пользователем и его сессиями. После этого внес правки в весь код, что бы тесты начали выполняться - Метрика стала на много лучше . Запуск программы с данными в 25 000 срок показало работу в 0.8 секунд. Это укладывается в мой первоначальный бюджет. - Теперь профилировщик показывает две точки роста. Это 71% Object#collect_stats_from_users и 50% Array#map -### Ваша находка №2 +### Моя находка №2 - Ruby-prof в режиме Graph так же показал две точки роста 71% в Object#collect_stats_from_users и 50% в Array#map -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? +- Я решил изменить метод collect_stats_from_users и теперь он сам обрабатывает все данные, без передачи блока и лишнего обхода в цикле +- А вот метрика не сильно изменилась, нужно менять подход +- Проблема сохраняется, нужно внести изменения в исходные данные -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? +### Моя находка №3 +- Ruby-prof в режиме Graph так же показал две точки роста 67% в Object#collect_stats_from_users и 50% в Array#map +- Решил изменить самый первый цикл. Сделаю правильный сбор данных который в последствии не нужно будет повторно обрабатывать. +- Метрика стала лучше . Запуск программы с данными в 25 000 срок показало работу в 0.2 секунды. +- Теперь точка роста цикл Array#each в модуле parsing ## Результаты -В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. - -*Какими ещё результами можете поделиться* +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Мне удалось обработать весь файл в среднем на 28 секунд. Это укладывается в мой бюджет. ## Защита от регрессии производительности Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* \ No newline at end of file