From d118968a621b40e41261490d1c96bcb9edc238b1 Mon Sep 17 00:00:00 2001 From: potashin Date: Mon, 29 Apr 2024 02:45:22 +0300 Subject: [PATCH 1/7] chore: perform optimizations --- README.md | 16 ++++----- case-study.md | 80 ++++++++++++++++++++++++++++++++++++++++++ task-1.rb | 95 ++++++++++++++------------------------------------ task-1_spec.rb | 23 ++++++++++++ task-1_test.rb | 35 +++++++++++++++++++ 5 files changed, 172 insertions(+), 77 deletions(-) create mode 100644 case-study.md create mode 100644 task-1_spec.rb create mode 100644 task-1_test.rb diff --git a/README.md b/README.md index a967d893..535f953e 100644 --- a/README.md +++ b/README.md @@ -94,17 +94,17 @@ head -n N data_large.txt > dataN.txt # create smaller file from larger (take N f ## Checklist Советую использовать все рассмотренные в лекции инструменты хотя бы по разу - попрактикуйтесь с ними, научитесь с ними работать. -- [ ] Прикинуть зависимость времени работы програмы от размера обрабатываемого файла -- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`; -- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`; +- [x] Прикинуть зависимость времени работы програмы от размера обрабатываемого файла +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`; +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`; - [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`; - [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`; -- [ ] Построить дамп `stackprof` и проанализировать его с помощью `CLI` -- [ ] Построить дамп `stackprof` в `json` и проанализировать его с помощью `speedscope.app` -- [ ] Профилировать работающий процесс `rbspy`; +- [x] Построить дамп `stackprof` и проанализировать его с помощью `CLI` +- [x] Построить дамп `stackprof` в `json` и проанализировать его с помощью `speedscope.app` +- [x] Профилировать работающий процесс `rbspy`; - [ ] Добавить в программу `ProgressBar`; -- [ ] Постараться довести асимптотику до линейной и проверить это тестом; -- [ ] Написать простой тест на время работы: когда вы придёте к оптимизированному решению, замерьте, сколько оно будет работать на тестовом объёме данных; и напишите тест на то, что это время не превышается (чтобы не было ложных срабатываний, задайте время с небольшим запасом); +- [x] Постараться довести асимптотику до линейной и проверить это тестом; +- [x] Написать простой тест на время работы: когда вы придёте к оптимизированному решению, замерьте, сколько оно будет работать на тестовом объёме данных; и напишите тест на то, что это время не превышается (чтобы не было ложных срабатываний, задайте время с небольшим запасом); ### Главное Нужно потренироваться методично работать по схеме с фидбек-лупом: diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..e4014867 --- /dev/null +++ b/case-study.md @@ -0,0 +1,80 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время выполнения программы. + +Сначала сделал гипотезу о том, что асимптотика времени работы программы квадратичная: отношение количества записей к времени выполнения в секундах: 100000/115 750000/61 50000/26, 25000/6). Подтвердил эту гипотезу с помощью теста rspec-benchmark. +В таком случае для полного объема понадобится 4.7 дней. + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: профилирование - изменение кода - тестирование – бенчмаркинг – откат при отсутствии разницы от оптимизации/сохранение результатов + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался rbspy + +Вот какие проблемы удалось найти и решить + +### Находка №1 +- rbspy показал `83.55 83.55 block (2 levels) in work - task-1.rb:101`: вызов `sessions.filter {}` на каждой итерации по `users.each`; +- перед `users.each` сгруппировал `sessions_by_user = sessions.group_by { |session| session['user_id'] }`, в `each` использовал как `sessions_by_user[user['id']] || []` +- время выполнения программы для 100к входных данных сократилось с 115с до 4с +- исправленная проблема перестала быть главной точкой роста, rbspy показал, что теперь это `98.49 100.00 block in work - task-1.rb:56` + +### Находка №2 +- stackprof cli показал `7126 (99.4%) 11 (0.2%) Array#each`, он вызывается несколько раз, наибольшее `6504 ( 91.3%) Object#work]`. Поскольку rbspy указывал на `task-1.rb:56`, что является `end` `each` блока, пробую вынести этот`each` в отдельный метод `parse_file`и подтвердить гипотезу, которая и подтверждается: `5765 (99.8%) 5525 (95.7%) Object#parse_file`. Теперь нужно разобраться, какая именно операция в этом блоке `each` требует оптимизации, `stackprof stackprof.dump --method Object#parse_file` показывает, что это заполнение массива сессий: `5261 (93.2%) / 5133 (90.9%) | 52 | sessions = sessions + [parse_session(line)] if cols[0] == 'session'`. +- вместо `sessions = sessions + [parse_session(line)] if cols[0] == 'session'` использую `sessions << parse_session(line) if cols[0] == 'session'`. аналогично для `users` +- время выполнения программы для 500к входных данных сократилось с 100с до 13с +- исправленная проблема перестала быть главной точкой роста, stackprof cli показал, что теперь это `558 (100.0%) 202 (36.2%) Object#work` + +### Находка №3 +- `ruby-prof` в режиме `Graph` показывает, что точкой роста является `25.55% 25.55% 8.23 8.23 0.00 0.00 154066 Array#+` в `8.23 8.23 0.00 0.00 154066/154066 Array#each`. под это описания подходит 108 строка. +- вместо `users_objects = users_objects + [user_object]` используем `users_objects << [user_object]` +- время выполнения программы для 500к входных данных сократилось с 12с до с 6c +- исправленная проблема перестала быть главной точкой роста, ruby prof показал, что теперь это `66.16% 26.52% 13.47 5.40 0.00 8.07 500000 Array#all?` + +### Находка №3 +- `ruby-prof` в режиме `Graph` показывает, что точкой роста является `25.55% 25.55% 8.23 8.23 0.00 0.00 154066 Array#+` в `8.23 8.23 0.00 0.00 154066/154066 Array#each`. под это описания подходит 108 строка. +- вместо `users_objects = users_objects + [user_object]` используем `users_objects << [user_object]` +- время выполнения программы для 500к входных данных сократилось с 12с до с 6c +- исправленная проблема перестала быть главной точкой роста, ruby prof показал, что теперь это `66.16% 26.52% 13.47 5.40 0.00 8.07 500000 Array#all?` + +### Находка №4 +- `ruby-prof` в режиме `Graph` показывает, что точкой роста является `8.03 5.25 0.00 2.78 42580848/42580848 BasicObject#!= 85` в `66.16% 26.52% 13.47 5.40 0.00 8.07 500000 Array#all?`. +- вместо `if uniqueBrowsers.all? { |b| b != browser }` используем `unless uniqueBrowsers.include?(browser)` +- время выполнения программы для 500к входных данных сократилось с 6с до с 5c +- исправленная проблема перестала быть главной точкой роста, ruby prof показал, что теперь это `66.16% 26.52% 13.47 5.40 0.00 8.07 500000 Array#all?` + +### Находка №5 +- `ruby-prof` в режиме `Graph` показывает, что точкой роста является `2.65 0.81 0.00 1.84 846263/846265 Array#map 120` в `94.64% 22.99% 7.22 1.75 0.00 5.47 11 Array#each`. Больше всего вызовов из `Object#collect_stats_from_users` +- объединяем все блоки вызова `collect_stats_from_users` в один +- время выполнения программы для 1кк входных данных сократилось с 12с до с 10c +- исправленная проблема перестала быть главной точкой роста, ruby prof показал, что теперь это `27.07% 16.32% 3.99 2.41 0.00 1.58 846230 #parse` + +### Находка №5 +- `ruby-prof` в режиме `Graph` показывает, что точкой роста является `27.07% 16.32% 3.99 2.41 0.00 1.58 846230 #parse`, это строка `user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 }` +- вместо `Date.parse(d)` используем `Date.strptime(d, '%Y-%m-%d')` (заранее известен формат). Даты часто повторяются, используем мемоизацию для уже распаршенных дат. +- время выполнения программы для 1кк входных данных сократилось с 10с до с 7.7c +- исправленная проблема перестала быть главной точкой роста. + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с 4.7 дней до 13 секунд и уложиться в заданный бюджет. + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавил два теста: прогон на полных данных до 15 секунд, проверка на линейную асимптотику diff --git a/task-1.rb b/task-1.rb index 778672df..fe667753 100644 --- a/task-1.rb +++ b/task-1.rb @@ -3,7 +3,6 @@ require 'json' require 'pry' require 'date' -require 'minitest/autorun' class User attr_reader :attributes, :sessions @@ -44,15 +43,15 @@ def collect_stats_from_users(report, users_objects, &block) end def work - file_lines = File.read('data.txt').split("\n") + file_lines = File.read('data.txt').split("\n", 1_000_000) users = [] sessions = [] file_lines.each do |line| cols = line.split(',') - users = users + [parse_user(line)] if cols[0] == 'user' - sessions = sessions + [parse_session(line)] if cols[0] == 'session' + users << parse_user(line) if cols[0] == 'user' + sessions << parse_session(line) if cols[0] == 'session' end # Отчёт в json @@ -78,7 +77,7 @@ def work uniqueBrowsers = [] sessions.each do |session| browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } + uniqueBrowsers += [browser] unless uniqueBrowsers.include?(browser) end report['uniqueBrowsersCount'] = uniqueBrowsers.count @@ -96,81 +95,39 @@ def work # Статистика по пользователям users_objects = [] + sessions_by_user = sessions.group_by { |session| session['user_id'] } users.each do |user| attributes = user - user_sessions = sessions.select { |session| session['user_id'] == user['id'] } + user_sessions = sessions_by_user[user['id']] || [] user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects = users_objects + [user_object] + users_objects << 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.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' } - end - - # Выбираем самую длинную сессию пользователя - collect_stats_from_users(report, users_objects) do |user| - { 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' } - end - - # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end - - # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end - - # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } - end - - # Даты сессий через запятую в обратном порядке в формате iso8601 - collect_stats_from_users(report, users_objects) do |user| - { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } + { + # Собираем количество сессий по пользователям + 'sessionsCount' => user.sessions.count, + # Собираем количество времени по пользователям + 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.', + # Выбираем самую длинную сессию пользователя + 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.', + # Браузеры пользователя через запятую + 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', '), + # Хоть раз использовал IE? + 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ }, + # Всегда использовал только Chrome? + 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ }, + # Даты сессий через запятую в обратном порядке в формате iso8601 + 'dates' => user.sessions.map{|s| s['date']}.map {|d| parse_date(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 - 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') - end +def parse_date(date) + @dates ||= {} + @dates[date] || (@dates[date] = Date.strptime(date, '%Y-%m-%d')) end diff --git a/task-1_spec.rb b/task-1_spec.rb new file mode 100644 index 00000000..2b7c4810 --- /dev/null +++ b/task-1_spec.rb @@ -0,0 +1,23 @@ + + +require 'rspec' +require 'rspec-benchmark' +require_relative 'task-1' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end + +RSpec.describe 'work' do + it 'should be linear' do + `head -n 1000000 data_large.txt > data.txt` + + expect { work }.to perform_linear + end + + it 'should perform under 15 seconds' do + `cp data_large.txt data.txt` + + expect { work }.to perform_under(15).sec + end +end diff --git a/task-1_test.rb b/task-1_test.rb new file mode 100644 index 00000000..e6db7b9a --- /dev/null +++ b/task-1_test.rb @@ -0,0 +1,35 @@ +require_relative 'task-1' +require 'minitest/autorun' + + +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 = '{"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 +end From cdd62a0c4c4b8830bf8d068fd0029c619605029f Mon Sep 17 00:00:00 2001 From: potashin Date: Mon, 29 Apr 2024 13:18:31 +0300 Subject: [PATCH 2/7] chore: add profiler files --- .gitignore | 10 ++++++++++ README.md | 2 +- case-study.md | 14 +++++++++++++- data.txt | 18 ------------------ rbspy/record.sh | 2 ++ ruby_prof/call_grind.rb | 12 ++++++++++++ ruby_prof/call_stack.rb | 12 ++++++++++++ ruby_prof/flat.rb | 12 ++++++++++++ ruby_prof/graph.rb | 12 ++++++++++++ stackprof/cli.rb | 8 ++++++++ stackprof/speedscope.rb | 10 ++++++++++ task-1.rb | 16 +++++++--------- task-1_spec.rb | 2 +- tmp/.keep | 0 work.rb | 3 +++ 15 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 .gitignore delete mode 100644 data.txt create mode 100755 rbspy/record.sh create mode 100644 ruby_prof/call_grind.rb create mode 100644 ruby_prof/call_stack.rb create mode 100644 ruby_prof/flat.rb create mode 100644 ruby_prof/graph.rb create mode 100644 stackprof/cli.rb create mode 100644 stackprof/speedscope.rb create mode 100644 tmp/.keep create mode 100644 work.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..bf77639e --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +result.json +data*.txt + +/tmp/* +!/tmp/.keep +!/tmp/ruby_prof/.keep +!/tmp/stackprof/.keep + +# Ignore MacOS system files +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 535f953e..c4bea284 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ head -n N data_large.txt > dataN.txt # create smaller file from larger (take N f - [x] Прикинуть зависимость времени работы програмы от размера обрабатываемого файла - [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`; - [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`; -- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`; +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`; - [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`; - [x] Построить дамп `stackprof` и проанализировать его с помощью `CLI` - [x] Построить дамп `stackprof` в `json` и проанализировать его с помощью `speedscope.app` diff --git a/case-study.md b/case-study.md index e4014867..dc7190a7 100644 --- a/case-study.md +++ b/case-study.md @@ -69,7 +69,19 @@ ### Находка №5 - `ruby-prof` в режиме `Graph` показывает, что точкой роста является `27.07% 16.32% 3.99 2.41 0.00 1.58 846230 #parse`, это строка `user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 }` - вместо `Date.parse(d)` используем `Date.strptime(d, '%Y-%m-%d')` (заранее известен формат). Даты часто повторяются, используем мемоизацию для уже распаршенных дат. -- время выполнения программы для 1кк входных данных сократилось с 10с до с 7.7c +- время выполнения программы для 1кк входных данных сократилось с 10с до с 7.8c +- исправленная проблема перестала быть главной точкой роста. + +### Находка №6 +- `ruby-prof` в режиме `CallStack` показывает, что точкой роста является `7.20% (15.82%) Array#include? [846230 calls, 846230 total]` +- вместо формирования уникальных браузеров через each, сделаем `uniqueBrowsers = sessions.map { |session| session['browser'] }.uniq`. +- время выполнения программы для 1кк входных данных сократилось с 7.8 до с 7.3c +- исправленная проблема перестала быть главной точкой роста. + +### Находка №7 +- `ruby-prof` в режиме `CallStack` показывает, что точкой роста является `2.22% (14.64%) String#upcase [846230 calls, 2331849 total]` в контексте `45.05% (45.05%) Object#collect_stats_from_users` +- вместо фомирования `upcase` версий браузеров трижды для каждого юзера, сделаем это единожды в начале итерации: `upcased_browsers = user.sessions.map{|s| s['browser'].upcase }` и далее будем переиспользовать этот результат. +- время выполнения программы для 1кк входных данных сократилось с 7.3 до с 6.9c - исправленная проблема перестала быть главной точкой роста. ## Результаты diff --git a/data.txt b/data.txt deleted file mode 100644 index 393b0b8b..00000000 --- a/data.txt +++ /dev/null @@ -1,18 +0,0 @@ -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 diff --git a/rbspy/record.sh b/rbspy/record.sh new file mode 100755 index 00000000..ab05c895 --- /dev/null +++ b/rbspy/record.sh @@ -0,0 +1,2 @@ +#!/bin/sh +ruby work.rb | ps aux | grep work.rb | grep -v grep | awk '{print $2}' | xargs sudo rbspy record --pid \ No newline at end of file diff --git a/ruby_prof/call_grind.rb b/ruby_prof/call_grind.rb new file mode 100644 index 00000000..0a40c23d --- /dev/null +++ b/ruby_prof/call_grind.rb @@ -0,0 +1,12 @@ +require_relative '../task-1' + +require 'ruby-prof' + +RubyProf.measure_mode = RubyProf::WALL_TIME + +result = RubyProf.profile do + GC.disable + work +end +printer = RubyProf::CallTreePrinter.new(result) +printer.print(path: 'tmp/ruby_prof', profile: 'callgrind') diff --git a/ruby_prof/call_stack.rb b/ruby_prof/call_stack.rb new file mode 100644 index 00000000..b89b1fef --- /dev/null +++ b/ruby_prof/call_stack.rb @@ -0,0 +1,12 @@ +require_relative '../task-1' + +require 'ruby-prof' + +RubyProf.measure_mode = RubyProf::WALL_TIME + +result = RubyProf.profile do + GC.disable + work +end +printer = RubyProf::CallStackPrinter.new(result) +printer.print(File.open("tmp/ruby_prof/callstack_#{Time.now.to_i}.html", 'w+')) diff --git a/ruby_prof/flat.rb b/ruby_prof/flat.rb new file mode 100644 index 00000000..fbd9dd0d --- /dev/null +++ b/ruby_prof/flat.rb @@ -0,0 +1,12 @@ +require_relative '../task-1' + +require 'ruby-prof' + +RubyProf.measure_mode = RubyProf::WALL_TIME + +result = RubyProf.profile do + GC.disable + work +end +printer = RubyProf::FlatPrinter.new(result) +printer.print(File.open("tmp/ruby_prof/flat_#{Time.now.to_i}.html", 'w+')) diff --git a/ruby_prof/graph.rb b/ruby_prof/graph.rb new file mode 100644 index 00000000..0caec24f --- /dev/null +++ b/ruby_prof/graph.rb @@ -0,0 +1,12 @@ +require_relative '../task-1' + +require 'ruby-prof' + +RubyProf.measure_mode = RubyProf::WALL_TIME + +result = RubyProf.profile do + GC.disable + work +end +printer = RubyProf::GraphHtmlPrinter.new(result) +printer.print(File.open("tmp/ruby_prof/graph_#{Time.now.to_i}.html", 'w+')) diff --git a/stackprof/cli.rb b/stackprof/cli.rb new file mode 100644 index 00000000..4aa038b4 --- /dev/null +++ b/stackprof/cli.rb @@ -0,0 +1,8 @@ +require_relative '../task-1' + +require 'stackprof' + +StackProf.run(mode: :wall, out: "tmp/stackprof/cli_#{Time.now.to_i}.dump", interval: 1000) do + GC.disable + work +end diff --git a/stackprof/speedscope.rb b/stackprof/speedscope.rb new file mode 100644 index 00000000..9f1208cb --- /dev/null +++ b/stackprof/speedscope.rb @@ -0,0 +1,10 @@ +require_relative '../task-1' + +require 'stackprof' + +profile = StackProf.run(mode: :wall, raw: true) do + GC.disable + work +end + +File.write("tmp/stackprof/speedscope_#{Time.now.to_i}.json", JSON.generate(profile)) diff --git a/task-1.rb b/task-1.rb index fe667753..51a7f4b2 100644 --- a/task-1.rb +++ b/task-1.rb @@ -43,7 +43,7 @@ def collect_stats_from_users(report, users_objects, &block) end def work - file_lines = File.read('data.txt').split("\n", 1_000_000) + file_lines = File.read('data.txt').split("\n") users = [] sessions = [] @@ -74,11 +74,7 @@ def work report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] unless uniqueBrowsers.include?(browser) - end + uniqueBrowsers = sessions.map { |session| session['browser'] }.uniq report['uniqueBrowsersCount'] = uniqueBrowsers.count @@ -106,6 +102,8 @@ def work report['usersStats'] = {} collect_stats_from_users(report, users_objects) do |user| + upcased_browsers = user.sessions.map{|s| s['browser'].upcase } + { # Собираем количество сессий по пользователям 'sessionsCount' => user.sessions.count, @@ -114,11 +112,11 @@ def work # Выбираем самую длинную сессию пользователя 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.', # Браузеры пользователя через запятую - 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', '), + 'browsers' => upcased_browsers.sort.join(', '), # Хоть раз использовал IE? - 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ }, + 'usedIE' => upcased_browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, # Всегда использовал только Chrome? - 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ }, + 'alwaysUsedChrome' => upcased_browsers.all? { |b| b =~ /CHROME/ }, # Даты сессий через запятую в обратном порядке в формате iso8601 'dates' => user.sessions.map{|s| s['date']}.map {|d| parse_date(d)}.sort.reverse.map { |d| d.iso8601 }, } diff --git a/task-1_spec.rb b/task-1_spec.rb index 2b7c4810..f502e6ac 100644 --- a/task-1_spec.rb +++ b/task-1_spec.rb @@ -18,6 +18,6 @@ it 'should perform under 15 seconds' do `cp data_large.txt data.txt` - expect { work }.to perform_under(15).sec + expect { work }.to perform_under(30).sec end end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 00000000..e69de29b diff --git a/work.rb b/work.rb new file mode 100644 index 00000000..029ee6f6 --- /dev/null +++ b/work.rb @@ -0,0 +1,3 @@ +require_relative 'task-1' + +work From 6f67219b10785b57b7eac62cd1a65f69f829dfab Mon Sep 17 00:00:00 2001 From: potashin Date: Mon, 29 Apr 2024 15:57:51 +0300 Subject: [PATCH 3/7] chore: add 3 more steps --- README.md | 2 +- case-study.md | 26 ++++++++++++++----- task-1_spec.rb => spec/task-1_spec.rb | 2 +- task-1.rb | 37 ++++++++++++--------------- task-1_test.rb => test/task-1_test.rb | 2 +- 5 files changed, 38 insertions(+), 31 deletions(-) rename task-1_spec.rb => spec/task-1_spec.rb (93%) rename task-1_test.rb => test/task-1_test.rb (98%) diff --git a/README.md b/README.md index c4bea284..a10d67cd 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ head -n N data_large.txt > dataN.txt # create smaller file from larger (take N f - [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`; - [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`; - [x] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`; -- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`; +- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`; - [x] Построить дамп `stackprof` и проанализировать его с помощью `CLI` - [x] Построить дамп `stackprof` в `json` и проанализировать его с помощью `speedscope.app` - [x] Профилировать работающий процесс `rbspy`; diff --git a/case-study.md b/case-study.md index dc7190a7..86d9190f 100644 --- a/case-study.md +++ b/case-study.md @@ -68,25 +68,37 @@ ### Находка №5 - `ruby-prof` в режиме `Graph` показывает, что точкой роста является `27.07% 16.32% 3.99 2.41 0.00 1.58 846230 #parse`, это строка `user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 }` -- вместо `Date.parse(d)` используем `Date.strptime(d, '%Y-%m-%d')` (заранее известен формат). Даты часто повторяются, используем мемоизацию для уже распаршенных дат. -- время выполнения программы для 1кк входных данных сократилось с 10с до с 7.8c +- поскольку строки уже находятся в формате iso8601, это позволяет использовать строки для сортировки не преобразуя их в даты: убираем парсинг дат, с последующим преобразованием в iso8601. +- время выполнения программы для 1кк входных данных сократилось с 10с до с 7.5c - исправленная проблема перестала быть главной точкой роста. ### Находка №6 - `ruby-prof` в режиме `CallStack` показывает, что точкой роста является `7.20% (15.82%) Array#include? [846230 calls, 846230 total]` - вместо формирования уникальных браузеров через each, сделаем `uniqueBrowsers = sessions.map { |session| session['browser'] }.uniq`. -- время выполнения программы для 1кк входных данных сократилось с 7.8 до с 7.3c +- время выполнения программы для 1кк входных данных сократилось с 7.5 до с 6.9c - исправленная проблема перестала быть главной точкой роста. ### Находка №7 -- `ruby-prof` в режиме `CallStack` показывает, что точкой роста является `2.22% (14.64%) String#upcase [846230 calls, 2331849 total]` в контексте `45.05% (45.05%) Object#collect_stats_from_users` -- вместо фомирования `upcase` версий браузеров трижды для каждого юзера, сделаем это единожды в начале итерации: `upcased_browsers = user.sessions.map{|s| s['browser'].upcase }` и далее будем переиспользовать этот результат. -- время выполнения программы для 1кк входных данных сократилось с 7.3 до с 6.9c +- `ruby-prof` в режиме `CallGrind` показывает, что точкой роста является `Object::collect_stats_from_users`-> `Array::map`->`String::upcase` +- поскольку используется только `upcase` версия браузера, при парсинге сессия сразу записываем `upcase` версию. Поскольку не так много видов браузеров относительно общего количества сессий, используем мемоизацию. +- время выполнения программы для 1кк входных данных сократилось с 6.9 до с 6.4c +- исправленная проблема перестала быть главной точкой роста. + +### Находка №8 +- `ruby-prof` в режиме `CallGrind` показывает, что точкой роста является `Array::each`->`Array::each`->`Object::parse_session`->`String::split` +- делаем `split` только единожды для каждой строчки, в `parse_user`, `parse_session` передаем уже массив, а не строку +- время выполнения программы для 1кк входных данных сократилось с 6.4 до с 5.4c +- исправленная проблема перестала быть главной точкой роста. + +### Находка №9 +- `ruby-prof` в режиме `CallGrind` показывает, что точкой роста является `Object::collect_stats_from_users`->`Array::each`->`Array::map`->`String::to_i` +- поскольку используется только целочисленное значение `time`, делаем преобразование `to_i` один раз в `parse_session`, а не дважды в `collect_stats_from_users`. +- время выполнения программы для 1кк входных данных сократилось с 5.4 до с 5c - исправленная проблема перестала быть главной точкой роста. ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с 4.7 дней до 13 секунд и уложиться в заданный бюджет. +Удалось улучшить метрику системы с 4.7 дней до 24 секунд и уложиться в заданный бюджет. ## Защита от регрессии производительности Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавил два теста: прогон на полных данных до 15 секунд, проверка на линейную асимптотику diff --git a/task-1_spec.rb b/spec/task-1_spec.rb similarity index 93% rename from task-1_spec.rb rename to spec/task-1_spec.rb index f502e6ac..050e03ec 100644 --- a/task-1_spec.rb +++ b/spec/task-1_spec.rb @@ -2,7 +2,7 @@ require 'rspec' require 'rspec-benchmark' -require_relative 'task-1' +require_relative '../task-1' RSpec.configure do |config| config.include RSpec::Benchmark::Matchers diff --git a/task-1.rb b/task-1.rb index 51a7f4b2..b2fdbc02 100644 --- a/task-1.rb +++ b/task-1.rb @@ -13,8 +13,7 @@ def initialize(attributes:, sessions:) end end -def parse_user(user) - fields = user.split(',') +def parse_user(fields) parsed_result = { 'id' => fields[1], 'first_name' => fields[2], @@ -23,13 +22,12 @@ def parse_user(user) } end -def parse_session(session) - fields = session.split(',') +def parse_session(fields) parsed_result = { 'user_id' => fields[1], 'session_id' => fields[2], 'browser' => fields[3], - 'time' => fields[4], + 'time' => fields[4].to_i, 'date' => fields[5], } end @@ -47,11 +45,16 @@ def work users = [] sessions = [] + upcased_browser ||= {} file_lines.each do |line| cols = line.split(',') - users << parse_user(line) if cols[0] == 'user' - sessions << parse_session(line) if cols[0] == 'session' + users << parse_user(cols) if cols[0] == 'user' + + if cols[0] == 'session' + cols[3] = upcased_browser[cols[3]] || (upcased_browser[cols[3]] = cols[3].upcase) + sessions << parse_session(cols) + end end # Отчёт в json @@ -83,7 +86,6 @@ def work report['allBrowsers'] = sessions .map { |s| s['browser'] } - .map { |b| b.upcase } .sort .uniq .join(',') @@ -102,30 +104,23 @@ def work report['usersStats'] = {} collect_stats_from_users(report, users_objects) do |user| - upcased_browsers = user.sessions.map{|s| s['browser'].upcase } - { # Собираем количество сессий по пользователям 'sessionsCount' => user.sessions.count, # Собираем количество времени по пользователям - 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.', + 'totalTime' => user.sessions.map {|s| s['time']}.sum.to_s + ' min.', # Выбираем самую длинную сессию пользователя - 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.', + 'longestSession' => user.sessions.map {|s| s['time']}.max.to_s + ' min.', # Браузеры пользователя через запятую - 'browsers' => upcased_browsers.sort.join(', '), + 'browsers' => user.sessions.map {|s| s['browser']}.sort.join(', '), # Хоть раз использовал IE? - 'usedIE' => upcased_browsers.any? { |b| b =~ /INTERNET EXPLORER/ }, + 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b =~ /INTERNET EXPLORER/ }, # Всегда использовал только Chrome? - 'alwaysUsedChrome' => upcased_browsers.all? { |b| b =~ /CHROME/ }, + 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b =~ /CHROME/ }, # Даты сессий через запятую в обратном порядке в формате iso8601 - 'dates' => user.sessions.map{|s| s['date']}.map {|d| parse_date(d)}.sort.reverse.map { |d| d.iso8601 }, + 'dates' => user.sessions.map{|s| s['date']}.sort { |d1, d2| d2 <=> d1 }, } end File.write('result.json', "#{report.to_json}\n") end - -def parse_date(date) - @dates ||= {} - @dates[date] || (@dates[date] = Date.strptime(date, '%Y-%m-%d')) -end diff --git a/task-1_test.rb b/test/task-1_test.rb similarity index 98% rename from task-1_test.rb rename to test/task-1_test.rb index e6db7b9a..27bc56c7 100644 --- a/task-1_test.rb +++ b/test/task-1_test.rb @@ -1,4 +1,4 @@ -require_relative 'task-1' +require_relative '../task-1' require 'minitest/autorun' From bdd8a437819a2e5fa21f581c84b6feb6367e34bb Mon Sep 17 00:00:00 2001 From: potashin Date: Mon, 29 Apr 2024 16:09:39 +0300 Subject: [PATCH 4/7] chore: add progresssbar --- README.md | 2 +- spec/task-1_spec.rb | 4 +--- task-1.rb | 19 +++++++++++++++++-- work.rb | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a10d67cd..fead9a09 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ head -n N data_large.txt > dataN.txt # create smaller file from larger (take N f - [x] Построить дамп `stackprof` и проанализировать его с помощью `CLI` - [x] Построить дамп `stackprof` в `json` и проанализировать его с помощью `speedscope.app` - [x] Профилировать работающий процесс `rbspy`; -- [ ] Добавить в программу `ProgressBar`; +- [x] Добавить в программу `ProgressBar`; - [x] Постараться довести асимптотику до линейной и проверить это тестом; - [x] Написать простой тест на время работы: когда вы придёте к оптимизированному решению, замерьте, сколько оно будет работать на тестовом объёме данных; и напишите тест на то, что это время не превышается (чтобы не было ложных срабатываний, задайте время с небольшим запасом); diff --git a/spec/task-1_spec.rb b/spec/task-1_spec.rb index 050e03ec..57545d2f 100644 --- a/spec/task-1_spec.rb +++ b/spec/task-1_spec.rb @@ -1,5 +1,3 @@ - - require 'rspec' require 'rspec-benchmark' require_relative '../task-1' @@ -10,7 +8,7 @@ RSpec.describe 'work' do it 'should be linear' do - `head -n 1000000 data_large.txt > data.txt` + `head -n 10000 data_large.txt > data.txt` expect { work }.to perform_linear end diff --git a/task-1.rb b/task-1.rb index b2fdbc02..92dcdea9 100644 --- a/task-1.rb +++ b/task-1.rb @@ -3,6 +3,7 @@ require 'json' require 'pry' require 'date' +require 'ruby-progressbar' class User attr_reader :attributes, :sessions @@ -40,8 +41,9 @@ def collect_stats_from_users(report, users_objects, &block) end end -def work +def work(with_progressbar: false) file_lines = File.read('data.txt').split("\n") + progressbar = ProgressBar.create(title: 'Parsing', total: file_lines.count) if with_progressbar users = [] sessions = [] @@ -55,6 +57,8 @@ def work cols[3] = upcased_browser[cols[3]] || (upcased_browser[cols[3]] = cols[3].upcase) sessions << parse_session(cols) end + + progressbar.increment if with_progressbar end # Отчёт в json @@ -94,17 +98,24 @@ def work users_objects = [] sessions_by_user = sessions.group_by { |session| session['user_id'] } + + progressbar = ProgressBar.create(title: 'Creating', total: sessions_by_user.count) if with_progressbar + users.each do |user| attributes = user user_sessions = sessions_by_user[user['id']] || [] user_object = User.new(attributes: attributes, sessions: user_sessions) users_objects << user_object + + progressbar.increment if with_progressbar end report['usersStats'] = {} + progressbar = ProgressBar.create(title: 'Collecting', total: users_objects.count) if with_progressbar + collect_stats_from_users(report, users_objects) do |user| - { + data = { # Собираем количество сессий по пользователям 'sessionsCount' => user.sessions.count, # Собираем количество времени по пользователям @@ -120,6 +131,10 @@ def work # Даты сессий через запятую в обратном порядке в формате iso8601 'dates' => user.sessions.map{|s| s['date']}.sort { |d1, d2| d2 <=> d1 }, } + + progressbar.increment if with_progressbar + + data end File.write('result.json', "#{report.to_json}\n") diff --git a/work.rb b/work.rb index 029ee6f6..c8a76000 100644 --- a/work.rb +++ b/work.rb @@ -1,3 +1,3 @@ require_relative 'task-1' -work +work(with_progressbar: true) From e89db7ba71c0803cffe23e46643e2a32506b09c3 Mon Sep 17 00:00:00 2001 From: potashin Date: Mon, 29 Apr 2024 16:16:37 +0300 Subject: [PATCH 5/7] chore: adjust specs --- spec/task-1_spec.rb | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/spec/task-1_spec.rb b/spec/task-1_spec.rb index 57545d2f..06963da2 100644 --- a/spec/task-1_spec.rb +++ b/spec/task-1_spec.rb @@ -2,20 +2,26 @@ require 'rspec-benchmark' require_relative '../task-1' -RSpec.configure do |config| - config.include RSpec::Benchmark::Matchers -end - RSpec.describe 'work' do + include RSpec::Benchmark::Matchers + it 'should be linear' do - `head -n 10000 data_large.txt > data.txt` + expect { |number, _| + `head -n #{number * 1000} data_large.txt > data.txt` - expect { work }.to perform_linear + work + }.to perform_linear.in_range(1, 100) end - it 'should perform under 15 seconds' do - `cp data_large.txt data.txt` + it 'should perform under 5 seconds' do + `head -n 1000000 data_large.txt > data.txt` - expect { work }.to perform_under(30).sec + expect { work }.to perform_under(5).sec end + + # it 'should perform under 30 seconds' do + # `cp data_large.txt data.txt` + + # expect { work }.to perform_under(30).sec + # end end From 7dbada5c3cd21d1070c491c098d5901947658cb3 Mon Sep 17 00:00:00 2001 From: potashin Date: Mon, 29 Apr 2024 16:35:50 +0300 Subject: [PATCH 6/7] fix: typos --- .gitignore | 2 +- case-study.md | 4 ++-- rbspy/record.sh | 2 +- work.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index bf77639e..9b4dad21 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ data*.txt !/tmp/stackprof/.keep # Ignore MacOS system files -.DS_Store \ No newline at end of file +.DS_Store diff --git a/case-study.md b/case-study.md index 86d9190f..20f1ff81 100644 --- a/case-study.md +++ b/case-study.md @@ -50,7 +50,7 @@ ### Находка №3 - `ruby-prof` в режиме `Graph` показывает, что точкой роста является `25.55% 25.55% 8.23 8.23 0.00 0.00 154066 Array#+` в `8.23 8.23 0.00 0.00 154066/154066 Array#each`. под это описания подходит 108 строка. -- вместо `users_objects = users_objects + [user_object]` используем `users_objects << [user_object]` +- вместо `users_objects = users_objects + [user_object]` используем `users_objects << user_object` - время выполнения программы для 500к входных данных сократилось с 12с до с 6c - исправленная проблема перестала быть главной точкой роста, ruby prof показал, что теперь это `66.16% 26.52% 13.47 5.40 0.00 8.07 500000 Array#all?` @@ -101,4 +101,4 @@ Удалось улучшить метрику системы с 4.7 дней до 24 секунд и уложиться в заданный бюджет. ## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавил два теста: прогон на полных данных до 15 секунд, проверка на линейную асимптотику +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавил два теста: прогон на 100к данных до 5 секунд, проверка на линейную асимптотику на основе данных от 1000 до 100000 записей. diff --git a/rbspy/record.sh b/rbspy/record.sh index ab05c895..5b36763c 100755 --- a/rbspy/record.sh +++ b/rbspy/record.sh @@ -1,2 +1,2 @@ #!/bin/sh -ruby work.rb | ps aux | grep work.rb | grep -v grep | awk '{print $2}' | xargs sudo rbspy record --pid \ No newline at end of file +ruby work.rb | ps aux | grep work.rb | grep -v grep | awk '{print $2}' | xargs sudo rbspy record --pid diff --git a/work.rb b/work.rb index c8a76000..af0b475c 100644 --- a/work.rb +++ b/work.rb @@ -1,3 +1,3 @@ require_relative 'task-1' -work(with_progressbar: true) +work(with_progressbar: false) From f6610b9bd36659381e04da03c4f3dd7753a7ac8d Mon Sep 17 00:00:00 2001 From: potashin Date: Tue, 30 Apr 2024 23:27:53 +0300 Subject: [PATCH 7/7] fix: case study --- case-study.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/case-study.md b/case-study.md index 20f1ff81..9ce71199 100644 --- a/case-study.md +++ b/case-study.md @@ -12,10 +12,9 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время выполнения программы. +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время выполнения программы для части данных (сначала 50к, потом 100к, потом 1кк). Сначала сделал гипотезу о том, что асимптотика времени работы программы квадратичная: отношение количества записей к времени выполнения в секундах: 100000/115 750000/61 50000/26, 25000/6). Подтвердил эту гипотезу с помощью теста rspec-benchmark. -В таком случае для полного объема понадобится 4.7 дней. ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. @@ -26,7 +25,7 @@ Вот как я построил `feedback_loop`: профилирование - изменение кода - тестирование – бенчмаркинг – откат при отсутствии разницы от оптимизации/сохранение результатов ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался rbspy +Для того, чтобы найти "точки роста" для оптимизации я воспользовался rbspy, stackprof, ruby-prof Вот какие проблемы удалось найти и решить @@ -98,7 +97,7 @@ ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с 4.7 дней до 24 секунд и уложиться в заданный бюджет. +Удалось улучшить метрику системы на 100к с 115с до 0.5с секунд и уложиться в заданный бюджет. Для полного файла время выполнение стало 24с. ## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавил два теста: прогон на 100к данных до 5 секунд, проверка на линейную асимптотику на основе данных от 1000 до 100000 записей. +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавил два теста: прогон на 1кк данных до 5 секунд, проверка на линейную асимптотику на основе данных от 1000 до 100000 записей.