-
Notifications
You must be signed in to change notification settings - Fork 140
Homework 2 #124
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?
Homework 2 #124
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,5 @@ | ||
data | ||
data_large.txt | ||
.ruby-version | ||
reports | ||
Gemfile.lock |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# frozen_string_literal: true | ||
|
||
ruby '3.3.6' | ||
|
||
source 'https://rubygems.org' | ||
git_source(:github) { |repo| "https://github.com/#{repo}.git" } | ||
|
||
gem 'oj' | ||
gem 'memory_profiler' | ||
gem 'ruby-prof' | ||
gem 'stackprof' | ||
gem 'rspec-benchmark' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# Measure ruby code performance | ||
# Usage: | ||
# require 'bench_wrapper' | ||
# measure do | ||
# code to measure | ||
# end | ||
|
||
require "json" | ||
require "benchmark" | ||
|
||
def measure(&block) | ||
no_gc = (ARGV[0] == "--no-gc") | ||
|
||
if no_gc | ||
GC.disable | ||
else | ||
GC.start | ||
end | ||
|
||
memory_before = `ps -o rss= -p #{Process.pid}`.to_i/1024 | ||
puts "Memory BEFORE: #{memory_before}" | ||
gc_stat_before = GC.stat | ||
time = Benchmark.realtime do | ||
yield | ||
end | ||
# puts ObjectSpace.count_objects | ||
# unless no_gc | ||
# GC.start(full_mark: true, immediate_sweep: true, immediate_mark: false) | ||
# end | ||
# puts ObjectSpace.count_objects | ||
memory_after = `ps -o rss= -p #{Process.pid}`.to_i/1024 | ||
gc_stat_after = GC.stat | ||
|
||
puts({ | ||
RUBY_VERSION => { | ||
gc: no_gc ? 'disabled' : 'enabled', | ||
time: time.round(2), | ||
gc_count: gc_stat_after[:count] - gc_stat_before[:count], | ||
memory: "%d MB" % (memory_after - memory_before) | ||
} | ||
}.to_json) | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative 'memory_reporter' | ||
require_relative 'bench_wrapper' | ||
require_relative 'task-2' | ||
|
||
# path = "data/data#{ARGV[0] || 50000}.txt" | ||
path = 'data_large.txt' | ||
report_memory = ARGV[0] == '--report-memory' | ||
|
||
if report_memory | ||
reporter = MemoryReporter.new | ||
|
||
reporter.start | ||
work(path) | ||
else | ||
measure do | ||
work(path) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,44 +12,110 @@ | |
Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
||
## Формирование метрики | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: Программа не должна потреблять больше 70Мб памяти при обработке файла data_large.txt в течение всей своей работы. | ||
|
||
## Гарантия корректности работы оптимизированной программы | ||
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
||
## Feedback-Loop | ||
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* | ||
|
||
Вот как я построил `feedback_loop`: *как вы построили feedback_loop* | ||
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за время около 10мин, за исключением переписывания логики метода на работу в потоковом стиле. | ||
|
||
Вот как я построил `feedback_loop`: | ||
Создал бенчмарк и ruby файлы под каждый вид профилирования. Как аргумент передаю количество строк. | ||
Вот как я построил `feedback_loop`: | ||
- Запускаю бенчмарк. | ||
- Запускаю профилировщик. | ||
- Определяю главную точку роста. | ||
- Вношу изменения. | ||
- Проверяю гипотезу повторным запуском профилировщика. | ||
- Защищаю изменения тестом. | ||
|
||
Для первоначальной оценки запустил бенчмарк на 50_000 строк: | ||
{"3.3.6":{"gc":"enabled","time":14.96,"gc_count":357,"memory":"269 MB"}} | ||
|
||
Запустил в отдельном треде репорт потребления памяти в динамике: | ||
* [2025-02-12 22:53:50 +0400] Memory usage: 22.00 MB | ||
* [2025-02-12 22:53:52 +0400] Memory usage: 178.00 MB | ||
* [2025-02-12 22:53:54 +0400] Memory usage: 194.00 MB | ||
* [2025-02-12 22:53:56 +0400] Memory usage: 210.00 MB | ||
* [2025-02-12 22:53:57 +0400] Memory usage: 229.00 MB | ||
* [2025-02-12 22:53:59 +0400] Memory usage: 255.00 MB | ||
* [2025-02-12 22:54:01 +0400] Memory usage: 281.00 MB | ||
* [2025-02-12 22:54:03 +0400] Memory usage: 304.00 MB | ||
* [2025-02-12 22:54:05 +0400] Memory usage: 334.00 MB | ||
|
||
Решаю начать поиск точек роста с memory_profiler | ||
|
||
## Вникаем в детали системы, чтобы найти главные точки роста | ||
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* | ||
Для того, чтобы найти "точки роста" для оптимизации я воспользовался memory_profiler | ||
|
||
Вот какие проблемы удалось найти и решить | ||
|
||
### Ваша находка №1 | ||
- какой отчёт показал главную точку роста | ||
- как вы решили её оптимизировать | ||
- как изменилась метрика | ||
- как изменился отчёт профилировщика | ||
- memory_profiler показал главную точку роста в строке 7.16 GB rails-optimization-task2/task-2.rb:55 | ||
- Проблема конкатенации уже знакома по прошлому заданию, решаю ее заменой метода + на << | ||
То же самое проделываю и с предыдущей строкой. | ||
- Метрика уменьшилась на 54 Мб на 50_000 срок | ||
{"3.3.6":{"gc":"enabled","time":14.16,"gc_count":128,"memory":"215 MB"}} | ||
Кратно уменьшилось количество срабатываний gc | ||
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 | ||
- какой отчёт показал главную точку роста | ||
- как вы решили её оптимизировать | ||
- как изменилась метрика | ||
- как изменился отчёт профилировщика | ||
- memory_profiler показал главную точку роста в строке | ||
2.60 GB /Users/alex/pets/rails-optimization-task2/task-2.rb:104 | ||
также обратил внимание на большое количество алоцированные пробелов | ||
223085 " " | ||
|
||
169220 /rails-optimization-task2/task-2.rb:143 | ||
53865 /rails-optimization-task2/task-2.rb:40 | ||
- Вынес select из итератора, сгруппировав сессии по user_id | ||
Исправил конкатенацию пробела, она там не нужна("#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}"). | ||
|
||
Избавился от Date.parse, он также не нужен. | ||
|
||
Добавил # frozen_string_literal: true | ||
- Количество потребляемой памяти уменьшилось на 160MB на 50_000 строк | ||
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.3.6":{"gc":"enabled","time":0.27,"gc_count":30,"memory":"53 MB"}} | ||
- Найденные точки роста престали ими быть | ||
|
||
Смотрю метрики на 400_000 строк: | ||
|
||
{"3.3.6":{"gc":"enabled","time":2.48,"gc_count":51,"memory":"412 MB"}} | ||
|
||
### Ваша находка №3 | ||
- Использую stackprof для поиска очередной точки роста: | ||
|
||
(5876907 (57.5%) 5876907 (57.5%) String#split) | ||
|
||
### Ваша находка №X | ||
- какой отчёт показал главную точку роста | ||
- как вы решили её оптимизировать | ||
- как изменилась метрика | ||
- как изменился отчёт профилировщика | ||
Главная точка роста split методы в work и parse_session иетодах | ||
Зная что далее всеравно предстоит переписывать код в потоковом стиле, решаю не тратить время на | ||
оптимизацию split метода, потому как далее эта логика поменяется. | ||
- Переписал код в потоковом стиле. Использовал гем Oj | ||
|
||
Так как порядок не важен, сначала запишем usersStats (как объект, куда по мере обработки добавим пары "имя юзера" => stats), а затем добавим остальные ключи. | ||
|
||
- После внедрения новой логики получил метрику на data_large.txt файле в 1MB и выполнил бюджет | ||
|
||
{"3.3.6":{"gc":"enabled","time":3.78,"gc_count":3037,"memory":"1 MB"}} | ||
- 2738453(51.0%) 2738453(51.0%) String#split - остался главной точкой роста | ||
|
||
|
||
### Ваша находка №4 | ||
- Отчет ruby-prof qcachegring показал главную точку роста в String#split(36%) | ||
- Изучил возможности профилирования в ruby-prof qcachegring, но т.к. бюджет в 1MB | ||
меня вполне устравивает, решл завершить оптимизацию. | ||
- Защитил изменетестом, который проверяет потребление памяти на большом файле(менее 70 Mb). | ||
|
||
## Результаты | ||
В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. | ||
Удалось улучшить метрику системы с оценки в 1.5Gb до 1Mb для большого файла и уложиться в заданный бюджет. | ||
|
||
Наиболее полезным и удобным показалось профилирование потребления памяти с помощью memory_profiler и ruby-prof. | ||
|
||
Гигантскую долю памяти удалось сэкономить преписав программу в потоковом стиле с помощью гема Oj. | ||
|
||
*Какими ещё результами можете поделиться* | ||
Кажется для более точной оценки потребления памяти необходимо учитывать память которая была выделена | ||
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. нам не так важно обычно сколько именно было добавлено дополнительно; важно в результате просто сколько процесс потребляет (например чтобы в лимиты в k8s вписываться) |
||
до начала выполнения профилируемого метода(~20Mb) и вычитать это значение от потребления памяти которое зафмксировали по заверешению работы метода. | ||
|
||
## Защита от регрессии производительности | ||
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* | ||
Защитил изменетестом, который проверяет потребление памяти на большом файле(менее 70 Mb). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# frozen_string_literal: true | ||
|
||
class MemoryReporter | ||
DEFAULT_LIMIT_MB = 700 | ||
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. 70 же |
||
DEFAULT_INTERVAL = 1 | ||
DEFAULT_LOG_FILE = 'reports/memory_usage.log'.freeze | ||
|
||
class MemoryUsageError < StandardError; end | ||
|
||
def initialize(limit_mb: DEFAULT_LIMIT_MB, log_file: DEFAULT_LOG_FILE) | ||
@limit_mb = limit_mb | ||
@log_file = log_file | ||
end | ||
|
||
def start | ||
@thread = Thread.new do | ||
loop do | ||
mem_usage_mb = current_memory_usage | ||
log_memory_usage(mem_usage_mb) | ||
raise MemoryUsageError, "Memory usage exceeded limit of #{@limit_mb} MB" if mem_usage_mb > @limit_mb | ||
sleep DEFAULT_INTERVAL | ||
rescue => e | ||
puts e.message | ||
Process.kill("KILL", Process.pid) | ||
end | ||
end | ||
@thread.abort_on_exception = true | ||
end | ||
|
||
private | ||
|
||
def current_memory_usage | ||
(`ps -o rss= -p #{Process.pid}`.to_i / 1024) | ||
end | ||
|
||
def log_memory_usage(mem_usage_mb) | ||
File.open(@log_file, 'a') do |f| | ||
f.puts "[#{Time.now}] Memory usage: #{format('%.2f', mem_usage_mb)} MB" | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'fileutils' | ||
require 'memory_profiler' | ||
require 'ruby-prof' | ||
require 'stackprof' | ||
require_relative '../task-2' | ||
|
||
REPORTS_DIR = 'reports' | ||
FileUtils.mkdir_p(REPORTS_DIR) | ||
|
||
# path = "data/data#{ARGV[0] || 50000}.txt" | ||
path = 'data_large.txt' | ||
mode = ARGV[0] || 'memory_profiler' | ||
|
||
case mode | ||
when 'memory_profiler' | ||
report = MemoryProfiler.report do | ||
work(path) | ||
end | ||
report.pretty_print(scale_bytes: true) | ||
|
||
when 'stackprof' | ||
StackProf.run(mode: :object, out: "#{REPORTS_DIR}/stackprof.dump", raw: true) do | ||
work(path) | ||
end | ||
|
||
when 'ruby-prof' | ||
profile = RubyProf::Profile.new(measure_mode: RubyProf::MEMORY) | ||
|
||
result = profile.profile do | ||
work(path) | ||
end | ||
printer = RubyProf::CallTreePrinter.new(result) | ||
printer.print(path: REPORTS_DIR, profile: 'callgrind') | ||
|
||
else | ||
puts "Invalid mode: #{mode}. Use 'flat', 'graph', 'callstack', 'stackprof' or 'callgrind'." | ||
exit 1 | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"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"]}},"totalUsers":3,"totalSessions":15,"uniqueBrowsersCount":14,"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"} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rspec-benchmark' | ||
require_relative '../task-2' | ||
|
||
RSpec.describe 'Memory usage' do | ||
include RSpec::Benchmark::Matchers | ||
|
||
before do | ||
File.write('result.json', '') | ||
end | ||
|
||
it 'consumes no more than 70 MB of memory' do | ||
expect { work('data_large.txt', 'result.json') } | ||
.to perform_allocation(70_000_000).bytes | ||
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. perform_allocation это не то; нам надо MAX RSS оценить, а не кол-во аллокаций и не объём выделенной памяти |
||
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.
вот, сразу всё видно - 22МБ чисто для старта; потом заргузка файла; потом плавный рост со временем