Skip to content
Closed
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/
data_large.txt
result.json
benchmarks/CPU/flat.txt
data_large.txt.gz
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
16 changes: 16 additions & 0 deletions benchmarks/CPU/benchmark-ips.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require 'benchmark/ips'

require_relative '../../task-1'

Benchmark.ips do |x|
# The default is :stats => :sd, which doesn't have a configurable confidence
# confidence is 95% by default, so it can be omitted
x.config(
stats: :bootstrap,
confidence: 95,
)

x.report("work") do
work('../data.txt')
end
end
9 changes: 9 additions & 0 deletions benchmarks/CPU/benchmark.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require 'benchmark'

require_relative '../../task-1'

time = Benchmark.realtime do |x|
work('../../data_large.txt')
end

puts "Finish in #{time}"
27 changes: 27 additions & 0 deletions benchmarks/CPU/other-benchmark-ips.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require 'benchmark/ips'
require 'oj'
require 'json'
require 'multi_json'

Benchmark.ips do |x|

hash = {:totalUsers=>3, :uniqueBrowsersCount=>14, :totalSessions=>15, :allBrowsers=>"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49", :usersStats=>{"Leida Cira"=>{:sessionsCount=>6, :totalTime=>"455 min.", :longestSession=>"118 min.", :browsers=>"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39", :usedIE=>true, :alwaysUsedChrome=>false, :dates=>["2017-09-27", "2017-03-28", "2017-02-27", "2016-10-23", "2016-09-15", "2016-09-01"]}, "Palmer Katrina"=>{:sessionsCount=>5, :totalTime=>"218 min.", :longestSession=>"116 min.", :browsers=>"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17", :usedIE=>true, :alwaysUsedChrome=>false, :dates=>["2017-04-29", "2016-12-28", "2016-12-20", "2016-11-11", "2016-10-21"]}, "Gregory Santos"=>{:sessionsCount=>4, :totalTime=>"192 min.", :longestSession=>"85 min.", :browsers=>"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49", :usedIE=>false, :alwaysUsedChrome=>false, :dates=>["2018-09-21", "2018-02-02", "2017-05-22", "2016-11-25"]}}}

x.report("oj") do
Oj.dump(hash)
end

x.report('json') do
hash.to_json
end

x.report('MultiJson') do
MultiJson.dump(hash)
end

x.report('MultiJson pretty') do
MultiJson.dump(hash, :pretty => true)
end

x.compare!
end
12 changes: 12 additions & 0 deletions benchmarks/CPU/ruby-prof-flat.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require 'ruby-prof'
require_relative '../../task-1'

RubyProf.measure_mode = RubyProf::WALL_TIME

result = RubyProf.profile do
GC.disable
work('../../data.txt')
end

printer = RubyProf::FlatPrinter.new(result)
printer.print(File.open("flat.txt", "w+"))
46 changes: 0 additions & 46 deletions case-study-template.md

This file was deleted.

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

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

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

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

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

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

## Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: Количество итераций в секунду.

При чтении и парсинга файла data.txt Выходные данные из benchmark-ips

`2.995k (± 1.5%) i/s`

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

Вот как я построил `feedback_loop`:

* Тестирование
* Создание профайла. Поиск точек старта
* Оптимизация кода

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

- rbspy
- benchmark
- ruby-prof

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

### Ваша находка №1
Вынес тест отдельно из программы. Чтобы не мазолил глаза. Не место тестам в теле основной программы.
Добавил возможность менять подключаемый файл с данными и отключение GC. Через аттрибуты метода work.
Для поиска точки роста использовал профилировщики:
* rbspy - не дал результатов. Основная нагрузка была в самом block in work. Метод work грамозкий и является антипатерном GodObject.
Для получения более подробной информации метод надо разбить на простые методы.
* ruby-prof - Указал на высокую нагрузку CPU при чтении и записи данных. А так же при парсинге даты.
* benchmarks/ips (file: data.txt)
`2.995k (± 1.5%) i/s`

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

* Использовал метод File#foreach. Чтобы не записывать все данные в память. А проводить все операции по строчно.
Обернул реализацию парсинга данных в блок #foreach

И всё пошло по бороде. Если начинать с точки старта, требуется для начало провести не хилый рефакт

Довел код до зеленных тестов. Пока получилось правда с запашком.

Треды не включал. Всё происходит синхронно. Тесты запускаю в IDE Rubymine, что возможно могут дать не точные результаты.
А они вообще могут быть точными?

#### Результаты benchmarks/ips на маленьком файле

`2.111k (± 3.4%) i/s - 10.441k in 5.035906s`

#### Результат ruby-prof на большом файле
```
Total: 52.027112

14.89 39.910 7.745 0.000 32.166 1 <Class::IO>#foreach
10.85 8.168 5.642 0.000 2.525 2750940 User#update
7.90 4.108 4.108 0.000 0.000 3250940 String#split
7.76 8.193 4.037 0.000 4.157 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json
```

### Ваша находка №2
Вынес код чтения файла и формирование отчета для пользователя в отдельный метод

#### Результаты benchmarks/ips на маленьком файле

