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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/profiling/results/*
/samples/data_*
/test/data.txt
/test/result.json
114 changes: 96 additions & 18 deletions case-study-template.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Case-study оптимизации
# Case-study оптимизация. Часть 2.

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

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

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

## Асимптотика
До оптимизации:
```
Calculating -------------------------------------
File: 512Kb 4.817 (± 5.3%) i/s - 24.000 in 5.032638s
File: 1Mb 2.527 (± 6.3%) i/s - 13.000 in 5.183298s
File: 2Mb 1.203 (±15.4%) i/s - 6.000 in 5.132240s
File: 4Mb 0.586 (±19.5%) i/s - 3.000 in 5.269748s
File: 8Mb 0.287 (±14.0%) i/s - 2.000 in 7.100129s
with 99.0% confidence

Comparison:
File: 512Kb: 4.8 i/s
File: 1Mb: 2.5 i/s - 1.91x (± 0.15) slower
File: 2Mb: 1.2 i/s - 4.00x (± 0.68) slower
File: 4Mb: 0.6 i/s - 8.18x (± 1.96) slower
File: 8Mb: 0.3 i/s - 16.77x (± 3.11) slower

```
Исходя из данных показателей мы видим, что увеличение объема обрабатываемых данных в 2 раза ведет к росту времени работы в 2 раза

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

Вот как я построил `feedback_loop`: *как вы построили feedback_loop*
Вот как я построил `feedback_loop`:
1. Изначально выбрал маленький размер исходных данных (около 1Мб) позволяющий скрипту успешно отработать без оптимизаций
2. Поиск базовой метрики (время и память)
3. Дописал тест на регрессию по времени и памяти
4. Профилирование и поиск "точек роста"
5. Внесение изменений в код
6. Повторное тестирование и сбор новых метрик
7. Увеличение объема данных

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

Гемы:
* ruby-prof
* memory_profiler
* get_process_mem
* benchmark-ips
Stdlib:
* benchmark
C
* valgrind (tool - massif)

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

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

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

### Ваша находка №X
О вашей находке №X
Согласно результатам ruby-prof в режиме cpu:
```
%self total self wait child calls name
23.99 9.674 2.918 0.000 6.756 1 IO#each_line
6.89 1.007 0.838 0.000 0.169 48639 Array#map
6.74 1.413 0.819 0.000 0.594 89009 <Class::Date>#iso8601
6.71 2.488 0.816 0.000 1.672 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json
5.94 2.302 0.722 0.000 1.581 89009 Object#parse_session
5.64 3.431 0.685 0.000 2.746 16214 Object#aggregate_user_stats
```
### Находка №1
Проанализировав исходные данные выяснилось, что даты изначально приходят в нужном формате, поэтому нет нужды создавать Date объект
Copy link
Owner

Choose a reason for hiding this comment

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

👍

Также были убраны неиспользуемые поля, а вместо полей first_name и last_name было сделано поле full_name.
```
Comparison:
File: 512Kb: 10.9 i/s
File: 1Mb: 5.9 i/s - 1.85x (± 0.14) slower
File: 2Mb: 2.8 i/s - 3.94x (± 0.57) slower
File: 4Mb: 1.4 i/s - 7.90x (± 1.57) slower
File: 8Mb: 0.6 i/s - 16.93x (± 5.53) slower
with 99.0% confidence
```
Фактически мы видим двукратный прирост производительности

### Находка №2
Также стандартная json библиотека не самая производительная заменив ее на gem oj, мы получили еще небольшой прирост:
```
Comparison:
File: 512Kb: 12.5 i/s
File: 1Mb: 7.0 i/s - 1.78x (± 0.12) slower
File: 2Mb: 3.4 i/s - 3.68x (± 0.42) slower
File: 4Mb: 1.6 i/s - 7.71x (± 1.27) slower
File: 8Mb: 0.8 i/s - 15.36x (± 3.33) slower
with 99.0% confidence
```

Однако нерешенной остается проблема резкого выделения [памяти](https://imgur.com/cvkmvMS) связанная с формирование json строки

### Находка №3
Вместо библиотеки, которая целиком парсит hash в json, можно использовать yajl, которая позволяет "стримить" json в файл
Copy link
Owner

Choose a reason for hiding this comment

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

👍 плюсик за стриминг!

Мы немного проигрываем в производительности на маленьких файлах
```
Comparison:
File: 512Kb: 12.3 i/s
File: 1Mb: 6.8 i/s - 1.81x (± 0.13) slower
File: 2Mb: 3.3 i/s - 3.67x (± 0.33) slower
File: 4Mb: 1.6 i/s - 7.56x (± 1.22) slower
File: 8Mb: 0.8 i/s - 16.21x (± 5.33) slower
with 99.0% confidence
```
Однако мы существенно сокращаем объемы потребляемой ![памяти](https://imgur.com/pzdL2kW)

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

*Какими ещё результами можете поделиться*
В среднем файл обрабатывается за 28 - 29 секунд при средних затратах по памяти 600 - 620Мб без резких скачков по памяти

## Защита от регресса производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали*
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавлены дополнительные тесты для защиты от регрессий по памяти и времени выполнения
Binary file removed data_large.txt.gz
Binary file not shown.
104 changes: 104 additions & 0 deletions lib/task-1.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# frozen_string_literal: true

require 'yajl'
#require 'ruby-progressbar'

def parse_user(fields)
{
id: fields[1],
full_name: fields[2] << " " << fields[3]
}
end

def parse_session(fields)
{
user_id: fields[1],
browser: fields[3].upcase!,
time: fields[4].to_i,
date: fields[5].chomp!,
}
end

def aggregate_user_stats(data)
return {} unless data

user = data[1][:user]
sessions = data[1][:sessions]
time = sessions.map {|s| s[:time] }
browsers = sessions.map { |s| s[:browser] }
{
user[:full_name]=> {
sessionsCount: sessions.count,
totalTime: time.sum.to_s << ' min.',
longestSession: time.max.to_s << ' min.',
browsers: browsers.sort!.join(', '),
usedIE: browsers.any? { |b| b.include?('INTERNET EXPLORER') },
alwaysUsedChrome: browsers.all? { |b| b.include?('CHROME')},
dates: sessions.map{|s| s[:date]}.sort!.reverse!
}
}
end

def work(input: "data.txt", output: "result.json")
# Отчёт в json
# - Сколько всего юзеров +
# - Сколько всего уникальных браузеров +
# - Сколько всего сессий +
# - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом +
#
# - По каждому пользователю
# - сколько всего сессий +
# - сколько всего времени +
# - самая длинная сессия +
# - браузеры через запятую +
# - Хоть раз использовал IE? +
# - Всегда использовал только Хром? +
# - даты сессий в порядке убывания через запятую +

data = {}
_tmp_hash = {}
report = {
totalUsers: 0,
uniqueBrowsersCount: 0,
totalSessions: 0,
allBrowsers: [],
usersStats: {}
}

#bar_output = ENV["NOPROGRESS"] ? File.open(File::NULL, "w") : $stdout
#bar = ProgressBar.create(total: nil, output: bar_output)
File.open(input) do |f|
f.each_line do |line|
cols = line.split(',')
key = cols[1]
if cols[0] == "user"
if data[key].nil?
report[:usersStats].merge!(aggregate_user_stats(data.shift))
end
data[key] ||= {}
data[key][:user] = parse_user(cols)
report[:totalUsers] += 1
else
data[key] ||= {}
data[key][:sessions] ||= []
session = parse_session(cols)
browser = session[:browser]
if _tmp_hash[browser].nil?
_tmp_hash[browser] = 1
report[:allBrowsers].push(browser)
report[:uniqueBrowsersCount] += 1
end
report[:totalSessions] += 1
data[key][:sessions].push(session)
end
# bar.increment
end
report[:usersStats].merge!(aggregate_user_stats(data.shift))
report[:allBrowsers] = report[:allBrowsers].sort!.join(',')
# bar.finish
end
File.open(output, 'w') do |f|
Yajl::Encoder.encode(report, f)
f << "\n"
end
end
46 changes: 46 additions & 0 deletions profiling/ips.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
ENV["NOPROGRESS"] = '1'
require_relative '../lib/task-1.rb'
require 'benchmark/ips'

FILES = {
"512Kb" => 524_268,
"1Mb" => 1_048_576,
"2Mb" => 2_097_152,
"4Mb" => 4_194_304,
"8Mb" => 8_388_608,
}.freeze

class GCSuite
def warming(*)
run_gc
end

def running(*)
run_gc
end

def warmup_stats(*)
end

def add_report(*)
end

private

def run_gc
GC.enable
GC.start
GC.disable
end
end


suite = GCSuite.new

Benchmark.ips do |x|
x.config(suite: suite, stats: :bootstrap, confidence: 99)
FILES.keys.each do |size|
x.report("File: #{size}") { work(input: "../samples/data_#{size}", output: "/dev/null") }
end
x.compare!
end
14 changes: 14 additions & 0 deletions profiling/ruby-prof.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ENV["NOPROGRESS"] = '1'

require_relative '../lib/task-1.rb'
require 'ruby-prof'

RubyProf.measure_mode = RubyProf::CPU_TIME
result = RubyProf.profile do
work(input: "../samples/data_4Mb", output: "/dev/null")
end

printer = RubyProf::MultiPrinter.new(result)
printer2 = RubyProf::CallTreePrinter.new(result)
printer.print(path: "./results", profile: "task-1")
printer2.print(path: "./results", profile: "task-1_calltree")
10 changes: 10 additions & 0 deletions profiling/stack-prof.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ENV["NOPROGRESS"] = '1'

require_relative '../lib/task-1.rb'
require 'stackprof'

mode = ENV["MODE"] || :wall

StackProf.run(mode: mode.to_sym, out: "./results/stackprof.dump") do
work(input: "../samples/data_4Mb", output: "/dev/null")
end
1 change: 0 additions & 1 deletion result.json

This file was deleted.

23 changes: 23 additions & 0 deletions samples/create_samples.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

FILES = {
"512Kb" => 524_268,
"1Mb" => 1_048_576,
"2Mb" => 2_097_152,
"4Mb" => 4_194_304,
"8Mb" => 8_388_608,
}.freeze

FILES.each do |name, size|
copied = 0
source = File.open("data_large.txt")
target = File.open("data_#{name}", "w+")
while (line = source.gets)
break if copied >= size
target.puts line
copied += line.size
end
ensure
source.close
target.close
end
Loading