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

Cpu optimization #147

Open
wants to merge 9 commits 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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
data*
!data3.txt
result.json
/ruby_prof_reports/
/stackprof_reports/
56 changes: 0 additions & 56 deletions case-study-template.md

This file was deleted.

102 changes: 102 additions & 0 deletions case-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Case-study оптимизации

## Актуальная проблема
Задача - обработать файл с данными, чуть больше ста мегабайт.

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

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

## Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я решила использовать такую метрику:
Итерации в секунду работы программы на входном файле в 13000 строк (`data3.txt`). Последние две итерации - использовала итерации в секунду для основного входного файла (`data_large.txt`). Бюджет на эту последнюю метрику - 30 секунд.

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

## Feedback-Loop
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроила эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за пару минут.
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`:
- Профилировала программу на тестовом файле, используя один из [профилировщиков](#profiler-names)
- Выделяла одну "точку роста"
- Пробовала оптимизировать ее
- Проверяла, как изменилась метрика
- Смотрела, изменился ли отчет профилировщика
- Если да, сохраняла изменения
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍


## Вникаем в детали системы, чтобы найти главные точки роста
Для того, чтобы найти "точки роста" для оптимизации я использовала <a id="profiler-names"></a>профилировщики `rbspy`, `ruby-prof` и `stackprof`.

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

### sessions.select для поиска сессий конкретного пользователя
- Видно на отчете `ruby-prof` (использовала принтер `Flat`)
- Для оптимизации я добавила хэш `uid_to_sessions`, в который сразу добавляла сессии для конкретных пользователей при парсинге файла
- Метрика выросла с 0.750 i/s до **5.980 i/s**
Copy link
Collaborator

Choose a reason for hiding this comment

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

я бы тут не советовал использовать ips

ips это больше для бенчмарков, когда что-то выполняется реально много раз в секунду

а у нас тут < 1 ips зачастую

- Исправленная проблема больше не была на первых строках отчета профилировщика

### Подсчет количества уникальных браузеров
- Следующую точку роста так же показал отчет `ruby-prof` (использовала принтер `Callstack`)
- Для подсчета уникальных браузеров мы создавали массив, проходили его для каждой записи сессии и сравнивали, что ни один из элементов не равен браузеру из сессии. Для оптимизации я добавила `Set` для хранения уникальных браузеров, избавившись от массива
- Метрика выросла до **7.790 i/s**
- Проблема перестала появляться в первых строках отчета профилировщика

### Обновление массивов users и sessions с помощью метода +
- Определила эту точку роста при помощи профилировщика `ruby-prof`, используя принтер `graph`
- Так как старые массивы нам нет надобности сохранять, заменила выражения
```ruby
sessions = sessions + ...
users = users + ...
```
на эквивалентные
```ruby
sessions.concat(...)
users.concat(...)
```
- Метрика выросла до **11.930 i/s**
- Проблема перестала появляться в отчете профилировщика `ruby-prof`

### Обновление массива users_objects с помощью метода +
- Данная точка роста была обнаружена при помощи профилировщика `rbspy` (Появлялась только при прогоне программы на достаточно большом входном файле, файл `data_large.txt` до сих пор обрабатывался очень долго)
Copy link
Collaborator

Choose a reason for hiding this comment

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

лайк за rbspy; его киллер-фича что можно подключиться к работающему процессу и посмотреть где он там застрял

или можно например профилировать процесс пумы и посмотреть на что уходит основное время работы сервера в целом

- Поменяла, как и в предыдущем пункте, метод `+` на метод `concat` для обновления массива `users_objects`
- На маленьком файле (`data3.txt`) метрика выросла лишь до **12.400 i/s**, но основной файл (`data_large.txt`) впервые начал обрабатываться менее чем за 45 секунд
- `rbspy` перестал выводить данный метод

### Использование Date.parse
- Проблема найдена при помощи профилировщика `ruby-prof`
- Заменила цепочку методов
```ruby
.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 }
```
на
```ruby
.sort
.reverse
.map do |date|
date_ary = date.split('-')
Date.new(date_ary[0].to_i, date_ary[1].to_i, date_ary[2].to_i).iso8601
end
```
- Метрика выросла до **15.200 i/s**, а метрика для файла `data_large.txt` составила **0.033 i/s**
- Метод `Date.parse` стал занимать меньше процентов в отчете профилировщика
Copy link
Collaborator

Choose a reason for hiding this comment

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

с датой можно вообще ничего не делать, это пасхалочка


### Array.each внутри `collect_stats_from_users`
- Показывали профилировщики `ruby-prof` и `stackprof`
- Оптимизировала вычисление статов - слила вызов шести разных методов в один `evaluate_stats`
- Метрика для файла `data_large.txt` увеличилась до **0.037 i/s** (таким образом стала укладываться в бюджет)
- Метод Array.each перестал быть главной точкой роста в отчете профилировщика `ruby-prof`

### Array#split и Array#[] внутри метода parse_to_users_and_sessions
- Показывал профилировщик `ruby-prof`
- Заменила вызов этих двух методов на вызов `.start_with?`
- Метрика выросла до **0.042 i/s**
- Методы стали занимать меньше процентов в отчете профилировщика

## Результаты
В результате проделанной оптимизации наконец удалось обработать файл с данными.
Удалось улучшить метрику системы - количество итераций работы программы на файле `data3.txt` - c *0.750 i/s* до *20.820 i/s*, а количетсво итераций работы программы на файле `data_large.txt` до **0.042 i/s** и уложиться в заданный бюджет.

## Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы я добавила perfomance-тест `spec/task-1_spec.rb`
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍


Loading