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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.byebug_history
data_large.txt
data_small.txt
tmp/
graphviz.png
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_allocations_profile.dot
ruby_prof_graph_allocations_profile.html
ruby_prof.png
data.txt
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.5.3
9 changes: 9 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
source 'https://rubygems.org'
Copy link
Owner

Choose a reason for hiding this comment

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

👍 Плюс за Gemfile


gem 'rspec'
gem 'byebug'
gem 'gc_tracer'
gem 'memory_profiler'
gem 'stackprof'
gem 'ruby-prof'
gem 'get_process_mem'
Copy link
Owner

Choose a reason for hiding this comment

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

👍

38 changes: 38 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
GEM
remote: https://rubygems.org/
specs:
byebug (11.0.0)
diff-lcs (1.3)
gc_tracer (1.5.1)
get_process_mem (0.2.3)
memory_profiler (0.9.12)
rspec (3.8.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.8.0)
rspec-core (3.8.0)
rspec-support (~> 3.8.0)
rspec-expectations (3.8.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-mocks (3.8.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-support (3.8.0)
ruby-prof (0.17.0)
stackprof (0.2.12)

PLATFORMS
ruby

DEPENDENCIES
byebug
gc_tracer
get_process_mem
memory_profiler
rspec
ruby-prof
stackprof

BUNDLED WITH
2.0.1
12 changes: 12 additions & 0 deletions benchmark.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require 'benchmark'
require_relative 'task-1'

time = Benchmark.realtime do
puts "rss before parsing: #{"%d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)}"

parser = Parser.new()
parser.work('tmp/data_small.txt') # 1MB

puts "rss after parsing: #{"%d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)}"
end
puts "Finish in #{time.round(2)}"
111 changes: 97 additions & 14 deletions case-study-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,127 @@
## Актуальная проблема
В нашем проекте возникла серьёзная проблема.

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

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

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

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

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


Сначала я хотела протестировать програму с болсшим файлом, но после 10 минут ожидания
ее окончания, а не смогла дождаться и решила делать оптимизацию с файлом меньшего размера.

Чтобы найти более-менее нормалное колицчество строк, обработка которич не занимает сильно много времени,
Copy link
Owner

Choose a reason for hiding this comment

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

Много опечаток, писали с телефона?

была использована метрика с Benchmark.realtime
Я сократила файл до 98_000 строк, но ето не сильно помогло мне, так как обработка файла все равно прочодила
цлишком долго для меня.
В итоге, я сократила количество строк до 30_450, что было эквивалентно 1.1 МБ. - здесь
время обработки файла заняло 50.04 секунды. Эта метрика стала исчодной и основной тоцчкой для меня.

Для начала, я решила использовать все метрики из лекции, чтобы практиковать их применение.
Copy link
Owner

Choose a reason for hiding this comment

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

👍 👍

В процессе оптимизации я формировала метрики с помощью:
1. Memory Gem set
2. MemoryProfiler
3.Ruby Prof
4. StackProf

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

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

Вот как я построил `feedback_loop`: *как вы построили feedback_loop*
Вот как я построила `feedback_loop`: *как вы построили feedback_loop*
1. Сделать изменение в коде
2. Проверить проходит ли данное изменение в коде
3. Если тест проходит, проверить улуцчшилисж ли показатели по метрикам
4. Если тест не прочодит, цм пункт 1.
5. Если показатели по метрикам приемлемы, push to GitHub

## Вникаем в детали системы, чтобы найти 20% точек роста
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались*
Для того, чтобы найти "точки роста" для оптимизации я воспользовался:
1. Benchmark.realtime
2. MemoryProfiler
3. Ruby Prof


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

### Ваша находка №1
О вашей находке №1
Трудно удержаться от рефакторинга кода сразу
Copy link
Owner

Choose a reason for hiding this comment

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

👍 👍


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

### Ваша находка №X
О вашей находке №X
Несколько проблемных областей были найдены для пользователей `users.each` и `sessions.each`
Copy link
Owner

Choose a reason for hiding this comment

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

Хорошо было бы указать как именно были найдены

Как показала практика, лучше избегать использования знака конкатенации в `users_objects = users_objects + [user_object]` и использовать вместо него `<<`. Таким образом, мы можем избежать создания дополнительных объектов в памяти. Я также старалась избегать присвоения значений дополнительным переменным, когда это было возможно, чтобы чтение программы все еще имело смысл.

