-
Notifications
You must be signed in to change notification settings - Fork 195
Homework 1 #162
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 1 #162
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 @@ | ||
data | ||
data_large.txt | ||
result.json | ||
ruby_prof_reports |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
3.3.6 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# frozen_string_literal: true | ||
|
||
ruby '3.3.6' | ||
|
||
source 'https://rubygems.org' | ||
git_source(:github) { |repo| "https://github.com/#{repo}.git" } | ||
|
||
|
||
gem 'pry' | ||
gem 'rspec-benchmark' | ||
gem 'ruby-progressbar' | ||
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) | ||
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.3) | ||
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) | ||
ruby-progressbar (1.13.0) | ||
stackprof (0.2.27) | ||
|
||
PLATFORMS | ||
arm64-darwin-24 | ||
ruby | ||
|
||
DEPENDENCIES | ||
pry | ||
rspec-benchmark | ||
ruby-prof | ||
ruby-progressbar | ||
stackprof | ||
|
||
RUBY VERSION | ||
ruby 3.3.6p108 | ||
|
||
BUNDLED WITH | ||
2.5.22 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# 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 | ||
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 | ||
gc_stat_after = GC.stat | ||
memory_after = `ps -o rss= -p #{Process.pid}`.to_i/1024 | ||
|
||
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,11 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative 'bench_wrapper' | ||
require_relative 'task-1' | ||
|
||
# path = "data/data#{ARGV[0] || 50000}.txt" | ||
path = "data_large.txt" | ||
|
||
measure do | ||
work(path) | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,45 +12,105 @@ | |
Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
||
## Формирование метрики | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: | ||
- Определил глобальну метрику - время выполнения программы на data_large файле. Бюджет - 3с. | ||
- Т.к. работать с такими большими данными неэффективно, решил создать несколько файлов с 10_000, 25_000, 50_000, 100_000 и 200_000 строк. | ||
- Оценил асимптотику: | ||
* 10000 - "time":0.6,"gc_count":29,"memory":"78 MB"/ | ||
* 25000 - "time":3.53,"gc_count":103,"memory":"128 MB" | ||
* 50000 - "time":13.97,"gc_count":352,"memory":"147 MB" | ||
* 100000 - "time":55.02,"gc_count":1290,"memory":"261 MB" | ||
* 200000 - "time":229.75,"gc_count":4968,"memory":"180 MB" | ||
> 55,02/13,97 ~ 4; 229,75/55,02 ~ 4 - асимптотика времени работы программы составляет квадратичную то есть время работы растёт пропорционально квадрату размера обрабатываемого файла O(n^2). Дал приблизительную оценку времени выполнения файла в 3 млн строк - 15ч. | ||
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. Лайк за оценку |
||
- Для эффективной работы выбрал начать с файла в 50_000 строк. Оставил метрику на время выполнения файла, но бюджет решил обновлять после каждой итерации | ||
по оптимизации программы. Для первой итерации установил бюджет в 2 секунды. | ||
|
||
## Гарантия корректности работы оптимизированной программы | ||
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
||
## Feedback-Loop | ||
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* | ||
|
||
Вот как я построил `feedback_loop`: *как вы построили feedback_loop* | ||
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за около 15 минут, так как аытался использовать различгые отчеты | ||
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. время фидбек-лупа я скорее имею в виду это то сколько вам прихоидтся "тупо ждать" результата то есть если вы сделали какое-то изменение и на проверку того, что стало лучше/хуже через минут 10 это очень плохо, потому что уже теряется фокус, начинается группировка изменений в одну итерацию и тд |
||
изучая их возможности. | ||
|
||
Создал бенчмарк и ruby файлы под каждый вид профилирования. Как аргумент передаю количество строк. | ||
Вот как я построил `feedback_loop`: | ||
- Запускаю бенчмарк. | ||
- Запускаю профилировщик. | ||
- Определяю главную точку роста. | ||
- Вношу изменения. | ||
- Проверяю гипотезу повторным запуском профилировщика. | ||
- Защищаю изменения тестом. | ||
|
||
## Вникаем в детали системы, чтобы найти главные точки роста | ||
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* | ||
Для того, чтобы найти "точки роста" для оптимизации я воспользовался ruby-prof(flat), | ||
|
||
Вот какие проблемы удалось найти и решить | ||
|
||
### Ваша находка №1 | ||
- какой отчёт показал главную точку роста | ||
- как вы решили её оптимизировать | ||
- как изменилась метрика | ||
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? | ||
- Отчет ruby-prof flat показал 70.78% времени выполнения на Array#select | ||
- Проблема была в формировании юзер сессий отдеьлным селектом по каждому юзеру. Чтобы избежать этого решил сгруппировать | ||
сессии по user_id до итерирования по юзерам. | ||
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. главное тут исправилась асимпототика с O(N^2) на O(N) по идее тут можно было бы заново оценить асимптотику и тогда можно было бы по идее даже задаться какой-то аппроксимацией а-ля если 3М - 30 сек, то надо уложиться 300К в 1сек и тогда должно быть норм |
||
- После внесения изменений выполнил поставленную метрику в 2с, получил 0.86с на 50_000 строк. | ||
- Исправленная проблема перестала быть главной точкой роста | ||
|
||
### Ваша находка №2 | ||
- какой отчёт показал главную точку роста | ||
- как вы решили её оптимизировать | ||
- как изменилась метрика | ||
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? | ||
Для следующего поиска выбрал объем данных в 100_000 строк, перед оптимизацией бенчмарк оценил длительность - 2.43с | ||
Установил промежуточный бюджет в 1с. | ||
|
||
### Ваша находка №X | ||
- какой отчёт показал главную точку роста | ||
- как вы решили её оптимизировать | ||
- как изменилась метрика | ||
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? | ||
### Ваша находка №2 | ||
- Отчет callstack показал что 54.67% занимает метод Array#+ На каждую итерацию создается новый массив, а это тяжелая операция. | ||
- Проблему решил добавлением элементов в массив через << (users << parse_user(line), sessions << parse_session(line)) | ||
- Бенчмарк показал что метрику в 0.87c | ||
- Исправленная проблема перестала быть главной точкой роста | ||
- Защитил изменения обновлением перформенс теста. | ||
|
||
Оценил бенчмарком время выполнения на 200_000 строк(1.96с). Принял решение создать файл для тестов на | ||
400_000 строк("time":5.11,"gc_count":542,"memory":"561 MB") для более точной оценки точек роста. | ||
Оценил ассимпототику(400_000 - 5.11, 200_000 - 2.1, 100_000 - 0.9) - уже больше похоже на линейную зависимость. | ||
Оптимистически предполагаю, что достижения бюджета в 3с для 400_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 | ||
- Для поиска главной точки роста решил использовать stackprof. Большую часть времени забирал на себя Array#each, но | ||
я предположил что надо смотреть на то что конкретно в этом блоке тратится больше времени. Заметил что 55.66% времени | ||
в этом блоке занимает Array#all?. Подтверждаю свою находку перезапуском ruby-prof callstack. | ||
- Вместо того чтобы в каждой итерации проверять браузер на уникальность(uniqueBrowsers.all?), решил использовать Set, | ||
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. Set 👍 |
||
который по определению хранит только уникальные значения. | ||
- Метрика показала 4.36c, что не удовлетворяет промежуточному бюджету в 3с. | ||
- Исправленная проблема перестала быть главной точкой роста | ||
- Защитил изменения обновлением перформенс теста. | ||
|
||
### Ваша находка №4 | ||
- Для поиска главной точки роста решил использовать stackprof speedscope. Оценил удобство исользования и количество доступной информации. | ||
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. 👍 stackprof + speedscope = one love |
||
Определил что главная точка роста на данный момент Object#collect_stats_from_users(41%). | ||
- В первую очередь вынес в отдельнвую перемменную user_stats = report['usersStats']. | ||
Далее убрал ненужную тут конкатенацию "#{user.attributes['first_name']} #{user.attributes['last_name']}" | ||
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. много изменений в одну итерацию == не понятно что сработало |
||
Вместо использования merge, который создает дополнительный хэш, обновляю данные напрямую block.call(user).each { |key, value| stats[key] = value } | ||
Также заметил что в первой итерации не исправил в одном месте Array#+, заменил на << | ||
- Метрика показала 2.09c, что удовлетворяет промежуточному бюджету в 3с. | ||
Удивился, так как предполагаю что с учетом линейной асимптотики этого уже должно хватить для достижния главного бюджета в 30 секунд | ||
на большом файле. Проверяю и подтверждаю достижение бюджета: | ||
{"3.3.6":{"gc":"enabled","time":24.02,"gc_count":92,"memory":"1856 MB"}} | ||
- Однако исправленная проблема не перестала быть главной точкой роста | ||
- Защитил изменения обновлением перформенс теста. | ||
{"3.3.6":{"gc":"enabled","time":24.02,"gc_count":92,"memory":"1856 MB"}} | ||
|
||
Не смотря на достижение бюджета, решаю провести еще одну итерацию по оптимизации выполнения метода Object#collect_stats_from_users: | ||
|
||
- С помощью callstack отчета отмечаю большую долю выполнения на <Class::Date>#parse | ||
- Отмечаю что Date.parse тут необязателен, использую { 'dates' => user.sessions.map { |s| s['date'] }.sort.reverse } | ||
- Бенчмарк показывает 19.3с на большом файле. Бюджет выполнен, решаю остановить оптимизацию. | ||
- Избавился от точки роста в <Class::Date>#parse | ||
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. да-да, это пасхалочка одна из моих любимых |
||
|
||
## Результаты | ||
В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. | ||
Удалось улучшить метрику системы с оценки времени выполнения в 15 часов до 19.3с и уложиться в заданный бюджет. | ||
|
||
*Какими ещё результами можете поделиться* | ||
Для себя отметил качество и удобство использования callstack а также stackrprof speedscope отчетов. | ||
Отметил что при оптимизации лучше опираться на точки роста в отчетах, так как один раз при очевидной попытки оптимизации | ||
"на глаз" метода Array#map получил регресс в метрике. | ||
Также оптимальным показалось использование не одного конкретного профилировщика, а использование их в комплексе, для лучшей оценки точек роста. | ||
|
||
## Защита от регрессии производительности | ||
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* | ||
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написал перформанс тест который запускает | ||
программу 2 раза и проверяет время выполнения на прокинутом в него файле за определнное количество времени. Тест обновлял | ||
по мере оптимизации, увеличивая количество строк и задавая соответствующее время выполнения. | ||
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,54 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'fileutils' | ||
require 'ruby-prof' | ||
require 'stackprof' | ||
require_relative '../task-1' | ||
|
||
REPORTS_DIR = 'ruby_prof_reports' | ||
FileUtils.mkdir_p(REPORTS_DIR) | ||
|
||
path = "data/data#{ARGV[0] || 50000}.txt" | ||
mode = ARGV[1] || 'flat' | ||
|
||
profile = RubyProf::Profile.new(measure_mode: RubyProf::WALL_TIME) | ||
|
||
result = profile.profile do | ||
work(path, no_gc: true) | ||
end | ||
|
||
case mode | ||
when 'callgrind' | ||
printer = RubyProf::CallTreePrinter.new(result) | ||
printer.print(path: REPORTS_DIR, profile: "callgrind") | ||
puts "Callgrind report generated at #{REPORTS_DIR}/callgrind.out" | ||
|
||
when 'graph' | ||
printer = RubyProf::GraphHtmlPrinter.new(result) | ||
File.open("#{REPORTS_DIR}/graph.html", 'w+') { |file| printer.print(file) } | ||
puts "Graph HTML report generated at #{REPORTS_DIR}/graph.html" | ||
|
||
when 'flat' | ||
printer = RubyProf::FlatPrinter.new(result) | ||
File.open("#{REPORTS_DIR}/flat.txt", 'w+') { |file| printer.print(file) } | ||
puts "Flat profile report generated at #{REPORTS_DIR}/flat.txt" | ||
|
||
when 'callstack' | ||
printer = RubyProf::CallStackPrinter.new(result) | ||
File.open("#{REPORTS_DIR}/callstack.html", 'w+') { |file| printer.print(file) } | ||
puts "CallStack report generated at #{REPORTS_DIR}/callstack.html" | ||
when 'stackprof' | ||
StackProf.run(mode: :wall, out: "#{REPORTS_DIR}/stackprof.dump", interval: 1000) do | ||
work(path, no_gc: true) | ||
end | ||
|
||
when 'stackprof_speedscope' | ||
profile = StackProf.run(mode: :wall, raw: true) do | ||
work(path, no_gc: true) | ||
end | ||
File.write("#{REPORTS_DIR}/stackprof_speedscope.json", JSON.generate(profile)) | ||
|
||
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,14 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative 'rspec_helper' | ||
|
||
describe 'Performance reporter' do | ||
let(:file_path) { 'data_large.txt' } | ||
let(:time) { 28 } | ||
|
||
it 'create report' do | ||
expect { | ||
work(file_path) | ||
}.to perform_under(time).sec.warmup(2).times.sample(2).times | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
require 'rspec-benchmark' | ||
require_relative "../task-1" | ||
|
||
|
||
RSpec.configure do |config| | ||
config.include RSpec::Benchmark::Matchers | ||
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.
да-да, по сути метрику, которую вам бы хотелось, вы не можете реально вычислить
поэтому приходится вводить промежуточные метрики (время для обработки файла x, y, z); они нам нужны на каждом шаге оптимизации чтобы понять насколько изменение было полезным