-
Notifications
You must be signed in to change notification settings - Fork 24
optimized version and tests #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| /profiling/results/* | ||
| /samples/data_* | ||
| /test/data.txt | ||
| /test/result.json |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| # Case-study оптимизация. Часть 2. | ||
|
|
||
| ## Актуальная проблема | ||
| В нашем проекте возникла серьёзная проблема. | ||
|
|
||
| Необходимо было обработать файл с данными, чуть больше ста мегабайт. | ||
|
|
||
| У нас уже была программа на `ruby`, которая умела делать нужную обработку. | ||
|
|
||
| Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. | ||
|
|
||
| Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
|
||
| ## Формирование метрики | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: объем затрачиваемой памяти и время выполнения | ||
|
|
||
| ## Гарантия корректности работы оптимизированной программы | ||
| Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. | ||
|
|
||
| ## Асимптотика | ||
| До оптимизации: | ||
| ``` | ||
| Calculating ------------------------------------- | ||
| File: 512Kb 4.817 (± 5.3%) i/s - 24.000 in 5.032638s | ||
| File: 1Mb 2.527 (± 6.3%) i/s - 13.000 in 5.183298s | ||
| File: 2Mb 1.203 (±15.4%) i/s - 6.000 in 5.132240s | ||
| File: 4Mb 0.586 (±19.5%) i/s - 3.000 in 5.269748s | ||
| File: 8Mb 0.287 (±14.0%) i/s - 2.000 in 7.100129s | ||
| with 99.0% confidence | ||
|
|
||
| Comparison: | ||
| File: 512Kb: 4.8 i/s | ||
| File: 1Mb: 2.5 i/s - 1.91x (± 0.15) slower | ||
| File: 2Mb: 1.2 i/s - 4.00x (± 0.68) slower | ||
| File: 4Mb: 0.6 i/s - 8.18x (± 1.96) slower | ||
| File: 8Mb: 0.3 i/s - 16.77x (± 3.11) slower | ||
|
|
||
| ``` | ||
| Исходя из данных показателей мы видим, что увеличение объема обрабатываемых данных в 2 раза ведет к росту времени работы в 2 раза | ||
|
|
||
| ## Feedback-Loop | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений в среднем за 10 секунд | ||
|
|
||
| Вот как я построил `feedback_loop`: | ||
| 1. Изначально выбрал маленький размер исходных данных (около 1Мб) позволяющий скрипту успешно отработать без оптимизаций | ||
| 2. Поиск базовой метрики (время и память) | ||
| 3. Дописал тест на регрессию по времени и памяти | ||
| 4. Профилирование и поиск "точек роста" | ||
| 5. Внесение изменений в код | ||
| 6. Повторное тестирование и сбор новых метрик | ||
| 7. Увеличение объема данных | ||
|
|
||
| ## Вникаем в детали системы, чтобы найти 20% точек роста | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался | ||
|
|
||
| Гемы: | ||
| * ruby-prof | ||
| * memory_profiler | ||
| * get_process_mem | ||
| * benchmark-ips | ||
| Stdlib: | ||
| * benchmark | ||
| C | ||
| * valgrind (tool - massif) | ||
|
|
||
| Вот какие проблемы удалось найти и решить | ||
| Согласно результатам ruby-prof в режиме cpu: | ||
| ``` | ||
| %self total self wait child calls name | ||
| 23.99 9.674 2.918 0.000 6.756 1 IO#each_line | ||
| 6.89 1.007 0.838 0.000 0.169 48639 Array#map | ||
| 6.74 1.413 0.819 0.000 0.594 89009 <Class::Date>#iso8601 | ||
| 6.71 2.488 0.816 0.000 1.672 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json | ||
| 5.94 2.302 0.722 0.000 1.581 89009 Object#parse_session | ||
| 5.64 3.431 0.685 0.000 2.746 16214 Object#aggregate_user_stats | ||
| ``` | ||
| ### Находка №1 | ||
| Проанализировав исходные данные выяснилось, что даты изначально приходят в нужном формате, поэтому нет нужды создавать Date объект | ||
| Также были убраны неиспользуемые поля, а вместо полей first_name и last_name было сделано поле full_name. | ||
| ``` | ||
| Comparison: | ||
| File: 512Kb: 10.9 i/s | ||
| File: 1Mb: 5.9 i/s - 1.85x (± 0.14) slower | ||
| File: 2Mb: 2.8 i/s - 3.94x (± 0.57) slower | ||
| File: 4Mb: 1.4 i/s - 7.90x (± 1.57) slower | ||
| File: 8Mb: 0.6 i/s - 16.93x (± 5.53) slower | ||
| with 99.0% confidence | ||
| ``` | ||
| Фактически мы видим двукратный прирост производительности | ||
|
|
||
| ### Находка №2 | ||
| Также стандартная json библиотека не самая производительная заменив ее на gem oj, мы получили еще небольшой прирост: | ||
| ``` | ||
| Comparison: | ||
| File: 512Kb: 12.5 i/s | ||
| File: 1Mb: 7.0 i/s - 1.78x (± 0.12) slower | ||
| File: 2Mb: 3.4 i/s - 3.68x (± 0.42) slower | ||
| File: 4Mb: 1.6 i/s - 7.71x (± 1.27) slower | ||
| File: 8Mb: 0.8 i/s - 15.36x (± 3.33) slower | ||
| with 99.0% confidence | ||
| ``` | ||
|
|
||
| Однако нерешенной остается проблема резкого выделения [памяти](https://imgur.com/cvkmvMS) связанная с формирование json строки | ||
|
|
||
| ### Находка №3 | ||
| Вместо библиотеки, которая целиком парсит hash в json, можно использовать yajl, которая позволяет "стримить" json в файл | ||
| Мы немного проигрываем в производительности на маленьких файлах | ||
| ``` | ||
| Comparison: | ||
| File: 512Kb: 12.3 i/s | ||
| File: 1Mb: 6.8 i/s - 1.81x (± 0.13) slower | ||
| File: 2Mb: 3.3 i/s - 3.67x (± 0.33) slower | ||
| File: 4Mb: 1.6 i/s - 7.56x (± 1.22) slower | ||
| File: 8Mb: 0.8 i/s - 16.21x (± 5.33) slower | ||
| with 99.0% confidence | ||
| ``` | ||
| Однако мы существенно сокращаем объемы потребляемой  | ||
|
|
||
| ## Результаты | ||
| В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
| В среднем файл обрабатывается за 28 - 29 секунд при средних затратах по памяти 600 - 620Мб без резких скачков по памяти | ||
|
|
||
| ## Защита от регресса производительности | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавлены дополнительные тесты для защиты от регрессий по памяти и времени выполнения |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,35 +12,74 @@ | |
| Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
|
||
| ## Формирование метрики | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: объем затрачиваемой памяти и время выполнения | ||
|
|
||
| ## Гарантия корректности работы оптимизированной программы | ||
| Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. | ||
|
|
||
| ## Feedback-Loop | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений в среднем за 10 секунд | ||
|
|
||
| Вот как я построил `feedback_loop`: *как вы построили feedback_loop* | ||
| Вот как я построил `feedback_loop`: | ||
| 1. Изначально выбрал маленький размер исходных данных (около 1Мб) позволяющий скрипту успешно отработать без оптимизаций | ||
| 2. Поиск базовой метрики (время и память) | ||
| 3. Дописал тест на регрессию по времени и памяти | ||
| 4. Профилирование и поиск "точек роста" | ||
| 5. Внесение изменений в код | ||
| 6. Повторное тестирование и сбор новых метрик | ||
| 7. Увеличение объема данных | ||
|
|
||
| ## Вникаем в детали системы, чтобы найти 20% точек роста | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался | ||
|
|
||
| Гемы: | ||
| * ruby-prof | ||
| * memory_profiler | ||
| * get_process_mem | ||
|
|
||
| Stdlib: | ||
| * benchmark | ||
|
|
||
| Вот какие проблемы удалось найти и решить | ||
|
|
||
| ### Ваша находка №1 | ||
| О вашей находке №1 | ||
| ### Находка №1 | ||
| Считывание всего файла в строку с дальнейшим разбиением на массив строк крайне неэффективно и было одним из основным блокеров для работы с большими файлами | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Опечатка
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Как именно это обнаружили? |
||
|
|
||
| Использование построчной обработки существенно ускорило выполнение программы примерно в 2,3 раза | ||
|
|
||
| ### Находка №2 | ||
| Memory_profiler показал, что аллоцируется огромное количество строк, хоть большинство из них собираются GC, однако все равно увеличивают объем потребляемой памяти и время выполнения (как минимум за счет работы GC) | ||
|
|
||
| Для исправления этой проблемы был использован ```# frozen_string_literal: true``` и в качестве ключей стали использоваться Symbol | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Насколько сильно помогло? |
||
|
|
||
| ### Находка №3 | ||
| Избыточное и неоптимальное использование итераторов | ||
|
|
||
| По возможности, обработка данных переписана так, чтобы использовать минимальное число итераций и использоание "in-place" модификаций чтобы снизить расходы по памяти | ||
|
|
||
| ### Ваша находка №2 | ||
| О вашей находке №2 | ||
| ### Находка №4 | ||
| Генерация "сущностей без надобности" | ||
|
|
||
| ### Ваша находка №X | ||
| О вашей находке №X | ||
| Был убран класс User, а так же вместо отдельных массивов users и sessions был использован хеш ключем, которого стал user_id, что упростило связывание пользователей и сессий | ||
|
|
||
| ### Находка №5 | ||
| Неоптимальная аггрегация данных | ||
|
|
||
| Вместо того, чтобы агрегировать статистику по завершении обработки файла, код был переписан так, чтобы статистика аггрегировалась "по ходу" обработки файла, также для снижения потребляемой памяти, посчитанные данные удалялись | ||
|
|
||
| ### Находка №6 | ||
| Использование "медленных" методов без особой надобности | ||
|
|
||
| Ruby-prof показал, что метод Date#parse занимает порядка 8% от всего времени выполнения, поэтому он был заменен на Date#strptime | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
|
||
| ### Находка №7 | ||
| Использование регулярных выражений без особой надобности | ||
|
|
||
| Поиск по регулярным выражениям был заменен на поиск по вхождению подстроки, поскольку он более производительный | ||
|
|
||
| ## Результаты | ||
| В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
| Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* | ||
|
|
||
| *Какими ещё результами можете поделиться* | ||
| В среднем файл обрабатывается за 29 - 30 секунд при средних затратах по памяти 850 - 860Мб | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
|
||
| ## Защита от регресса производительности | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали* | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавлены дополнительные тесты для защиты от регрессий по памяти и времени выполнения | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'yajl' | ||
| #require 'ruby-progressbar' | ||
|
|
||
| def parse_user(fields) | ||
| { | ||
| id: fields[1], | ||
| full_name: fields[2] << " " << fields[3] | ||
| } | ||
| end | ||
|
|
||
| def parse_session(fields) | ||
| { | ||
| user_id: fields[1], | ||
| browser: fields[3].upcase!, | ||
| time: fields[4].to_i, | ||
| date: fields[5].chomp!, | ||
| } | ||
| end | ||
|
|
||
| def aggregate_user_stats(data) | ||
| return {} unless data | ||
|
|
||
| user = data[1][:user] | ||
| sessions = data[1][:sessions] | ||
| time = sessions.map {|s| s[:time] } | ||
| browsers = sessions.map { |s| s[:browser] } | ||
| { | ||
| user[:full_name]=> { | ||
| sessionsCount: sessions.count, | ||
| totalTime: time.sum.to_s << ' min.', | ||
| longestSession: time.max.to_s << ' min.', | ||
| browsers: browsers.sort!.join(', '), | ||
| usedIE: browsers.any? { |b| b.include?('INTERNET EXPLORER') }, | ||
| alwaysUsedChrome: browsers.all? { |b| b.include?('CHROME')}, | ||
| dates: sessions.map{|s| s[:date]}.sort!.reverse! | ||
| } | ||
| } | ||
| end | ||
|
|
||
| def work(input: "data.txt", output: "result.json") | ||
| # Отчёт в json | ||
| # - Сколько всего юзеров + | ||
| # - Сколько всего уникальных браузеров + | ||
| # - Сколько всего сессий + | ||
| # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + | ||
| # | ||
| # - По каждому пользователю | ||
| # - сколько всего сессий + | ||
| # - сколько всего времени + | ||
| # - самая длинная сессия + | ||
| # - браузеры через запятую + | ||
| # - Хоть раз использовал IE? + | ||
| # - Всегда использовал только Хром? + | ||
| # - даты сессий в порядке убывания через запятую + | ||
|
|
||
| data = {} | ||
| _tmp_hash = {} | ||
| report = { | ||
| totalUsers: 0, | ||
| uniqueBrowsersCount: 0, | ||
| totalSessions: 0, | ||
| allBrowsers: [], | ||
| usersStats: {} | ||
| } | ||
|
|
||
| #bar_output = ENV["NOPROGRESS"] ? File.open(File::NULL, "w") : $stdout | ||
| #bar = ProgressBar.create(total: nil, output: bar_output) | ||
| File.open(input) do |f| | ||
| f.each_line do |line| | ||
| cols = line.split(',') | ||
| key = cols[1] | ||
| if cols[0] == "user" | ||
| if data[key].nil? | ||
| report[:usersStats].merge!(aggregate_user_stats(data.shift)) | ||
| end | ||
| data[key] ||= {} | ||
| data[key][:user] = parse_user(cols) | ||
| report[:totalUsers] += 1 | ||
| else | ||
| data[key] ||= {} | ||
| data[key][:sessions] ||= [] | ||
| session = parse_session(cols) | ||
| browser = session[:browser] | ||
| if _tmp_hash[browser].nil? | ||
| _tmp_hash[browser] = 1 | ||
| report[:allBrowsers].push(browser) | ||
| report[:uniqueBrowsersCount] += 1 | ||
| end | ||
| report[:totalSessions] += 1 | ||
| data[key][:sessions].push(session) | ||
| end | ||
| # bar.increment | ||
| end | ||
| report[:usersStats].merge!(aggregate_user_stats(data.shift)) | ||
| report[:allBrowsers] = report[:allBrowsers].sort!.join(',') | ||
| # bar.finish | ||
| end | ||
| File.open(output, 'w') do |f| | ||
| Yajl::Encoder.encode(report, f) | ||
| f << "\n" | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| ENV["NOPROGRESS"] = '1' | ||
| require_relative '../lib/task-1.rb' | ||
| require 'benchmark/ips' | ||
|
|
||
| FILES = { | ||
| "512Kb" => 524_268, | ||
| "1Mb" => 1_048_576, | ||
| "2Mb" => 2_097_152, | ||
| "4Mb" => 4_194_304, | ||
| "8Mb" => 8_388_608, | ||
| }.freeze | ||
|
|
||
| class GCSuite | ||
| def warming(*) | ||
| run_gc | ||
| end | ||
|
|
||
| def running(*) | ||
| run_gc | ||
| end | ||
|
|
||
| def warmup_stats(*) | ||
| end | ||
|
|
||
| def add_report(*) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def run_gc | ||
| GC.enable | ||
| GC.start | ||
| GC.disable | ||
| end | ||
| end | ||
|
|
||
|
|
||
| suite = GCSuite.new | ||
|
|
||
| Benchmark.ips do |x| | ||
| x.config(suite: suite, stats: :bootstrap, confidence: 99) | ||
| FILES.keys.each do |size| | ||
| x.report("File: #{size}") { work(input: "../samples/data_#{size}", output: "/dev/null") } | ||
| end | ||
| x.compare! | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