Cначала я решил разобраться с кодом для итерации пользователей в строке 128.
Я также изменилa код для итераций сессий с`uniqueBrowsers += [browser]` на `uniqueBrowsers << session['browser']`. Таким образом, мы могли бы уменьшить количество дополнительных объектов, созданных ранее.
После изменения кода для использования `<<` и удаления дополнительных переменных мне удалось сократить время обработки файла размером 1 МБ с 50 до 38,44 с.
### Ваша находка 3
Я также заметила с помощью MemoryProfiler, что в этой строке кода было создано много объектов.

```
report['allBrowsers'] =
sessions
.map { |s| s['browser'] }
.map { |b| b.upcase }
.sort
.uniq
.join(',')
```
Многие изменения должны быть сделаны непосредственно во время чтения файла, особенно создание массива сессий и уникальных браузеров. Это позволило бы нам уменьшить использование `map`, который создает новые объекты массива. Я также использовала `upcase!` Вместо `upcase` для изменения значения на прямую. После рефакторинга кода `report ['allBrowser'] выглядел так:

```
report['allBrowsers'] =
unique_browsers
.sort
.join(',')
```

Изменение сессий на хеш, где key - это id пользователя, а value - массив сессий для этого пользователя, который помог избавитьця от метода «select», который неожиданно использовал кучу ресурсов памяти.
Теперь вместо `user_sessions = sessions.select {| session | session ['user_id'] == user ['id']} `мы только что использовали` user_sessions = session [user ['id']] `

После этой оптимизации программа для разбора файла 1MB запустилась за 0,68 секунды.

### Ваша находка 4
`sort_by!` и `reverse!`, работали лучше, чем `sort` и` reverse` при создании `session dates` После этого оптимизация программы была завершена за 0,48 секунды.

### Ваша находка 5
Нет большой разницы в интерполяции строк между:
`user.sessions.map {| s | s [: time] .to_i} .sum.to_s << 'min.'`
а также
`" # {session_time.sum} min. "`
Также хорошо уменьшить дублирование кода, которое использует `map`

### Ваша находка 6
Лучше использовать символы для ключей хеша вместо строк, потому что ключи-строки будут выделять отдельные объекты при каждом их использовании.

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

rss before iteration: 75 MB
rss after iteration: 99 MB
```
до
```

Finish in 0.38

*Какими ещё результами можете поделиться*
rss before iteration: 13 MB
rss after iteration: 13 MB
```

## Защита от регресса производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали*
We should check tha correctness of tests
We should check that our new metrics do not become worse
130 changes: 130 additions & 0 deletions case-study-template_english.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
Show less
5000/5000
Character limit: 5000
# Case-study optimization
Copy link
Owner

Choose a reason for hiding this comment

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

плюс за english


## Actual problem
Our project has a serious problem.

It was necessary to process the data file, a little more than one hundred twenty megabytes (3 million lines).

We already had a program on `ruby` that knew how to do the necessary processing.

It worked successfully on files with a size of a couple of megabytes , but for a large file it worked too long, and it wasn’t clear if it would finish the job at all in some reasonable time.

I decided to fix this problem by optimizing this program.

## Metric formation


At first I wanted to test the program with a large file, but after 10 minutes of waiting for it to end and it didn't look like it was going to finish any time soon and decided to do the optimization with a smaller file.

In order to find a more or less normal number of lines, where file processing is not very time consuming I used benchmark.realtime.
I reduced the file to 98_000 lines, but this didn’t help me much, since the file processing was still too long for me.
As a result, I reduced the number of lines to 30_450, which was equivalent to 1.1 MB.
After that file processing time was 50.04 seconds. This value became the start metric for me.

At first, I decided to check all the metrics from the lecture in order to have some practice using them.
In the process of optimization, I formed metrics using:
1. Memory Gem set
2. MemoryProfiler
3. Ruby Prof
4. StackProf



## Guaranteed correct operation of an optimized program
The program was delivered with the test.
Running this test will prevent changes to the program logic during optimization.
The test was later rewritten in RSpec format.

## Feedback-Loop
In order to be able to quickly test hypotheses, I built an effective feedback loop,
which allowed me to get feedback on the effectiveness of the changes made.

Here's how I built feedback_loop: * how you built feedback_loop *
1. Make a code change
2. Check if the change passes in the code
3. If the test passes, check if the metrics have metrics
4. If the test does not read, see item 1.
5. If metrics are acceptable, push to GitHub

