-
Notifications
You must be signed in to change notification settings - Fork 183
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
optimization task (Lutovinova) #122
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,5 @@ | ||
.idea | ||
result.json | ||
data_large.txt | ||
ruby_prof_reports/* | ||
stackprof_reports/* |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
FROM ruby:3.1.0-alpine | ||
|
||
RUN apk update && apk upgrade && apk add --update --no-cache \ | ||
build-base tzdata bash htop | ||
|
||
WORKDIR /opt/app | ||
|
||
COPY Gemfile* ./ | ||
|
||
RUN gem install bundler | ||
RUN bundle install | ||
|
||
COPY . . |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# frozen_string_literal: true | ||
|
||
source "https://rubygems.org" | ||
|
||
ruby '3.1.0' | ||
|
||
# base | ||
gem 'date' | ||
gem 'json' | ||
gem 'minitest' | ||
gem 'pry' | ||
|
||
# for dev | ||
gem 'rspec' | ||
gem 'rspec-benchmark' | ||
gem 'ruby-prof' | ||
gem 'stackprof' | ||
gem 'ruby-progressbar' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
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) | ||
date (3.3.3) | ||
diff-lcs (1.5.0) | ||
json (2.6.3) | ||
method_source (1.0.0) | ||
minitest (5.18.0) | ||
parallel (1.22.1) | ||
pry (0.14.2) | ||
coderay (~> 1.1) | ||
method_source (~> 1.0) | ||
rspec (3.12.0) | ||
rspec-core (~> 3.12.0) | ||
rspec-expectations (~> 3.12.0) | ||
rspec-mocks (~> 3.12.0) | ||
rspec-benchmark (0.6.0) | ||
benchmark-malloc (~> 0.2) | ||
benchmark-perf (~> 0.6) | ||
benchmark-trend (~> 0.4) | ||
rspec (>= 3.0) | ||
rspec-core (3.12.1) | ||
rspec-support (~> 3.12.0) | ||
rspec-expectations (3.12.2) | ||
diff-lcs (>= 1.2.0, < 2.0) | ||
rspec-support (~> 3.12.0) | ||
rspec-mocks (3.12.3) | ||
diff-lcs (>= 1.2.0, < 2.0) | ||
rspec-support (~> 3.12.0) | ||
rspec-support (3.12.0) | ||
ruby-prof (1.6.1) | ||
ruby-progressbar (1.13.0) | ||
stackprof (0.2.23) | ||
|
||
PLATFORMS | ||
x86_64-linux | ||
|
||
DEPENDENCIES | ||
date | ||
json | ||
minitest | ||
parallel | ||
pry | ||
rspec | ||
rspec-benchmark | ||
ruby-prof | ||
ruby-progressbar | ||
stackprof | ||
|
||
RUBY VERSION | ||
ruby 3.1.0p0 | ||
|
||
BUNDLED WITH | ||
2.4.7 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -59,7 +59,7 @@ Case-study должен получиться рассказом с технич | |
|
||
### Совет: как посчитать кол-во строк в файле | ||
``` | ||
wc -l data_large.rb # (3250940) total line count | ||
wc -l data_large.txt # (3250940) total line count | ||
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. справедливо, никто кажется раньше не замечал |
||
``` | ||
|
||
### Совет: как создать меньший файл из большего, оставив перевые N строк | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
# Case-study оптимизации | ||
|
||
## Актуальная проблема | ||
|
||
В рамках домашнего задания дан скрипт, который выполняет обработку файла с данными по сессиям пользователей. | ||
Однако его работа критично медленная и справляется он только с небольшим входным файлом. | ||
Необходимо выполнить оптимизацию данной программы, чтобы требуемый файл был успешно обработан за адекватное время. | ||
|
||
## Формирование метрики | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумала использовать такую метрику: время выполнения программы на 10000 строках данных. | ||
Если мы хотим добиться чтобы на 3250940 строках в файле при линейной зависимости скорость обработки составляла не более 30 секунд, | ||
то на 10000 время обработки должно быть менее чем ~90 миллисекунд, добьемся того чтобы на 10000 скрипт выполнялся не более 90 миллисекунд. | ||
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. Вообще очень логично и здраво Но для профилирования хорошо, чтобы программа несколько секунд хотя бы успевала покрутиться 90мс маловато |
||
|
||
## Гарантия корректности работы оптимизированной программы | ||
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
Также были написаны тесты на выполнение метрики, чтобы добиться требуемого времени обработки и проверить линейную зависимость | ||
|
||
## Feedback-Loop | ||
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* | ||
|
||
Вот как я построил `feedback_loop`: | ||
- запуск профилировщика | ||
- выявление главной точки роста | ||
- внесение исправлений в код | ||
- запуск тестов для проверки корректности работы скрипта и выполнения метрики | ||
|
||
## Вникаем в детали системы, чтобы найти главные точки роста | ||
Перед первым запуском профилировщика запустила замеры времени на файлах с разным количеством строк (от 18 до 39366 с шагом x3). | ||
В результате первого прогона было понятно что зависимость нелинейная и нужно ее добиться. | ||
|
||
Для того, чтобы найти "точки роста" для оптимизации я воспользовался: | ||
- ruby-prof | ||
- stackprof | ||
|
||
Вот какие проблемы удалось найти и решить | ||
|
||
### Ваша находка №1 | ||
При первом запуске было выявлено текущее значение метрики - 1.34 sec. При этом тест с линейной зависимостью был провален. | ||
После запуска профилировщика была найдена первая точка роста: | ||
```bash | ||
%self total self wait child calls name location | ||
64.50 9.188 6.665 0.000 2.524 2011 Array#select | ||
``` | ||
т.к в коде он встречается только в одном месте - пытаемся исправить данную выборку тем что при парсинге строк сразу заполняем данные по сессиям | ||
сгруппировав их по пользовтелям. Это дало значительное уменьшение времени выполнения скрипта и привело к линейной зависимости от обхема данных. | ||
Но время пока еще не укладывается в метрику. | ||
```bash | ||
Failures: | ||
|
||
1) perfomance works faster then 10ms | ||
Failure/Error: expect { work('data_part_10000.txt') }.to perform_under(90).ms.sample(10).times | ||
expected block to perform under 90 ms, but performed above 149 ms (± 2.82 ms) | ||
``` | ||
|
||
### Ваша находка №2 | ||
После запуска профилировщика выявлена новая точка роста: | ||
```bash | ||
%self total self wait child calls name location | ||
28.23 0.734 0.310 0.000 0.424 13122 Array#all? | ||
``` | ||
т.к. места вызова данного метода 2 - решило проверить через stackprof в каком именно месте данная точка. | ||
По отчету было заметно что время потрачено на all? который вызван при подсчете уникальных браузеров. | ||
Также было видно что те же уникальные браузеры берутся при выводе в параметр allBrowsers, только отсортированные и в верхнем регистре. | ||
|
||
После правок время работы скрипта сократилось: | ||
```bash | ||
Failures: | ||
|
||
1) perfomance works faster then 10ms | ||
Failure/Error: expect { work('data_part_10000.txt') }.to perform_under(90).ms.sample(10).times | ||
expected block to perform under 90 ms, but performed above 112 ms (± 2.78 ms) | ||
# ./task-1-specs.rb:18:in `block (2 levels) in <top (required)>' | ||
``` | ||
|
||
### Ваша находка №3 | ||
После запуска профилировщика выявлена новая точка роста: | ||
```bash | ||
%self total self wait child calls name location | ||
22.41 0.308 0.074 0.000 0.234 9 Array#each | ||
``` | ||
Т.к. each втстречается несколько раз в коде, с помощью stackprof выясняем в каком именно месте происходит рост. | ||
Это метод each в collect_stats_from_users. | ||
Чтобы не пробегать каждый раз по всем пользователям было решено убрать этот метод и вынести заполнение всех данных в одном цикле | ||
Рефакторинг уменьшил время обработки | ||
```bash | ||
Failures: | ||
|
||
1) perfomance works faster then 10ms | ||
Failure/Error: expect { work('data_part_10000.txt') }.to perform_under(90).ms.sample(10).times | ||
expected block to perform under 90 ms, but performed above 95.8 ms (± 1.61 ms) | ||
# ./task-1-specs.rb:18:in `block (2 levels) in <top (required)>' | ||
``` | ||
|
||
### Ваша находка №4 | ||
Главной точкой роста остался метод each, но уже в другом месте. Исходя из отчета бОльшую часть времени внутри each выполняется фунцкия map, | ||
при этом у нее очень много вызовов. Правим эту часть | ||
Правки дали уменьшение по времени | ||
```bash | ||
Failures: | ||
|
||
1) perfomance works faster then 10ms | ||
Failure/Error: expect { work('data_part_10000.txt') }.to perform_under(90).ms.sample(10).times | ||
expected block to perform under 90 ms, but performed above 91.8 ms (± 2.46 ms) | ||
# ./task-1-specs.rb:18:in `block (2 levels) in <top (required)>' | ||
``` | ||
|
||
### Ваша находка №5 | ||
Точка роста осталась на методе each но на этот раз на методе each внтури сбора информации по пользователям. | ||
Вынесла этот кусок в отдельный метод, чтобы понять что именно увеличивает время. По отчету видно что дольше всего в цикле отрабатывает | ||
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. Супер-правильный ход с выносом анонимного блока в отдельный метод |
||
<Class::Date>#parse. Правим его | ||
После правок тесты прошли и стали подходить под метрику: | ||
```bash | ||
Finished in 1.63 seconds (files took 0.15055 seconds to load) | ||
2 examples, 0 failures | ||
``` | ||
|
||
При запуске бенчмарка оказалось что метрика неверна и предварительно подготовленные небольшие файлы отрабатывают больше чем за 30 секунд. | ||
Получила новые замеры на предварительно оптимизированном коде и решила уменьшить метркиу до 45 ms. Т.о снова пытаемся оптимизировать код но уже по новой метрике | ||
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. Лучше было бы взять файл побольше и ещё раз прикинуть за сколько он должен обработаться |
||
```bash | ||
Failures: | ||
|
||
1) perfomance works faster then 10ms | ||
Failure/Error: expect { work('data_part_10000.txt') }.to perform_under(45).ms.warmup(2).times.sample(10).times | ||
expected block to perform under 45 ms, but performed above 61.2 ms (± 2.76 ms) | ||
# ./task-1-specs.rb:18:in `block (2 levels) in <top (required)>' | ||
``` | ||
|
||
### Ваша находка №6 | ||
Точкой роста остается each который собирает данные по сессиям для каждого пользователя. Внутри него самое долгое это парсинг даты | ||
Т.к. дата уже в нужном для отчета формате, не парсим ее и получаем уменьшение времени: | ||
|
||
```bash | ||
Failures: | ||
|
||
1) perfomance works faster then 10ms | ||
Failure/Error: expect { work('data_part_10000.txt') }.to perform_under(45).ms.warmup(2).times.sample(10).times | ||
expected block to perform under 45 ms, but performed above 48.6 ms (± 687 μs) | ||
# ./task-1-specs.rb:18:in `block (2 levels) in <top (required)>' | ||
``` | ||
|
||
### Ваша находка №7 | ||
Точка роста все еще в методе each, но на первое место вышел метод split. После правок в его использовании получаем попадание в заданную метркиу. | ||
После запуска benchmark смотрим на время выполнения срипта. Получили время обработки исходного файла 36.89, что неплохо, но недостаточно для целевой метрики | ||
Сокращаю метрику до 35ms на 10000 строк и продолжаю оптимизировать | ||
```bash | ||
Failures: | ||
|
||
1) perfomance works faster then 10ms | ||
Failure/Error: expect { work('data_part_10000.txt') }.to perform_under(35).ms.warmup(2).times.sample(10).times | ||
expected block to perform under 35 ms, but performed above 37.1 ms (± 474 μs) | ||
# ./task-1-specs.rb:18:in `block (2 levels) in <top (required)>' | ||
``` | ||
|
||
### Ваша находка №8 | ||
Исходя из отчета stackproof теперь наиболее долгим процессом является parse_session | ||
Удалила из парсинга сохранение лишних данных, заменила ключи на символьные и удалила использование User.new | ||
В результате значение метрики достигнуто, запускаем benchmark на целевом файле и получаем время 30.7 | ||
|
||
## Результаты | ||
В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
Удалось улучшить метрику системы с 1.34 сек на 10000 строк до 35ms на 10000 строк и уложиться в заданный бюджет. | ||
|
||
За время оптимизации попробовала многие профилировщики. Визуально более простым и понятным показался ruby-prof. | ||
Cамым информативным в нетривиальных проблемах для меня оказался stackprof и ruby-prof с отчетам в виже callstack. | ||
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. 👍 |
||
|
||
## Защита от регрессии производительности | ||
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написаны тесты, которые находятся в файле tasks-1-specs | ||
|
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.
Nice