Skip to content
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
data/*
result.json
ruby_prof_flat.txt
12 changes: 12 additions & 0 deletions asymptotics.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require './task-2.rb'
require 'benchmark/ips'

Benchmark.ips do |bench|
bench.config(stats: :bootstrap, confidence: 99)
bench.warmup = 0
bench.report('Process 1Mb') { work('data/data_1MB.txt') }
bench.report('Process 4Mb') { work('data/data_4MB.txt') }
bench.report('Process 10Mb') { work('data/data_10MB.txt') }

bench.compare!
end
138 changes: 126 additions & 12 deletions case-study-template.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Case-study оптимизации
# Case-study оптимизации, часть 2 [(часть 1)](https://github.com/KirkovAlexey/task-1/blob/master/case-study-template.md)

## Актуальная проблема
В нашем проекте возникла серьёзная проблема.
Expand All @@ -12,35 +12,149 @@
Я решил исправить эту проблему, оптимизировав эту программу.

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

- Измерять количество потребляймой памяти
- Измерять количество затраченного времени

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

## Асимптотика

Для упрощения работы с данными подготовил программу `preparing-data.rb`, которая генерит данные нескольких размеров (1mb, 4Mb, 10Mb и оригинальный файл) из архива.
Copy link
Owner

Choose a reason for hiding this comment

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

👍


Для просмотра метрик я использую программу `asymptotics.rb`, которая замеряет метрики для данных разного объема.

До начала оптимизации были следующие параметры

``` shellsession

Calculating -------------------------------------
3.658 (± 1.7%) i/s - 19.000 in 5.200075s
0.938 (± 3.6%) i/s - 5.000 in 5.337153s
0.379 (± 3.2%) i/s - 2.000 in 5.289088s
with 99.0% confidence

Comparison:
Process 1Mb: 3.7 i/s
Process 4Mb: 0.9 i/s - 3.90x (± 0.16) slower
Process 10Mb: 0.4 i/s - 9.66x (± 0.44) slower
with 99.0% confidence

```

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

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

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

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

`benchmark`, `memory_profiler`, `ruby-prof`, `benchmark-ips`, `valgrind`

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

``` shellsession

%self total self wait child calls name
16.15 0.354 0.068 0.000 0.286 3 Array#each
10.94 0.046 0.046 0.000 0.000 22614 Array#include?
10.81 0.045 0.045 0.000 0.000 26693 String#split
10.33 0.047 0.043 0.000 0.004 22614 <Class::Date>#strptime
7.64 0.138 0.032 0.000 0.106 22614 Object#parse_session
7.23 0.057 0.030 0.000 0.027 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json
4.98 0.021 0.021 0.000 0.000 67478 String#encode
3.58 0.015 0.015 0.000 0.000 22614 Date#iso8601
3.45 0.014 0.014 0.000 0.000 8156 Array#map
2.56 0.011 0.011 0.000 0.000 17028 String#=~
2.34 0.010 0.010 0.000 0.000 68242 Array#last
2.08 0.009 0.009 0.000 0.000 8157 Array#sort
```

Метрика была получена с помощью `ruby-prof` в режиме `FlatPrinter`

### Ваша находка №1
О вашей находке №1

Аналируя метрику я увидел, что на четвертой строчке стоит парсинг даты.
В прошлой части я уже изменил парсинг даты с `Date.parse` на `Date.strptime`, это принесло результат.
В этот раз я обнаружил, что формат даты изначально совпадает с форматом `iso8601` и это означает что возможно удалить избытоное преобразование.

Метод после изменения:

``` shellsession
def parse_session(session)
fields = session.split(COMMA)
{
'user_id' => fields[1],
'session_id' => fields[2],
'browser' => fields[3].upcase,
'time' => fields[4].to_i,
'date' => fields[5]
}
end
```

После удаления избыточного преобразования время выполнения метода `parse_session` уменьшилось.
Вместо 32.72% стало 21.71% из 100% времени работы скрипта.

Профилировка выполнялась помощью `ruby-prof` в режиме `GraphHtmlPrinter`

``` shellsession
32.72% 7.62% 0.14 0.03 0.00 0.10 22614 Object#parse_session 32
0.04 0.04 0.00 0.00 22614/22614 <Class::Date>#strptime 34
0.03 0.03 0.00 0.00 22614/26693 String#split 32
0.02 0.02 0.00 0.00 22614/22614 Date#iso8601 34
0.01 0.01 0.00 0.00 22614/22614 String#upcase 34
0.01 0.01 0.00 0.00 22614/22614 String#to_i 34
```

``` shellsession
21.71% 7.04% 0.07 0.02 0.00 0.05 22614 Object#parse_session 32
0.04 0.04 0.00 0.00 22614/26693 String#split 32
0.01 0.01 0.00 0.00 22614/22614 String#upcase 34
0.00 0.00 0.00 0.00 22614/22614 String#to_i 34
```

### Ваша находка №2
О вашей находке №2

### Ваша находка №X
О вашей находке №X
По отчету видно что второе место по затратам занимает метод `Array#include?`, который используется при формировании массива с сессиями юзеров.
При анализе исходного кода видно, что мы можем использовать `SortedSet`, так как это нам даст выигрыш в скорости при поиске элемента в массиве и не будет необходимости сортировать массив для статистики.
Copy link
Owner

Choose a reason for hiding this comment

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

👍


Замеры при помощи `ruby-prof` в режиме `GraphHtmlPrinter` показали прирост времени выполения с `0.33529114723205566` до `0.31107091903686523`
Затраченное время также сократилось с `0.05` до `0.01` для `Array#include?` и `Set#include?` соотвественно.

### Ваша находка №3

Пятое место при профилировании с помощью `RubyProf::CPU_TIME` занимает преобразование `hash` в `json`.
Copy link
Owner

Choose a reason for hiding this comment

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

Можно было бы выиграть больше, начиная с первого!
По отчёту просто не понятно, что именно там за Array#each, но я показывал на 2й лекции, как с этим побороться.
Можно отрефачить все вызовы each в именованные методы, и тогда отчёт покажет, кто же конкретно на первом месте.

Это можно оптимизировать с помощью замены библиотеки на более быструю.
До этого я уже сталкивался с такой проблемой и знаю, что самая быстрая библиотека это `Oj`.
Будем использовать ее и записывать сразу в файл результат `Oj.to_file`

В итоге скорость изменилась с

``` shellsession
18.05% 9.01% 0.26 0.13 0.00 0.13 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json
```

на

``` shellsession
0.55% 0.55% 0.01 0.01 0.00 0.00 1 <Module::Oj>#to_file
```

Общее время также уменьшилось с `1.449656` до `1.1936809999999998`

## Результаты
В результате проделанной оптимизации наконец удалось обработать файл с данными.
Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце*
Я также пробовал заменить `each` на `while`, но это давало грошевую оптимизацию и не приносило ощутимой разницы в метрике.

*Какими ещё результами можете поделиться*

## Защита от регресса производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали*
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы был добавлен тест регрессии по времени
28 changes: 28 additions & 0 deletions feedback-loop.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require './task-2.rb'
require 'benchmark/ips'
require 'minitest/autorun'

class Test < Minitest::Test
def setup
File.write('result.json', '')
end

def test_result
Benchmark.ips do |bench|
bench.report("Process 1 MB of data") do
work('data/data_1MB.txt')
end
end
end

def test_correctness
work('fixtures/data.txt')
expected_result = File.read('fixtures/expected_result.json')
assert_equal expected_result, File.read('result.json')
end

def test_time
result = Benchmark.realtime { work('data/data_1MB.txt') }
assert(result.round(2) < 0.2)
end
end
18 changes: 18 additions & 0 deletions fixtures/data.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
user,0,Leida,Cira,0
session,0,0,Safari 29,87,2016-10-23
session,0,1,Firefox 12,118,2017-02-27
session,0,2,Internet Explorer 28,31,2017-03-28
session,0,3,Internet Explorer 28,109,2016-09-15
session,0,4,Safari 39,104,2017-09-27
session,0,5,Internet Explorer 35,6,2016-09-01
user,1,Palmer,Katrina,65
session,1,0,Safari 17,12,2016-10-21
session,1,1,Firefox 32,3,2016-12-20
session,1,2,Chrome 6,59,2016-11-11
session,1,3,Internet Explorer 10,28,2017-04-29
session,1,4,Chrome 13,116,2016-12-28
user,2,Gregory,Santos,86
session,2,0,Chrome 35,6,2018-09-21
session,2,1,Safari 49,85,2017-05-22
session,2,2,Firefox 47,17,2018-02-02
session,2,3,Chrome 20,84,2016-11-25
1 change: 1 addition & 0 deletions fixtures/expected_result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"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"]}}}
42 changes: 42 additions & 0 deletions preparing-data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

require 'zlib'

DATA = {
'1MB' => 1_048_576,
'4MB' => 4_194_304,
'10MB' => 10_485_760,
'large' => nil
}.freeze

DIR = 'data'.freeze

def create_directory
Dir.mkdir(DIR) unless File.exist?(DIR)
end

def ungz_file
gz = Zlib::GzipReader.open('data_large.txt.gz')
unzipped = StringIO.new(gz.read)
gz.close
unzipped
end

def create_data(result = ungz_file)
DATA.each do |name, size|
size ||= result.size
result.pos = 0
copied = 0
target = File.open("#{DIR}/data_#{name}.txt", 'w+')
while (line = result.gets)
break if copied >= size

target.puts line
copied += line.size
end
target.close
end
end

create_directory
create_data
2 changes: 1 addition & 1 deletion result.json

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions rubyp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'ruby-prof'
require './task-2.rb'

# RubyProf.measure_mode = RubyProf::WALL_TIME
RubyProf.measure_mode = RubyProf::CPU_TIME

result = RubyProf.profile do
work('data/data_1MB.txt', disable_gc: true)
end

# printer = RubyProf::FlatPrinter.new(result)
# printer.print(File.open('ruby_prof_flat.txt', 'w+'))

printer2 = RubyProf::GraphHtmlPrinter.new(result)
printer2.print(File.open('ruby_prof_graph.html', 'w+'))
#
# printer3 = RubyProf::CallStackPrinter.new(result)
# printer3.print(File.open('ruby_prof_callstack.html', 'w+'))

# printer4 = RubyProf::CallTreePrinter.new(result)
# printer4.print(:path => ".", :profile => 'callgrind')
Binary file added screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading