Skip to content
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

task_1 #131

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions benchmark-test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

require 'benchmark/ips'
require_relative 'task-1'

Benchmark.ips do |x|
x.config(stats: :bootstrap, confidance: 95)

x.report('10_000') { work('data_10_000.txt') }
x.report('20_000') { work('data_20_000.txt') }
x.report('40_000') { work('data_40_000.txt') }
x.compare!
end
82 changes: 82 additions & 0 deletions case-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Case-study оптимизации

## Актуальная проблема
В нашем проекте возникла серьёзная проблема.

Необходимо было обработать файл с данными, чуть больше ста мегабайт.

У нас уже была программа на `ruby`, которая умела делать нужную обработку.

Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время.

Я решил исправить эту проблему, оптимизировав эту программу.

## Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время обработки файла на 10 000 строк в секундах
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нам такая метрика не совсем подходит на всём протяжении этой работы.

Мы можем зафиксировать такую метрику на какой-то момент, потом взять файл побольше, потом ещё побольше. В таком случае нам та или иная метрика позволяет оценить эффект оптимизации, которую мы делаем на данном шаге

В идеале нас интересует время выполнения на полном объёме, но так как слишком долго ждать, мы ищем другие (временные) метрики.


## Гарантия корректности работы оптимизированной программы
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации.

## Feedback-Loop
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за несколько минут.

Вот как я построил `feedback_loop`:
- профилирование, поиск точки роста
- оптимизация bottleneck'а
- тестрование
- прогон benchmark
- если уложились в бюджет - коммитим изменения, иначе повторяем алгоритм

## Вникаем в детали системы, чтобы найти главные точки роста
Для того, чтобы найти "точки роста" для оптимизации я воспользовался профилировщиком ruby-prof

Вот какие проблемы удалось найти и решить

### Находка №1
- ruby-prof в формате callstack показал, что первая точка роста - Array#select:
`user_sessions = sessions.select { |session| session['user_id'] == user['id'] }`
- группировка сессии по пользователям
- время обработки сократилось с 7,73 s до 1,03 s
- да. %self: 87% -> 0%

### Находка №2
- ruby-prof в формате flat показал, что следующая точка роста Array#all?
- получение списка уникальных браузеров через map и uniq:
`unique_browsers = sessions.map { |session| session['browser'] }.uniq`
- время выполнения сократилось с 1,03 s до 0,37 s
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Стало слишком маленькое время. Тут было бы хорошо увеличить размер файла, чтобы программа успевала покрутиться секунд 5 в основном цикле

- да. %self: 26,1% -> 0,35%

### Находка №3
- ruby-prof в формате callstack -> `collect_stats_from_users`
- уменьшение числа вызовов метода, внутри метода сокращено количество вызовов map, по подсказке рубокопа конкатенация строк заменена на интерполяцию
- с 0,36 s до 0,29 s
- да

### Находка №4
- ruby-prof в формате flat -> Array#+
- заменила на `<<`
`users_objects = users.map { |user| User.new(attributes: user, sessions: sessions_by_users[user['id']] || []) }`
- c 0,29 s до 0,23 s
- да. %self: 19,24% -> 0%

### Находка №5
- ruby-prof в формате flat -> Array#each
- замена на map
- 0,2391 s -> 0,2214 s
- да

### Находка №6
- ruby-prof в формате flat -> String#split
- убран из методов `parse_user` и `parse_session`, сразу передается массив
- 0,2214 s -> 0,2109 s
- да. %self: 18,6% -> 9,86%

## Результаты
В результате проделанной оптимизации наконец удалось обработать файл с данными.
Удалось улучшить метрику системы и уложиться в заданный бюджет. После проделанной работы обработка файла занимает ~40 s.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Пойдёт, мб во втором задании удастся в 30 секунд уложиться


Обработка файла в 10 000 строк занимала ~7,72 с, после оптимизации время сократилось в 37 раз и стало ~0,21 с

## Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы были написаны performance-тесты, которые контролируют укладывается ли выполнение в заданный бюджет. Рассмотрены кейсы для файлов: 10 000, 20 000 и 40 000 строк, с бюджетом 100, 200 и 400 мс соответственно.

28 changes: 28 additions & 0 deletions performance_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require 'rspec'
require 'rspec-benchmark'
require_relative 'task-1.rb'

RSpec.configure do |config|
config.include RSpec::Benchmark::Matchers
end

describe 'data 10_000' do
it 'perform less 100 ms' do
file_lines = File.read('data_large.txt').split("\n").first(10_000)
expect { collect_stats(file_lines) }.to perform_under(100).ms.warmup(2).times.sample(10).times
end
end

describe 'data 20_000' do
it 'perform less 200 ms' do
file_lines = File.read('data_large.txt').split("\n").first(20_000)
expect { collect_stats(file_lines) }.to perform_under(200).ms.warmup(2).times.sample(10).times
end
end

describe 'data 40_000' do
it 'perform less 400 ms' do
file_lines = File.read('data_large.txt').split("\n").first(40_000)
expect { collect_stats(file_lines) }.to perform_under(400).ms.warmup(2).times.sample(10).times
end
end
24 changes: 24 additions & 0 deletions ruby_prof.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require 'ruby-prof'
require_relative 'task-1'

RubyProf.measure_mode = RubyProf::WALL_TIME

result = RubyProf.profile do
file_lines = File.read('data_large.txt').split("\n").first(10_000)
collect_stats(file_lines)
end

# _#{Time.new.to_i}
printer1 = RubyProf::FlatPrinter.new(result)
printer1.print(File.open("ruby_prof_reports/flat_#{Time.new.to_i}.txt", 'w+'))

printer2 = RubyProf::GraphHtmlPrinter.new(result)
printer2.print(File.open("ruby_prof_reports/graph_#{Time.new.to_i}.html", 'w+'))

printer3 = RubyProf::CallStackPrinter.new(result)
printer3.print(File.open("ruby_prof_reports/callstack_#{Time.new.to_i}.html", 'w+'))

printer4 = RubyProf::CallTreePrinter.new(result)
printer4.print(:path => "ruby_prof_reports", :profile => 'callgrind')
Loading