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

Task2 #96

Open
wants to merge 8 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
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
data/

ruby_prof_reports/
stackprof_reports/

ruby_prof.rb
ruby_prof_grind.rb
stackprof.rb
stackprof_speedscope.rb
memory_profiler.rb

docker-valgrind-massif/
massif.out.1
25 changes: 25 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
test:
ruby test_me.rb

bm:
ruby benchmarking.rb

memory:
ruby memory_profiler.rb

prof:
ruby ruby_prof.rb

prof-graph_read:
open ruby_prof_reports/graph.html

prof-call_stack_read:
open ruby_prof_reports/call_stack.html

prof-call_grind:
ruby ruby_prof_grind.rb

prof-call_grind_read:
qcachegrind ruby_prof_reports/callgrind.out.${P}

.PHONY: test
Binary file added Massif.jpg
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions benchmarking.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require 'benchmark'
require_relative 'task-2'

# 100_000
#
# Start
# MEMORY USAGE: 634 MB
# Finish in 125.11s

# File.foreach
# MEMORY USAGE: 596 MB
# Finish in 119.95

# 40_000
#
# File.foreach
# MEMORY USAGE: 316 MB
# Finish in 15.67

# 1.
# MEMORY USAGE: 179 MB
# Finish in 16.95

# 2.
# MEMORY USAGE: 65 MB
# Finish in 0.39

# 3.
# MEMORY USAGE: 53 MB
# Finish in 0.25

# 3.
# MEMORY USAGE: 53 MB
# Finish in 0.25

# 4.
# MEMORY USAGE: 51 MB
# Finish in 0.1

# 5.
# MEMORY USAGE: 29 MB
# Finish in 0.2

# 100_000
# END
# MEMORY USAGE: 30 MB
# Finish in 0.5

time = Benchmark.realtime do |x|
work('data/data_large.txt')
end
puts "Finish in #{time.round(2)}"
164 changes: 155 additions & 9 deletions case-study-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,190 @@
Я решил исправить эту проблему, оптимизировав эту программу.

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

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

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

Вот как я построил `feedback_loop`: *как вы построили feedback_loop*
benchmarking.rb
memory-profiler
ruby-prof_call_grind + ruby-prof_call_stack
Refactoring
Test
benchmarking.rb
ruby-prof_call_stack

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

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

### Ваша находка №0
Перевод программы на потоковый подход (File.foreach вместо File.read) значимо на объем памяти и время выполнения не повлиял

Стартовое значение метрики (40_000)
# MEMORY USAGE: 316 MB
# Finish in 16.95

### Ваша находка №1
- какой отчёт показал главную точку роста
memory-profiler:
MEMORY USAGE: 5425 MB
Total allocated: 6.70 GB (2039801 objects)
allocated memory by location:
4.59 GB: `sessions = sessions + [parse_session(line)] if cols[0] == 'session'`
1.55 GB: user_sessions = sessions.select { |session| session['user_id'] == user['id'] }

allocated objects by location:
611814 - { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } }

Allocated String Report:
144564 " " - 154
107718 "session" - 61

stackprof - менее информативен, далее не пользуемся
ruby-prof flat - менее информативен, далее не пользуемся
ruby-prof graph - неплохой, пользуемся как вспомогательным
ruby-prof call_stack - отличный в плане визуала, пользуемся как вторым основным, graph не нужен
2 точки роста:
54% - Object#collect_stats_from_users
-> 26% - Date#parse
42% - Foreach#each
-> 14% - #parse_session
ruby-prof call_grind - просто лучший, хотя дольше вызывать, пользуем только его и memory-profiler

- как вы решили её оптимизировать
- как изменилась метрика
Array#<< вместо Array#+ в `sessions = sessions + [parse_session(line)] if cols[0] == 'session'`

- как изменилась метрика: уменьшилась в 2 раза
# MEMORY USAGE: 179 MB

- как изменился отчёт профилировщика
memory-profiler:
1.6MB: `sessions = sessions + [parse_session(line)] if cols[0] == 'session'`

### Ваша находка №2
- какой отчёт показал главную точку роста
memory-profiler:
Total allocated: 1.96 GB (1959801 objects)
1.66 GB: user_sessions = sessions.select { |session| session['user_id'] == user['id'] }
allocated memory by class
-----------------------------------
1.69 GB Array
66.92 MB String

call_stack:
56% - Object#collect_stats_from_users
-> Array#each
-> Array#map
-> Data#parse

- как вы решили её оптимизировать
Снова идем за memory-profiler: в Array#each с #select меняем Array#+ на Array#<<. Эффект однако небоьшой, проблема -
в неприлично разбухающем количестве select-массивов при итерации юзеров. Поэтому заменил их промежуточным хэшем с одним проходом по sessions, из которого потом удобно формировать хэш юзеров.

- как изменилась метрика
# MEMORY USAGE: 65 MB: в 3 раза
# Finish in 0.38

- как изменился отчёт профилировщика
1.18 MB:)
allocated memory by class
-----------------------------------
67.16 MB String
28.59 MB Hash
26.59 MB Array

