Skip to content

Week_1 cpu (Shlyapnikov A.) #151

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

Open
wants to merge 4 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
reports/*
result.json
fixtures/*

11 changes: 11 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ruby '3.3.4'

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'
50 changes: 50 additions & 0 deletions Gemfile.lock
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.2)
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-23
ruby

DEPENDENCIES
pry
rspec-benchmark
ruby-prof
ruby-progressbar
stackprof

RUBY VERSION
ruby 3.3.4p94

BUNDLED WITH
2.5.23
48 changes: 48 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
run:
ruby bin/runner

unzip:
gzip -dk fixtures/data_large.txt.gz

prepare_data:
head -n 1000 fixtures/data_large.txt > fixtures/data1000.txt
Copy link
Collaborator

Choose a reason for hiding this comment

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

лайк за makefile!

head -n 2000 fixtures/data_large.txt > fixtures/data2000.txt
Copy link
Collaborator

Choose a reason for hiding this comment

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

fixtures лучше бы в gitignore добавить, чтобы не было 300к строк в PRе

Copy link
Author

Choose a reason for hiding this comment

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

Fixed

head -n 4000 fixtures/data_large.txt > fixtures/data4000.txt
head -n 8000 fixtures/data_large.txt > fixtures/data8000.txt
head -n 100000 fixtures/data_large.txt > fixtures/data100000.txt
head -n 200000 fixtures/data_large.txt > fixtures/data200000.txt

test:
ENVIRONMENT=test ruby tests/task-1_test.rb

perform_test:
ENVIRONMENT=test rspec tests/task_perform_spec.rb

all_tests:
make test
make perform_test

simple_benchmark:
ENVIRONMENT=test ruby lib/benchmarks/simple_benchmarks.rb

simple_benchmark_gb_dis:
ruby lib/benchmarks/simple_benchmarks.rb true

benchmarks_ips:
ruby lib/benchmarks/benchmarks_ips.rb

benchmarks_ips_gb_dis:
ruby lib/benchmarks/benchmarks_ips.rb true

report:
ruby lib/reporters/prof_reporter.rb $(T)

all_reports:
make report T='flat'
make report T='graph'
make report T='callstack'
make report T='callgrind'
make report T='stack-prof-cli'
make report T='stack-prof-json'

.PHONY: test
5 changes: 5 additions & 0 deletions bin/runner
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env ruby

require_relative "../lib/task-1"

work('fixtures/data1000.txt')
97 changes: 83 additions & 14 deletions case-study-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,114 @@
Я решил исправить эту проблему, оптимизировав эту программу.

## Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика*
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику:
- обрабатывать 200_000 строк не больше чем за 1,5 сек, если я уложусь в это время, то обработка всего файла уложится в бюджет
Copy link
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Author

Choose a reason for hiding this comment

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

Наверное как то так?
Целевая метрика - мы хотим что бы полной отчет формировался(3250940) не более чем за 30 сек?


Асимптотика перед рефакторингом:
``` bash
"Processing time from file 1000 rows: 0.0177"
"Processing time from file 2000 rows: 0.0504"
"Processing time from file 4000 rows: 0.1516"
"Processing time from file 8000 rows: 0.504"
```

Асимптотика после первой итерации рефаторинга(формирование и группировка сессий по user_id):

```bash
"Processing time from file 1000 rows: 0.0078"
"Processing time from file 2000 rows: 0.0185"
"Processing time from file 4000 rows: 0.0358"
"Processing time from file 8000 rows: 0.0685"
```

Асимптотика после второй итерации(создание пользователей сразу при обходе строк)

```bash
"Processing time from file 1000 rows: 0.0084"
"Processing time from file 2000 rows: 0.0176"
"Processing time from file 4000 rows: 0.0372"
"Processing time from file 8000 rows: 0.0707"
```

Асимптотика после третьей итерации(оптимизация `collect_stat_from user`)

```bash
"Processing time from file 1000 rows: 0.0102"
"Processing time from file 2000 rows: 0.0234"
"Processing time from file 4000 rows: 0.0592"
"Processing time from file 8000 rows: 0.0849"
"Processing time from file 100000 rows: 1.0904"
"Processing time from file 1000000 rows: 41.2784" // полный отчет
```

Асимптотика финальная - видно что она почти линейная
Copy link
Collaborator

Choose a reason for hiding this comment

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

ну она в принципе после первой итерации почти сразу и превращается в линейную

```bash
"Processing time from file 1000 rows: 0.007"
"Processing time from file 2000 rows: 0.012"
"Processing time from file 4000 rows: 0.0232"
"Processing time from file 8000 rows: 0.0452"
"Processing time from file 100000 rows: 0.5962"
"Processing time from file 1000000 rows: 29.8777"
```

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

## Feedback-Loop
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось*
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 5 минут(внес изменеия -> запустил бенчмарк и профилировщик-> посмотрел результат)
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`:
1. Создал Makefile с короткими алиасами для быстрого запуска тестов отчетов и бенчмарков
2. Запуск бенчмарков и профилировщика
3. Поиск точки роста
4. Внисение изменений
5. Проверка гипотизы
6. На каждой тиерации увеличивать данные на вход

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

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

### Ваша находка №1
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
1. Отчет graph html показал что главная точка роста является вызов `Array#select` программа проводит там 24% времени
2. Недостаток текущей реализации был в том что программа на каждом пользователи фильтровала массив обектов сессий
Решение - группировать сессии пользователя по `user_id` при парсинге файла и потом достовать их уже по ключу
3. После этого при входящих данных в 8000 строк время работы уменьшилось с 0.504 до 0.0685, то есть программа ускорилась ~7 раз
4. После этого исправленная проблемма перестала быть точкой роста


### Ваша находка №2
1. Отчет callstack.htm показал что теперь основное время программа находится в `Array#each` вызовы которой происходит в нескольких местах: обход строк файла, обход пользователей для формирования объектов пользователей. Так как обход строк файла нам необходим
2. Решил оптимизировать обход объектов пользователей - не делать повторный обход, а создавть пользователя сразу при обходе строк
3. Прирост метрики по бенчмаркам был незначительный 5%
4. По отчету точка роста изменилась - теперь программа большую часть времени стала проводить в методе `collect_stats_from_users` 34%
Но я приблизился к бюджету - после оптимизации этой точки роста сборка отчета всех данных занимает 44 сек.

### Ваша находка №3
1. Отчет callstack.htm показал что теперь основное время программа находится в `collect_stats_from_users`
2. решил повторно не обходить массив обектов пользователя, а собирать по ним данные сразу во время парсинга
3. Время полног отчета хоть и уменьшилось до 41 сек, но большого прироста я не получил, по этому сделал откат назат
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?

### Ваша находка №X
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
### Ваша находка №4
1. Еще раз сделал отчет callstack.htm с выключенным gc - отчет показа что есть точка рост в методе который собирает сессии пользователей, а также на инкрементирование прогресс бара
2. Оптимизация заключалась в следующем - добавлять сессии сразу в обект пользователя при обходе строк файла и отключить прогресс-бар при бенчмарках
3. Получил ощютимый прирост - формирование полного за 29.8777 сек.
4. После этого исправленная проблемма перестала быть точкой роста и я вышел на целевой бюджет - время формирования отчета за 30 сек


## Результаты
В результате проделанной оптимизации наконец удалось обработать файл с данными.
Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет.
Удалось улучшить метрику системы с *9,36 до 1.3274* и уложиться в заданный бюджет(полный отчет формируется за 29.8777).

*Какими ещё результами можете поделиться*
1. Пробовал запускать коллектинг данных по пользователям в несколько тредов(на кажый вызов `collect_stats_from_users(report, users_objects) do |user|` отдельный тред) - в моем случае это привело к регресу производительности с 37 сек. до 60
Copy link
Collaborator

Choose a reason for hiding this comment

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

с concurrency ещё будем разбираться в курсе

в целом в Ruby два треда не могут что-то считать одновременно; поэтому если задача именно в том чтобы что-то считать (а не ждать IO), то добавление треда не поможет, а скорее сделает хуже - как и произошло

но здорово, что проверили

2. метод `size` работает гораздо быстрее чем `count` - удалось выйграть на большом колличестве данных 3 сек.

## Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали*
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написал perfomance тест, который запускает метод `work` 10 раз и проверяет на 200_000 строках что он исполняется не более 1,5 сек.

18 changes: 0 additions & 18 deletions data.txt

This file was deleted.

Binary file removed data_large.txt.gz
Binary file not shown.
32 changes: 32 additions & 0 deletions lib/benchmarks/benchmarks_ips.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require 'benchmark/ips'
require_relative '../../lib/task-1'
require_relative '../utils/artifact_cleaner'

gb_disable = ARGV[0]

config = { stats: :bootstrap, confidence: 95}



Benchmark.ips do |x|

x.config(**config)

x.report('1000 rows') do
work('fixtures/data1000.txt', gb_disable)
end

x.report('2000 rows') do
work('fixtures/data2000.txt', gb_disable)
end

x.report('4000 rows') do
work('fixtures/data4000.txt', gb_disable)
end

x.report('8000 rows') do
work('fixtures/data8000.txt', gb_disable)
end
end

ArtifactCleaner.clean('result.json')
48 changes: 48 additions & 0 deletions lib/benchmarks/simple_benchmarks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require 'benchmark'
require_relative '../../lib/task-1'
require_relative '../utils/artifact_cleaner'

gb_disable = ARGV[0]


time_1000rows = Benchmark.realtime do
work('fixtures/data1000.txt', gb_disable)
end

time_2000rows = Benchmark.realtime do
work('fixtures/data2000.txt', gb_disable)
end

time_4000rows = Benchmark.realtime do
work('fixtures/data4000.txt', gb_disable)
end

time_8000rows = Benchmark.realtime do
work('fixtures/data8000.txt', gb_disable)
end

time_100000rows = Benchmark.realtime do
work('fixtures/data100000.txt', gb_disable)
end

time_200000rows = Benchmark.realtime do
work('fixtures/data200000.txt', gb_disable)
end

time_large = Benchmark.realtime do
work('fixtures/data_large.txt', gb_disable)
end

def printer(time, rows = 1000)
pp "Processing time from file #{rows} rows: #{time.round(4)}"
end

printer(time_1000rows)
printer(time_2000rows, 2000)
printer(time_4000rows, 4000)
printer(time_8000rows, 8000)
printer(time_100000rows, 100_000)
printer(time_200000rows, 200_000)
printer(time_large, 1_000_000)

ArtifactCleaner.clean('result.json')
Loading