-
Notifications
You must be signed in to change notification settings - Fork 24
Task 1, memory optimization in Ruby #14
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
base: master
Are you sure you want to change the base?
Changes from all commits
d8a4afd
2e3572e
0836a7f
932844b
0d4c184
b0894d6
fcd0b2f
ac92631
9d02a3b
15bad57
15e6389
d0b6a8c
ba521f7
4ff8624
70aeebc
ee50c21
810cf1e
ee45dc2
8a0bc74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| ruby_prof_allocations_profile.dot | ||
| ruby_prof_graph_allocations_profile.html | ||
| ruby_prof.png | ||
| data.txt | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| --require spec_helper |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 2.5.3 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| source 'https://rubygems.org' | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Плюс за |
||
|
|
||
| gem 'rspec' | ||
| gem 'byebug' | ||
| gem 'gc_tracer' | ||
| gem 'memory_profiler' | ||
| gem 'stackprof' | ||
| gem 'ruby-prof' | ||
| gem 'get_process_mem' | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| 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 |
| 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)}" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,44 +3,127 @@ | |
| ## Актуальная проблема | ||
| В нашем проекте возникла серьёзная проблема. | ||
|
|
||
| Необходимо было обработать файл с данными, чуть больше ста мегабайт. | ||
| Необходимо было обработать файл с данными, чуть больше ста двадцати мегабайт (3 млн строк). | ||
|
|
||
| У нас уже была программа на `ruby`, которая умела делать нужную обработку. | ||
|
|
||
| Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. | ||
|
|
||
| Я решил исправить эту проблему, оптимизировав эту программу. | ||
| Я решила исправить эту проблему, оптимизировав эту программу. | ||
|
|
||
| ## Формирование метрики | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* | ||
|
|
||
|
|
||
| Сначала я хотела протестировать програму с болсшим файлом, но после 10 минут ожидания | ||
| ее окончания, а не смогла дождаться и решила делать оптимизацию с файлом меньшего размера. | ||
|
|
||
| Чтобы найти более-менее нормалное колицчество строк, обработка которич не занимает сильно много времени, | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Много опечаток, писали с телефона? |
||
| была использована метрика с Benchmark.realtime | ||
| Я сократила файл до 98_000 строк, но ето не сильно помогло мне, так как обработка файла все равно прочодила | ||
| цлишком долго для меня. | ||
| В итоге, я сократила количество строк до 30_450, что было эквивалентно 1.1 МБ. - здесь | ||
| время обработки файла заняло 50.04 секунды. Эта метрика стала исчодной и основной тоцчкой для меня. | ||
|
|
||
| Для начала, я решила использовать все метрики из лекции, чтобы практиковать их применение. | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| Трудно удержаться от рефакторинга кода сразу | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 👍 |
||
|
|
||
| ### Ваша находка №2 | ||
| О вашей находке №2 | ||
|
|
||
| ### Ваша находка №X | ||
| О вашей находке №X | ||
| Несколько проблемных областей были найдены для пользователей `users.each` и `sessions.each` | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| Show less | ||
| 5000/5000 | ||
| Character limit: 5000 | ||
| # Case-study optimization | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. плюс за |
||
|
|
||
| ## 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 | ||
| 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) |
Large diffs are not rendered by default.
| 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') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