## We delve into the details of the system to find 20% of growth points
In order to find "growth points" for optimization, I used the following tools:
1. Benchmark.realtime
2. MemoryProfiler
3. Ruby Prof

Here are the problems that were found and solved.

### Your Find # 1
About your find # 1
It's hard to refrain from refactoring code right away.

### Your Find # 2
Several problematic areas were found for users `users.each` and sessions ` sessions.each`
As practice has shown, it is better to avoid using the concatenation character in `users_objects = users_objects + [user_object]` and use `<<` instead. Thus, we can avoid creating additional objects in memory. I also tried to avoid assigning values ​​to additional variables when it was possible, so that reading the program still made sense.

At first I decided to deal with the code for iterating users in line 128.
I also changed the code for session iterations from `UniqueBrowsers + = [browser]` to `uniqueBrowsers << session ['browser']`. Thus, we could reduce the number of additional objects created earlier.
After changing the code to use `<<` and removing additional variables, I was able to reduce the processing time of a 1 MB file from 50 to 38.44 seconds.
### Your Find 3
I also noticed using MemoryProfiler that many objects were created in this line of code.

`` `
report ['allBrowsers'] =
sessions
.map {| s | s ['browser']}
.map {| b | b.upcase}
.sort
.uniq
.join (',')
`` `
Many changes must be made directly while reading the file, especially the creation of an array of sessions and unique browsers. This would allow us to reduce the use of `map`, which creates new array objects. I also used `upcase!` Instead of `upcase` to change the value to a straight line. After refactoring the code, `report ['allBrowser'] looked like this:

`` `
report ['allBrowsers'] =
unique_browsers
.sort
.join (',')
`` `

Changing sessions to a hash, where key is the user id, and value is an array of sessions for this user who helped to get rid of the “select” method, which unexpectedly used a bunch of memory resources.
Now instead of `user_sessions = sessions.select {| session | session ['user_id'] == user ['id']} `we just used` user_sessions = session [user ['id']] `

After this optimization, the program for parsing the 1MB file started in 0.68 seconds.

### Your Find 4
`sort_by!` and `reverse!`, worked better than `sort` and` reverse` when creating `session dates` After that, the optimization of the program was completed in 0.48 seconds.

### Your Find 5
There is not much difference in the interpolation of lines between:
`user.sessions.map {| s | s [: time] .to_i} .sum.to_s << 'min.'`
and
`" # {session_time.sum} min. "`
It is also good to reduce duplication of code that uses `map`

### Your Find 6
It is better to use characters for hash keys instead of strings, because string keys will create individual objects each time they are used.

## Results
As a result of this optimization, we finally managed to process the data file.
It was possible to improve the system metric with
`` `
Finish in 50.6

rss before iteration: 75 MB
rss after iteration: 99 MB
`` `
  before
`` `

Finish in 0.38

rss before iteration: 13 MB
rss after iteration: 13 MB
`` `

## Protection against performance regress
We should check tha correctness of tests
We should not be any worse
8 changes: 8 additions & 0 deletions memory_prof.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'memory_profiler'
require_relative 'task-1'

parser = Parser.new()
report = MemoryProfiler.report do
parser.work('tmp/data_small.txt') # 1MB
end
report.pretty_print(scale_bytes: true)
1 change: 1 addition & 0 deletions result.json

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions ruby_prof.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require 'ruby-prof'
require_relative 'task-1'

parser = Parser.new()
result = RubyProf.profile do
parser.work('tmp/data_small.txt') # 1MB
end

printer = RubyProf::FlatPrinter.new(result)
printer.print(File.open("ruby_prof_flat_allocations_profile.txt", "w+"))

# run separately
printer = RubyProf::DotPrinter.new(result)
printer.print(File.open("ruby_prof_allocations_profile.dot", "w+"))

printer = RubyProf::GraphHtmlPrinter.new(result)
printer.print(File.open("ruby_prof_graph_allocations_profile.html", "w+"))

# ruby ruby_prof.rb
# dot -Tpng ruby_prof_allocations_profile.dot > ruby_prof.png
# brew install imgcat
# imgcat ruby_prof.png

# run separately
# qcachegrind tmp/profile.callgrind.out.92522
OUTPUT_DIR = 'tmp/'
printer = RubyProf::CallTreePrinter.new(result)
printer.print(path: OUTPUT_DIR, profile: 'profile')
Loading