### Ваша находка №3
- какой отчёт показал главную точку роста
Возвращаемся к отчетам ruby-prof, который теперь более показательные чем memory-profiler
memory-profiler:
48.05 MB - #map
call_stack:
56% - Object#collect_stats_from_users
-> Array#each
-> Array#map
-> Date#parse
Array#map [67551 calls, 67553 total]

Слишком много map
- как вы решили её оптимизировать
Убираем лишние maps при вызовах Object#collect_stats_from_users, также уберем Date.parse

- как изменилась метрика: меньше, чем хотелось бы
MEMORY USAGE: 53 MB
Finish in 0.25

### Ваша находка №X
- как изменился отчёт профилировщика
18% - Object#collect_stats_from_users
Array#map [6141 calls, 12284 total]

### Ваша находка №4
- какой отчёт показал главную точку роста
memory-profiler:
19MB: fields = session.split(',')
16MB: cols = line.split(',')

ruby-call-stack:
31.37% (41.21%) Object#parse_session [33859 calls, 33859 total]
21.96% (70.00%) String#split [33859 calls, 80000 total]

Проблема - в String#split в parse_session и parse_user
Убрал лишние #split, но лучше не стало
- как изменился отчёт профилировщика
28.85% (100.00%) Array#each [1 calls, 6144 total]
29.96 MB String
14.36 MB Array
9.24 MB Hash
Попробуем убрать лишние хранилища данные - хэши и массивы: в частности - промежуточный объект sessions и плохую сборку уникальных массивов

- как изменилась метрика: никак, но скорость уменьшилась более, чем двукратно (за счет удаления лишних переборов), однако на для памяти по сравнению с тем, чем она уже забита, это - ничто. И это первый вывод - смотри что оптимизируешь!

data_40k.txt
MEMORY USAGE: 51 MB
Finish in 0.1

data_large.txt
MEMORY USAGE: 1889 MB
Finish in 17.85

### Ваша находка №6
И тут я вспомнил, что забыл поменять запись файла на потоковую:(
- как вы решили её оптимизировать
- как изменилась метрика
Поменял запись
- как изменилась метрика: с одной стороны не сильно, но с другой - очевидно, что поменялась асимптотика
data_40k.txt
MEMORY USAGE: 29 MB
Finish in 0.2

data_large.txt
MEMORY USAGE: 31 MB
Finish in 15.81

- как изменился отчёт профилировщика
66.8MB - File.write
19MB - String.split
В целом значимых точек роста нет. Можно еще поработать со строками: #start_with?, символы в хэшах и проч., но во-первых все это уже делал, во-вторых - очевидно, что значимого влияния не будет и временные затраты себя не оправдают, и в-главных - бюджет достигнут с 31MB при необходимых 70, важно во-время остановится)

## Результаты
В результате проделанной оптимизации наконец удалось обработать файл с данными.
Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет.
Удалось улучшить метрику системы с *634 MB до 30MB для 100_000 строк, data_large.txt: 31MB* и уложиться в заданный бюджет.

*Какими ещё результами можете поделиться*
String#split - тяжелый, Array#<< - эффективный, потоки хороши (как обычно), всегда смотри что оптимизруешь)
Copy link
Collaborator

Choose a reason for hiding this comment

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

split не то чтобы тяжёлый, он просто создаёт объекты в нашем случае, на каждую строчку по штук 5, потом они удаляются GC

В принципе можно воспользосаться split with block, там изнутри блока можно записывать значения в заранее созданные 5 переменных и наверно избежать создания лишних объектов

Но создание этих объектов не критично. Для нас тут самое главное, что мы не накапливаем в памяти большого объёма данных, и GC может удалять ненужные объекты и стабильно держать потребление памяти на 40Мб при обработке сколь угодно большого файла

Copy link
Collaborator

Choose a reason for hiding this comment

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

Array << не то чтобы эффективный, скорее a = a + [b] дико не эффективно, я бы так поставил акцент


Massif
❯ ./profile.sh
==1== Massif, a heap profiler
==1== Copyright (C) 2003-2015, and GNU GPL'd, by Nicholas Nethercote
==1== Using Valgrind-3.12.0.SVN and LibVEX; rerun with -h for copyright info
==1== Command: ruby main.rb
==1==
MEMORY USAGE: 52 MB (37 MB on Visualizer)
==1==

## Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали*
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали*: perormance.rb
3 changes: 3 additions & 0 deletions main.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require_relative 'task-2.rb'

work('data/data_large.txt')
16 changes: 16 additions & 0 deletions performance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require 'rspec/core'
require 'rspec-benchmark'
require_relative 'task-2'

RSpec.configure do |config|
config.include RSpec::Benchmark::Matchers
end

describe 'basic work' do
let(:filepath) { 'data/data_test.txt' }

it 'eats less than 30MB' do
expect { work(filepath) }
.to perform_allocation(30_000_000).bytes
end
end
7 changes: 7 additions & 0 deletions profile.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

docker run -it \
-v $(pwd):/home/massif/test \
-e DATA_FILE=data/data_large.txt \
spajic/docker-valgrind-massif \
valgrind --tool=massif ruby main.rb
1 change: 1 addition & 0 deletions result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"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"]}},"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"}
Loading