-
Notifications
You must be signed in to change notification settings - Fork 139
Memory optimization Shlyapnikov #116
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,2 @@ | ||
| /fixtures/* | ||
| /reports/* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| ruby '3.3.4' | ||
|
|
||
| source 'https://rubygems.org' | ||
| git_source(:github) { |repo| "https://github.com/#{repo}.git" } | ||
|
|
||
|
|
||
| gem 'pry' | ||
| gem 'memory_profiler' | ||
| gem 'rspec-benchmark' | ||
| gem 'ruby-prof' | ||
| gem 'stackprof' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| GEM | ||
| remote: https://rubygems.org/ | ||
| specs: | ||
| benchmark-malloc (0.2.0) | ||
| benchmark-perf (0.6.0) | ||
| benchmark-trend (0.4.0) | ||
| coderay (1.1.3) | ||
| diff-lcs (1.5.1) | ||
| memory_profiler (1.1.0) | ||
| method_source (1.1.0) | ||
| pry (0.15.2) | ||
| coderay (~> 1.1) | ||
| method_source (~> 1.0) | ||
| rspec (3.13.0) | ||
| rspec-core (~> 3.13.0) | ||
| rspec-expectations (~> 3.13.0) | ||
| rspec-mocks (~> 3.13.0) | ||
| rspec-benchmark (0.6.0) | ||
| benchmark-malloc (~> 0.2) | ||
| benchmark-perf (~> 0.6) | ||
| benchmark-trend (~> 0.4) | ||
| rspec (>= 3.0) | ||
| rspec-core (3.13.2) | ||
| rspec-support (~> 3.13.0) | ||
| rspec-expectations (3.13.3) | ||
| diff-lcs (>= 1.2.0, < 2.0) | ||
| rspec-support (~> 3.13.0) | ||
| rspec-mocks (3.13.2) | ||
| diff-lcs (>= 1.2.0, < 2.0) | ||
| rspec-support (~> 3.13.0) | ||
| rspec-support (3.13.2) | ||
| ruby-prof (1.7.1) | ||
| stackprof (0.2.27) | ||
|
|
||
| PLATFORMS | ||
| arm64-darwin-23 | ||
| ruby | ||
|
|
||
| DEPENDENCIES | ||
| memory_profiler | ||
| pry | ||
| rspec-benchmark | ||
| ruby-prof | ||
| stackprof | ||
|
|
||
| RUBY VERSION | ||
| ruby 3.3.4p94 | ||
|
|
||
| BUNDLED WITH | ||
| 2.5.23 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| unzip: | ||
| gzip -dk fixtures/data_large.txt.gz | ||
|
|
||
| prepare_data: | ||
| make unzip | ||
| head -n 200000 fixtures/data_large.txt > fixtures/data_smal.txt | ||
| head -n 50000 fixtures/data_large.txt > fixtures/data_smal_50000.txt | ||
| head -n 10000 fixtures/data_large.txt > fixtures/data_smal_10000.txt | ||
|
|
||
| test: | ||
| ruby tests/reporter_test.rb | ||
|
|
||
| perform_test: | ||
| rspec tests/reporter_spec.rb | ||
|
|
||
| memeory_prof: | ||
| ./bin/report_builder.rb memeory_prof | ||
|
|
||
| memeory_prof_to_file: | ||
| ./bin/report_builder.rb memeory_prof > reports/memeory_prof_report.txt | ||
|
|
||
| stack_prof: | ||
| ./bin/report_builder.rb stack_prof | ||
|
|
||
| open_stack_prof: | ||
| stackprof reports/stackprof.dump | ||
|
|
||
| open_stack_prof_by_method: | ||
| stackprof reports/stackprof.dump --method $(T) | ||
|
|
||
| open_stack_prof_by_img: | ||
| stackprof --graphviz reports/stackprof.dump > reports/graphviz.dot | ||
| dot -Tpng reports/graphviz.dot > reports/graphviz.png | ||
|
|
||
| ruby_prof: | ||
| ./bin/report_builder.rb ruby_prof | ||
|
|
||
| open_ruby_prof: | ||
| qcachegrind reports/$(T) | ||
|
|
||
| all_reports: | ||
| make memeory_prof | ||
| make stack_prof | ||
| make ruby_prof | ||
|
|
||
| run: | ||
| ./bin/runner | ||
|
|
||
| benchmark: | ||
| ./bin/benchmark.rb | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| #!/usr/bin/env ruby | ||
|
|
||
| # frozen_string_literal: true | ||
|
|
||
| require 'benchmark' | ||
| require_relative '../lib/profiler' | ||
|
|
||
|
|
||
| time = Benchmark.realtime do | ||
| work('fixtures/data_large.txt', 'result.json', { watcher_enable: true }) | ||
| end | ||
|
|
||
|
|
||
|
|
||
| def printer(time, rows = 1000) | ||
| pp "Processing time from file: #{time.round(4)}" | ||
| end | ||
|
|
||
| printer(time) | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| #!/usr/bin/env ruby | ||
|
|
||
| # frozen_string_literal: true | ||
|
|
||
| require_relative '../lib/profiler' | ||
|
|
||
| begin | ||
| prof_type = ARGV[0] | ||
|
|
||
| available_types = %w[ | ||
| memeory_prof | ||
| stack_prof | ||
| ruby_prof | ||
| ] | ||
|
|
||
| raise StandardError, "unknow profiler type: #{prof_type}" unless available_types.include?(prof_type) | ||
|
|
||
| Profiler.make_report(prof_type) | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| #!/usr/bin/env ruby | ||
|
|
||
| require_relative "../lib/reporter" | ||
|
|
||
| work('fixtures/data_large.txt', 'result.json', { watcher_enable: true }) |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| # Case-study оптимизации | ||
|
|
||
| ## Актуальная проблема | ||
| В нашем проекте возникла серьёзная проблема. | ||
|
|
||
| Необходимо было обработать файл с данными, чуть больше ста мегабайт. | ||
|
|
||
| У нас уже была программа на `ruby`, которая умела делать нужную обработку. | ||
|
|
||
| Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. | ||
|
|
||
| Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
|
||
| ## Порядок роста используемой помяти в начеле оптимизации | ||
|
|
||
| 2025-02-01 15:17:52 +0300: 25 MB | ||
| 2025-02-01 15:17:53 +0300: 139 MB | ||
| 2025-02-01 15:17:54 +0300: 147 MB | ||
| 2025-02-01 15:17:56 +0300: 155 MB | ||
| 2025-02-01 15:17:57 +0300: 166 MB | ||
| 2025-02-01 15:17:59 +0300: 177 MB | ||
| 2025-02-01 15:18:00 +0300: 186 MB | ||
| 2025-02-01 15:18:02 +0300: 196 MB | ||
| 2025-02-01 15:18:03 +0300: 205 MB | ||
| 2025-02-01 15:18:05 +0300: 227 MB | ||
| 2025-02-01 15:18:06 +0300: 237 MB | ||
| 2025-02-01 15:18:08 +0300: 245 MB | ||
| 2025-02-01 15:18:09 +0300: 256 MB | ||
| 2025-02-01 15:18:10 +0300: 274 MB | ||
| 2025-02-01 15:18:12 +0300: 290 MB | ||
|
|
||
| ## Формирование метрики | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: программа затрачивает на обработку больших файлов не больше `70MB` | ||
| Перед оптимизацией программа затрачивает на обработку файла в 50000 строк 335 MB | ||
|
|
||
| ## Гарантия корректности работы оптимизированной программы | ||
| Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
|
||
| ## Feedback-Loop | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* | ||
|
|
||
| Вот как я построил `feedback_loop`: | ||
|
|
||
| 1. Сформировал короткие удобные алиаасы, что бы быстро получать обратную связь | ||
| 2. Быстро запустил профилировщики(`make all_reports`) -> посмотрел отчеты, нашел точку роста -> внес изменения и снова запусти профилировщик | ||
| 3. Создал отдельный вотчер, который запускается в отделном треде и проверяет количество используемой памяти(пишет лог), если оно привышает пороговое значение то выбрасывает исключение | ||
|
Collaborator
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. 👍 |
||
|
|
||
| ## Вникаем в детали системы, чтобы найти главные точки роста | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* | ||
|
|
||
| Вот какие проблемы удалось найти и решить | ||
|
|
||
| ### Ваша находка №1 | ||
| 1. отчет `memory prof` показал что основная точка роста которая потребляет много памяти это соединение двух массивов при обходе строк(на файле в 10000 строк потребление памяти 287 MB, общее потребление 440.21 MB) | ||
| 2. Заменил `users + [parse_user(line)]` на `users << parse_user(line)` | ||
| 3. Метрика изминилась - на файле 10000 общее потребление снизилось до 143.47 MB | ||
| 4. Точка роста перестала быть проблеммойчя | ||
|
|
||
| ### Ваша находка №2 | ||
| 1. Следующая точка роста `sessions.select { |session| session['user_id'] == user['id'] }` - потребляет памяти 104.07 MB на 10000 строк | ||
| 2. Поскольку метод селект каждый раз создает новый массив, а старый оставляет без изминений, написал собственный метод фильтрации | ||
|
|
||
| ```ruby | ||
| class Array | ||
| def custom_filter! | ||
| i = 0 | ||
| acc = [] | ||
| while i < self.size | ||
| if yield(self[i]) | ||
| acc << self[i] | ||
| self.delete(self[i]) | ||
| next | ||
| end | ||
| i += 1 | ||
| end | ||
| acc | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| 3. Алокация снизилась до 29.99 MB на 10000 строках | ||
| 4. Данная проблемма перестала быть точкой роста | ||
|
|
||
| - какой отчёт показал главную точку роста | ||
| - как вы решили её оптимизировать | ||
| - как изменилась метрика | ||
| - как изменился отчёт профилировщика | ||
|
|
||
| ### Ваша находка №3 | ||
| 1. Отчет `stack_prof` показывает что теперь алоцированно много объектов и памяти при парсинге даты и разбитие строки метод `split` | ||
| 2. Убрал парсинг даты так как она уже в нужном формате, `split` при обходе строк заменил на `line =~ /user/` | ||
| 3. алокация на 10000 строк уменьшилась до 10 MB | ||
| 4. Данная проблемма перестала быть точкой роста | ||
|
|
||
| ### Ваша находка №4 | ||
| 1. отчет `memory prof` показал что теперь много памяти алоцируется при чтении файла и при создании промежуточных объектов пользователя | ||
| 2. Сделал стриминговое чтение данных(читаю файл по строчно), и не создаю промежуточных объектов пользователя | ||
| 3. Потребление памяти снизилось до 65.42 MB на 100_000 строк | ||
| 4. Данная проблемма перестала быть точкой роста | ||
| - какой отчёт показал главную точку роста | ||
| - как вы решили её оптимизировать | ||
| - как изменилась метрика | ||
| - как изменился отчёт профилировщика | ||
|
|
||
| ### Ваша находка №5 | ||
|
Collaborator
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. отлично, что сделали несколько итераций прежде чем переписывать! максимум пользы |
||
| 1. отчет `memory pro` и вотчер показывает что осноную память программа расходует когда накапливает данные и десеарилизует их затем в JSON(на большых данных 1340 MB) | ||
|
Collaborator
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. Потребление памяти снизилось до константног - на полный отчет тратится всего 27 MB, так же увеличилась скорость 12.9!!!!! c за полный отчет | ||
|
Collaborator
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. йееее 🎉 |
||
| 4. Данная проблемма перестала быть точкой роста и мы уложились в бюджет | ||
|
|
||
|
|
||
| ## Результаты | ||
| В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
| Удалось улучшить метрику системы с 1400 MB до 27 MB и уложиться в заданный бюджет. | ||
|
|
||
| 1. Увиличилась скорость выполнения по сравнению с первым заданием до 13 с не смотря на большое количество IO операций | ||
|
|
||
| ## Защита от регрессии производительности | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написал перформанс тест который проверяет что программа потребляет не более 70 MB памяти | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class ProcessWatcher | ||
| LOG_FILE_PATH = 'fixtures/log.txt'.freeze | ||
|
|
||
| attr_reader :pid, :limit | ||
|
|
||
| class LongMemoryUsageError < StandardError; end | ||
|
|
||
| def initialize(pid:, limit:) | ||
| @pid = pid | ||
| @limit = limit | ||
| end | ||
|
|
||
| def watch | ||
| File.write(LOG_FILE_PATH, '') | ||
| f = File.open(LOG_FILE_PATH, 'w') | ||
| thread = Thread.new(pid, f) do |process_pid, file| | ||
| process = true | ||
|
|
||
| while process | ||
| memory = "%d" % (`ps -o rss= -p #{process_pid}`.to_i / 1024) | ||
| f.write("#{Time.now}: #{memory} MB \n") | ||
| raise LongMemoryUsageError, "usage memory to long #{memory}" if memory.to_i > limit | ||
| # sleep(1) | ||
|
Collaborator
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. м? sleep же нужен?
Author
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. Со sleep иногда бывало так что вотчер не успевал перехватить последний скачек памяти когда записывался json в файл и программа посто завершалась как будто все хорошо, вот я его и решил закоментить |
||
| end | ||
| rescue => e | ||
| file.close | ||
| raise e | ||
| end | ||
| thread.abort_on_exception = true | ||
| [thread, f] | ||
| end | ||
| 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.
👍 супер, сразу всё понятно