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

optimization task (Lutovinova) #122

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
5 changes: 5 additions & 0 deletions .gitignore
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/*
13 changes: 13 additions & 0 deletions Dockerfile.ruby
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM ruby:3.1.0-alpine
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice


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 . .
18 changes: 18 additions & 0 deletions Gemfile
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'
58 changes: 58 additions & 0 deletions Gemfile.lock
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Case-study должен получиться рассказом с технич

### Совет: как посчитать кол-во строк в файле
```
wc -l data_large.rb # (3250940) total line count
wc -l data_large.txt # (3250940) total line count
Copy link
Collaborator

Choose a reason for hiding this comment

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

справедливо, никто кажется раньше не замечал

```

### Совет: как создать меньший файл из большего, оставив перевые N строк
Expand Down
168 changes: 168 additions & 0 deletions case-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Case-study оптимизации

## Актуальная проблема

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

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

Choose a reason for hiding this comment

The 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 внтури сбора информации по пользователям.
Вынесла этот кусок в отдельный метод, чтобы понять что именно увеличивает время. По отчету видно что дольше всего в цикле отрабатывает
Copy link
Collaborator

Choose a reason for hiding this comment

The 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. Т.о снова пытаемся оптимизировать код но уже по новой метрике
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍


## Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написаны тесты, которые находятся в файле tasks-1-specs

Loading