diff --git a/case-study.md b/case-study.md new file mode 100644 index 0000000..daad772 --- /dev/null +++ b/case-study.md @@ -0,0 +1,105 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +Для начала я запустил небольшой бенчмарк, чтобы понять, какая асимптотика у программы. Для каждого измерения взял первые `n` строк из файла с полными данными. Результаты бенчмарка выглядели так: + +``` + user system total real +line_count = 10 0.001417 0.000258 0.001675 ( 0.011769) +line_count = 50 0.001417 0.000258 0.001675 ( 0.011346) +line_count = 100 0.002608 0.000000 0.002608 ( 0.013075) +line_count = 500 0.002582 0.009544 0.012126 ( 0.022563) +line_count = 1000 0.009544 0.010132 0.019676 ( 0.030331) +line_count = 5000 0.218717 0.020935 0.239652 ( 0.253703) +line_count = 10000 0.979160 0.000000 0.979160 ( 1.010272) +line_count = 20000 4.887525 0.017847 4.905372 ( 4.930110) +line_count = 30000 13.817134 0.099889 13.917023 ( 13.935027) +line_count = 40000 29.488743 0.109932 29.598675 ( 29.619598) +line_count = 50000 50.501303 0.109971 50.611274 ( 50.648952) +``` +График зависимости времени работы программы от объёма входных данных выглядел так: + +![График зависимости времени работы программы](execution%20time%20graph.png "График зависимости времени работы программы") + +Судя по графику, программа работала с асимптотикой `O(n^2)`. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время работы программы на полном объёме данных. + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: +1. Запуск профилировщик `rbspy` на малом объёме данных. +2. Анализ отчёта профилировщика и выявление главной точки роста. +3. Оптимизация главной точки роста. +4. Запуск теста идущего в комплекте с программой. +5. Запуск бенчмарка на малых объёмах данных и проверка попадания в бюджет. +6. Закрепление изменений в системе контроля версий. +7. Обновление бюджета в бенчмарке. + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* + +Вот какие проблемы удалось найти и решить + +### Итерация №1 +- Использовался отчёт профилировщика `rbspy` при запуске на файле с 10000 строк. Отчёт показал, что больше всего времени программа тратит на выборку сессий по пользователю из массива сессий. +- Было принято решение вынести эту строку из цикла и вместо этого выполнить `Array#group_by`. +- Время выполнения программы на файле с 10000 строками уменьшилось с 1.010272 до 0.283054 секунды. +- Также уменьшилось время выполнения программы на файлах большего размера. + ``` + user system total real + line_count = 11 0.001461 0.000487 0.001948 ( 0.012805) + line_count = 51 0.002695 0.000898 0.003593 ( 0.009555) + line_count = 100 0.003591 0.001197 0.004788 ( 0.010995) + line_count = 501 0.006650 0.000000 0.008918 ( 0.019601) + line_count = 1000 0.012564 0.000000 0.015094 ( 0.029157) + line_count = 5000 0.050438 0.019783 0.073774 ( 0.103414) + line_count = 10000 0.160801 0.000000 0.170127 ( 0.283054) + line_count = 20001 0.400933 0.048675 0.455922 ( 0.547208) + line_count = 30000 0.637398 0.019211 0.666364 ( 0.770074) + line_count = 40000 0.978085 0.010174 1.003803 ( 1.200759) + line_count = 50000 1.290851 0.019795 1.325995 ( 1.518988) + ``` +- График зависимости времени работы программы от объёма входных данных стал выглядеть так (синие точки): + + ![График зависимости времени работы программы](execution%20time%20graph%20-%20after%201st%20iteration.png "График зависимости времени работы программы") + ![График зависимости времени работы программы](execution%20time%20graph%20-%20after%201st%20iteration%20-%20zoom.png "График зависимости времени работы программы") + Судя по изменившейся форме графика можно сделать вывод, что асимптотика стала линейной в данном диапазоне обрабатываемых строкб возможно на более крупном масштабе мы увидим ещё одну степенную асимптоту. +- Отчёт профилировщика изменился значительно: исправленная проблема перестала быть главной точкой роста, время подключения библиотек стало основным. + +### Ваша находка №2 +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? + +### Ваша находка №X +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. + +*Какими ещё результами можете поделиться* + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* + diff --git a/execution time graph - after 1st iteration - zoom.png b/execution time graph - after 1st iteration - zoom.png new file mode 100644 index 0000000..c7b1f65 Binary files /dev/null and b/execution time graph - after 1st iteration - zoom.png differ diff --git a/execution time graph - after 1st iteration.png b/execution time graph - after 1st iteration.png new file mode 100644 index 0000000..c639f42 Binary files /dev/null and b/execution time graph - after 1st iteration.png differ diff --git a/execution time graph.png b/execution time graph.png new file mode 100644 index 0000000..cc9d416 Binary files /dev/null and b/execution time graph.png differ diff --git a/task-1.rb b/task-1.rb index 778672d..869795a 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 @@ -43,8 +42,8 @@ def collect_stats_from_users(report, users_objects, &block) end end -def work - file_lines = File.read('data.txt').split("\n") +def work(filename = 'data.txt') + file_lines = File.read(filename).split("\n") users = [] sessions = [] @@ -96,10 +95,10 @@ def work # Статистика по пользователям users_objects = [] + user_sessions = sessions.group_by { |session| session['user_id'] } 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) + user_object = User.new(attributes: attributes, sessions: user_sessions[user['id']]) users_objects = users_objects + [user_object] end @@ -107,7 +106,7 @@ def work # Собираем количество сессий по пользователям collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } + { 'sessionsCount' => user.sessions&.count.to_i } end # Собираем количество времени по пользователям @@ -143,34 +142,24 @@ def work 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 +# Метод для запуска работы с разным количеством строк файла `data_large.txt` +# @param [Integer] n количество строк +# @return [void] +def work_with_line_count(n) + filename = "data_large#{n}.txt" + `head -n #{n} data_large.txt > data_large#{n}.txt` unless File.exist?(filename) + work(filename) +ensure + File.delete(filename) if File.exist?(filename) +end - def test_result +if ENV.key? 'LINE_COUNT' + line_count = ENV['LINE_COUNT'].to_i + work_with_line_count(line_count) +else + if ENV.key? 'FILE_NAME' + work ENV['FILE_NAME'] + else 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 diff --git a/task-1_benchmark.rb b/task-1_benchmark.rb new file mode 100644 index 0000000..533d438 --- /dev/null +++ b/task-1_benchmark.rb @@ -0,0 +1,10 @@ +require 'benchmark' +require './task-1' + +include Benchmark + +Benchmark.bm do |x| + [11, 51, 100, 501, 1_000, 5_000, 10_000, 20_001, 30_000, 40_000, 50_000].each do |line_count| + x.report("line_count = #{line_count}") { work_with_line_count(line_count) } + end +end diff --git a/task-1_spec.rb b/task-1_spec.rb new file mode 100644 index 0000000..6f5a56e --- /dev/null +++ b/task-1_spec.rb @@ -0,0 +1,17 @@ +require 'rspec-benchmark' +require './task-1' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end + +describe 'Performance' do + describe '#work' do + let(:line_count) { 20_001 } + let(:expected_time) { 4 } + + it 'works under or equal expected time' do + expect { work_with_line_count(line_count) }.to perform_under(expected_time).sec.warmup(2).times.sample(4).times + end + end +end diff --git a/task-1_test.rb b/task-1_test.rb new file mode 100644 index 0000000..67e3b96 --- /dev/null +++ b/task-1_test.rb @@ -0,0 +1,34 @@ +require 'minitest/autorun' +require './task-1' + +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