From 58304953fdec7ebf392cd74ddc71871981f88d9c Mon Sep 17 00:00:00 2001 From: Danil Date: Tue, 28 Jan 2025 17:04:54 +0300 Subject: [PATCH 1/2] added tests and fixed first N1 --- task-1.rb | 57 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/task-1.rb b/task-1.rb index 778672df..d5f580dd 100644 --- a/task-1.rb +++ b/task-1.rb @@ -4,6 +4,11 @@ require 'pry' require 'date' require 'minitest/autorun' +require 'minitest/benchmark' +require 'benchmark' +require 'byebug' +require 'ruby-progressbar' +require 'ruby-prof' class User attr_reader :attributes, :sessions @@ -16,7 +21,8 @@ def initialize(attributes:, sessions:) def parse_user(user) fields = user.split(',') - parsed_result = { + + { 'id' => fields[1], 'first_name' => fields[2], 'last_name' => fields[3], @@ -26,7 +32,8 @@ def parse_user(user) def parse_session(session) fields = session.split(',') - parsed_result = { + + { 'user_id' => fields[1], 'session_id' => fields[2], 'browser' => fields[3], @@ -43,16 +50,30 @@ def collect_stats_from_users(report, users_objects, &block) end end -def work - file_lines = File.read('data.txt').split("\n") +def work(file_name = 'data.txt') + file_lines = File.read(file_name).split("\n") + + parser_progressbar = ProgressBar.create( + title: "Парсинг пользователей", + total: file_lines.count, + format: '%t, %a, %J, %E, %B' + ) users = [] sessions = [] file_lines.each do |line| + parser_progressbar.increment + cols = line.split(',') - users = users + [parse_user(line)] if cols[0] == 'user' - sessions = sessions + [parse_session(line)] if cols[0] == 'session' + case cols[0] + when 'user' + users.push(parse_user(line)) + when 'session' + sessions.push(parse_session(line)) + else + next + end end # Отчёт в json @@ -169,7 +190,29 @@ def setup end def test_result - work + time = 0 + result = RubyProf::Profile.profile do + time = Benchmark.realtime do + work + end + end + printer = RubyProf::CallTreePrinter.new(result) + printer.print(path: "profilers", profile: 'callgrind') + 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') + + assert time < 30, "Производительность ниже ожидаемой: #{time} секунд" + time_2 = 0 + result = RubyProf::Profile.profile do + time_2 = Benchmark.realtime do + work('data_large.txt') + end + end + + printer = RubyProf::CallTreePrinter.new(result) + printer.print(path: "profilers", profile: 'callgrind') + + assert time_2 < 5, "Производительность ниже ожидаемой: #{time_2} секунд" 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 From 7d2e7231681a6de7e438eb5e9c2f426fd9107258 Mon Sep 17 00:00:00 2001 From: Danil Date: Wed, 29 Jan 2025 13:09:29 +0300 Subject: [PATCH 2/2] Finished homework of first week --- case-study.md | 79 ++++++++++++++++++++++++ task-1.rb | 164 +++++++++++++++++--------------------------------- 2 files changed, 134 insertions(+), 109 deletions(-) create mode 100644 case-study.md diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..4d9dc662 --- /dev/null +++ b/case-study.md @@ -0,0 +1,79 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: Стремится к тому, чтобы метод должен выполняться быстрее 30 секунд. + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: *как вы построили feedback_loop* + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались*: +- добавил прогрессбар, понять, что нет смысла ждать, когда доедет до конца +- добавил ассерты, по которым принял решение, что будут укладываться по времени в нужный нам бюджет +- Добавил ruby-prof и на основе мини файла проверил, как распределяется нагрузка у программы + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- parser_progressbar, показал что огромную часть времени занимает парсинг строк (более 10 минут, хотя должно быть менее 30 секунд) +- вместо сложения массивов использовал Array#push элемента в существующий массив +- уменьшил с >10 мин до 70с +- перестала быть главной точкой роста ибо у users.each (прогрессбар показывает более 4 дней выполнения) + +### Ваша находка №2 +- Подсчёт количества уникальных браузеров занимает времени больше чем 30с, значит в любом случае нужно оптимизировать +- сгруппировать по ключу "браузер" и взять только ключи +- >30c изменилось на моментальную +- перестала быть главной точкой роста + +### Ваша находка №3 +- Сбор браузеров занимает около 1.5 минуты +- использовать результат полученный ранее и вывести в строку +- >1.5мин изменилось на моментальную +- перестала быть главной точкой роста + +### Ваша находка №4 +- users.each (прогрессбар показывает более 4 дней выполнения) +- сгруппировал сессии по user_id вне массива, чтоб не заниматься этим каждый раз, по мимо этого изменил + формат добавления в user_objects использую оператор "<<" вместо сложения сумм массивов + +- изменилась с >4 дней до 52c (Группировка сессий - 43c, Сбор cтатистики по пользователям - 9c) +- перестала быть точкой роста + + ### Ваша находка №5 + - парсинг строк (file_lines.each) показал что работа составляет около 1 минуты и 4с что больше чем 30с + - вместо сложения массивов использовал Array#push элемента в существующий массив + - уменьшил с >10 мин до 7c + - перестала быть главной точкой роста + + ### Ваша находка №6 + - метод collect_stats_from_users на данный момент занимает около 33с работы от 41с вообщем + - добавил предварительные вычисления, теперь все данные для пользователя собираются в одном хеше и сразу обновляют соответствующую запись + - уменьшил с 33с до 15.30c + - перестала быть главной точкой роста + + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с так называемой бесконечности(не смог дождаться завершения) до 17-18 секунд и уложиться в заданный бюджет. + +*Какими ещё результами можете поделиться* + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавлен тест о том, что он должен выполняться быстрее 30с diff --git a/task-1.rb b/task-1.rb index d5f580dd..84e0ef07 100644 --- a/task-1.rb +++ b/task-1.rb @@ -7,8 +7,6 @@ require 'minitest/benchmark' require 'benchmark' require 'byebug' -require 'ruby-progressbar' -require 'ruby-prof' class User attr_reader :attributes, :sessions @@ -19,58 +17,50 @@ def initialize(attributes:, sessions:) end end -def parse_user(user) - fields = user.split(',') - - { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], - } -end - -def parse_session(session) - fields = session.split(',') - - { - 'user_id' => fields[1], - 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[5], - } -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)) + + sessions = user.sessions + times = [] + browsers = [] + dates = [] + + sessions.each do |session| + times << session['time'].to_i + browsers << session['browser'] + dates << Date.strptime(session['date'], '%Y-%m-%d') + end + + user_stats = block.call(user, times, browsers, dates) + report['usersStats'][user_key].merge!(user_stats) end end def work(file_name = 'data.txt') - file_lines = File.read(file_name).split("\n") - - parser_progressbar = ProgressBar.create( - title: "Парсинг пользователей", - total: file_lines.count, - format: '%t, %a, %J, %E, %B' - ) - users = [] sessions = [] - file_lines.each do |line| - parser_progressbar.increment - + File.foreach(file_name).each do |line| cols = line.split(',') + case cols[0] when 'user' - users.push(parse_user(line)) + users << { + 'id' => cols[1], + 'first_name' => cols[2], + 'last_name' => cols[3], + 'age' => cols[4], + } when 'session' - sessions.push(parse_session(line)) + sessions << { + 'user_id' => cols[1], + 'session_id' => cols[2], + 'browser' => cols[3].upcase, + 'time' => cols[4], + 'date' => cols[5], + } else next end @@ -96,69 +86,40 @@ def work(file_name = 'data.txt') report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end + uniqueBrowsers = sessions.group_by { |session| session["browser"] }.keys report['uniqueBrowsersCount'] = uniqueBrowsers.count report['totalSessions'] = sessions.count - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') + report['allBrowsers'] = uniqueBrowsers.sort.join(',') + # Статистика по пользователям users_objects = [] + sessions_by_user = {} - 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 + sessions_by_user = sessions.group_by { |session| session['user_id'] } - 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 + users.each do |user| + user_sessions = sessions_by_user[user['id']] || [] + user_object = User.new(attributes: user, sessions: user_sessions) - # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } + users_objects << user_object end - # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } - end + report['usersStats'] = {} - # Даты сессий через запятую в обратном порядке в формате 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 } } + collect_stats_from_users(report, users_objects) do |user, times, browsers, dates| + { + 'sessionsCount' => user.sessions.count, + 'totalTime' => "#{times.sum} min.", + 'longestSession' => "#{times.max} min.", + 'browsers' => browsers.sort.join(', '), + 'usedIE' => browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, + 'alwaysUsedChrome' => browsers.all? { |b| b =~ /CHROME/ }, + 'dates' => dates.sort.reverse.map(&:iso8601) + } end File.write('result.json', "#{report.to_json}\n") @@ -190,30 +151,15 @@ def setup end def test_result - time = 0 - result = RubyProf::Profile.profile do - time = Benchmark.realtime do - work - end - end - printer = RubyProf::CallTreePrinter.new(result) - printer.print(path: "profilers", profile: 'callgrind') + work + 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') - assert time < 30, "Производительность ниже ожидаемой: #{time} секунд" - time_2 = 0 - result = RubyProf::Profile.profile do - time_2 = Benchmark.realtime do - work('data_large.txt') - end + time_2 = Benchmark.realtime do + work('data_large.txt') end - printer = RubyProf::CallTreePrinter.new(result) - printer.print(path: "profilers", profile: 'callgrind') - - assert time_2 < 5, "Производительность ниже ожидаемой: #{time_2} секунд" - 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') + assert time_2 < 30, "Производительность ниже ожидаемой: #{time_2} секунд" end end