```
2.929k (± 8.9%) i/s - 7.608k in 25.412108s
with 95.0% confidence
```
#### Результат ruby-prof на большом файле
```
Total: 48.899145

15.33 40.670 7.496 0.000 33.174 1 <Class::IO>#foreach
11.68 8.254 5.713 0.000 2.541 2750940 User#update
8.74 4.275 4.275 0.000 0.000 3250940 String#split
7.17 3.505 3.505 0.000 0.000 8250940 <Class::User>#instance
7.06 16.015 3.450 0.000 12.565 500000 Object#make_report
```

### Ваша находка №3
Операции с потоками вода/вывода данных очень тяжелые. Если с записью файлов пока ни чего не сделать.
То на с чтением можно по работать. Как раз на это и указывает профайл от ruby-prof.
Я вынес чтение файла в отдельный метод. Файл так же читается по строчно и заполняет массив строк.
Агрегирование данных будет происходить из данного массива.

#### benchmarks/ips Дал не значительный прирост c маленьким файлом.

#### Результат ruby-prof на большом файле
А тут уже интересно)
```
Total: 36.541374
Sort by: self_time

%self total self wait child calls name location
13.75 28.591 5.026 0.000 23.565 1 Array#reject!
11.51 5.818 4.206 0.000 1.612 2750940 User#update /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:11
8.84 3.229 3.229 0.000 0.000 3250940 String#split
8.44 5.849 3.085 0.000 2.764 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json
7.31 11.575 2.671 0.000 8.904 500000 Object#make_report /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:39
6.55 2.392 2.392 0.000 0.000 8250940 <Class::User>#instance /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:15
5.36 1.957 1.957 0.000 0.000 8126279 String#encode
3.30 1.205 1.205 0.000 0.000 1 <Class::IO>#foreach
3.01 1.099 1.099 0.000 0.000 2750940 Set#add /usr/share/rvm/
```

Но чтобы память не забивалась. Требуется очищать массив от устаревших данных.
А так же можно будет поиграться с тредами.

C тредами поялвилась проблема с последовательностью записи данных в отчет. Чтобы не забивать память массивом строк (а после это появились фризы). Решил откатить на время измерения. Буду продолжать отимизацию читаю файл по строчно.

### Ваша находка №4
Отчет ruby-prof указал на точку раста создания юзера.
```
12.91 29.410 5.906 0.000 23.505 3250940 Object#make_user /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:59
10.76 4.924 4.924 0.000 0.000 3250940 String#split
9.58 6.181 4.381 0.000 1.800 2750940 User#update /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:10
7.41 37.725 3.391 0.000 34.334 1 <Class::IO>#foreach
7.20 7.423 3.293 0.000 4.130 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json
```
При агргировании данных формируется излишний класс User после которого идёт агрегация данных.
Решение:
Убрать абстракию User

Провел очередной рефакт. Убрал абстракцию User.

###Результаты
На агреггирование большого файала ушло время
Finish in 25.983555996994255

В рамки бюджета уложились

ruby-prof показал такую информацию

```
Total: 0.000528
Sort by: self_time

%self total self wait child calls name location
15.18 0.000 0.000 0.000 0.000 18 Object#make_report /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:39
11.48 0.000 0.000 0.000 0.000 1 File#initialize
10.68 0.000 0.000 0.000 0.000 1 IO#close
8.59 0.000 0.000 0.000 0.000 1 <Class::IO>#foreach
5.49 0.000 0.000 0.000 0.000 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json
3.88 0.000 0.000 0.000 0.000 18 String#split
3.81 0.001 0.000 0.000 0.001 1 [global]# ruby-prof-flat.rb:7
3.69 0.000 0.000 0.000 0.000 36 Object#user? /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:31
2.62 0.001 0.000 0.000 0.000 1 Object#work /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:4
2.59 0.000 0.000 0.000 0.000 54 String#encode
2.20 0.000 0.000 0.000 0.000 10 Array#join
2.07 0.000 0.000 0.000 0.000 30 Object#browser /home/pavel/Документы/rails-optimization-2-task1/task-1.rb:35
1.75 0.000 0.000 0.000 0.000 15 Set#add /usr/share/rvm/rubies/ruby-2.6.3/lib/ruby/2.6.0/set.rb:348
1.72 0.000 0.000 0.000 0.000 44 String#upcase
```
Слишком часто вызывается метод предикат user. Оптимизировал данный процесс.

Проверил сериализацию данных из хеш в json, (хещ был взять с данных тестового файла) таких гемов:
* to_json
* oj
* multi_json

```
Comparison:
oj: 199997.6 i/s
MultiJson: 120658.5 i/s - 1.66x slower
MultiJson pretty: 96294.8 i/s - 2.08x slower
json: 54341.7 i/s - 3.68x slower

```

Oj стал фаворитом. Но сталкнулся с проблемой. Он не переводит символы в строки. (подобная проблема уже есть в issue)
Поставил библиотеку multi_json

Результат
Finish in 21.9 - 23.8

## Результаты
В результате проделанной оптимизации наконец удалось обработать файл с данными за 21-23 секунды.



## Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы был добавлен тест агреггирование 100_000 строк данных
Binary file removed data_large.txt.gz
Binary file not shown.
Loading