-
Notifications
You must be signed in to change notification settings - Fork 136
Task 2 - Romanenko Dmitriy #87
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
ecf0c6a
06ecf9a
25919d0
15c8564
0d5c2fd
5601b2f
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,16 @@ | ||
| FROM ruby:2.6-stretch | ||
|
|
||
| RUN apt-get update && apt-get install g++ valgrind -y \ | ||
| massif-visualizer \ | ||
| --no-install-recommends \ | ||
| && apt-get purge --auto-remove -y curl \ | ||
| && rm -rf /var/lib/apt/lists/* \ | ||
| && rm -rf /src/*.deb | ||
|
|
||
| RUN groupadd -r massif && useradd -r -g massif massif \ | ||
| && mkdir -p /home/massif/test && chown -R massif:massif /home/massif | ||
| USER massif | ||
| WORKDIR /home/massif/test | ||
|
|
||
| COPY Gemfile /home/massif/test | ||
| RUN bundle install |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| source 'https://rubygems.org' | ||
| gem 'oj' | ||
| gem 'progress_bar' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| GEM | ||
| remote: https://rubygems.org/ | ||
| specs: | ||
| highline (2.0.3) | ||
| oj (3.13.13) | ||
| options (2.3.2) | ||
| progress_bar (1.3.3) | ||
| highline (>= 1.6, < 3) | ||
| options (~> 2.3.0) | ||
|
|
||
| PLATFORMS | ||
| ruby | ||
|
|
||
| DEPENDENCIES | ||
| oj | ||
| progress_bar | ||
|
|
||
| BUNDLED WITH | ||
| 1.17.2 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| #!/bin/bash | ||
|
|
||
| docker build . -t spajic/docker-valgrind-massif |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,44 +12,81 @@ | |
| Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
|
||
| ## Формирование метрики | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: какое количество памяти использует программа для файла размером 20000 строк. Начальное значение - около 82 МБ. | ||
|
|
||
| ## Гарантия корректности работы оптимизированной программы | ||
| Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
|
||
| ## Feedback-Loop | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за ~30 секунд | ||
|
|
||
| Вот как я построил `feedback_loop`: *как вы построили feedback_loop* | ||
| Вот как я построил `feedback_loop`: | ||
| 1. Изучал отчеты профилировщиков | ||
| 2. Вносил изменения в программу | ||
| 3. Запускал тесты для проверки корректности работы программы | ||
| 4. Запускал тесты производительности | ||
|
|
||
| ## Вникаем в детали системы, чтобы найти главные точки роста | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался: | ||
| 1. Гемом memory_profiler; | ||
| 2. Stackprof; | ||
| 3. Ruby-prof в режиме graph; | ||
| 4. Ruby-prof в режиме callstack; | ||
| 5. Ruby-prof в режиме flat; | ||
| 6. Ruby-prof с визуализацией в QCacheGrind. | ||
|
|
||
| Вот какие проблемы удалось найти и решить | ||
|
|
||
| ### Ваша находка №1 | ||
| - какой отчёт показал главную точку роста | ||
| - как вы решили её оптимизировать | ||
| - как изменилась метрика | ||
| - как изменился отчёт профилировщика | ||
|
|
||
| ### Ваша находка №2 | ||
| - какой отчёт показал главную точку роста | ||
| - как вы решили её оптимизировать | ||
| - как изменилась метрика | ||
| - как изменился отчёт профилировщика | ||
|
|
||
| ### Ваша находка №X | ||
| - какой отчёт показал главную точку роста | ||
| - как вы решили её оптимизировать | ||
| - как изменилась метрика | ||
| - как изменился отчёт профилировщика | ||
| ### Выделение 15мб памяти для парсинга даты | ||
| - какой отчёт показал главную точку роста: гем memory_profiler | ||
| - как вы решили её оптимизировать: убрал лишние вызовы метода map, а также переводы в различные форматы | ||
| - как изменилась метрика: выделение памяти снизилось до 24.6 Мб | ||
| - как изменился отчёт профилировщика: проблема перестала быть главной точкой роста | ||
|
|
||
| ### Большое количество аллокаций для метода String#upcase | ||
| - какой отчёт показал главную точку роста: Stackprof | ||
| - как вы решили её оптимизировать: Всего было 4 вызова метода upcase, все они были использованы для браузеров сессий. Я оставил только 1 вызов метода upcase - при парсинге браузера сессий, тогда все остальные вызовы можно убрать | ||
| - как изменилась метрика: снизилась до 23 МБ | ||
| - как изменился отчёт профилировщика: проблема перестала быть главной точкой роста | ||
|
|
||
| ### Выделение большого количества памяти при сборе данных о пользователе | ||
| - какой отчёт показал главную точку роста: Ruby-prof в режиме graph | ||
| - как вы решили её оптимизировать: изначально блок кода выглядел так: | ||
| ``` | ||
| user_key = "#{@user['first_name']}" + ' ' + "#{@user['last_name']}" | ||
| user_data = { | ||
| 'sessionsCount' => @user_sessions.count, | ||
| 'totalTime' => @user_sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.', | ||
| 'longestSession' => @user_sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.', | ||
| 'browsers' => @user_sessions.map {|s| s['browser']}.sort.join(', '), | ||
| 'usedIE' => @user_sessions.map{|s| s['browser']}.any? { |b| b =~ /INTERNET EXPLORER/ }, | ||
| 'alwaysUsedChrome' => @user_sessions.map{|s| s['browser']}.all? { |b| b =~ /CHROME/ }, | ||
| 'dates' => @user_sessions.map{|s| s['date']}.sort.reverse | ||
| } | ||
| ``` | ||
| В нем я изменил конкатенацию строк, лишние приведения значений к типу int, использовал bang-методы, где это возможно. В результате данный блок кода стал выглядеть следующим образом: | ||
|
|
||
| ``` | ||
| user_key = "#{@user['first_name']} #{@user['last_name']}" | ||
| user_data = { | ||
| 'sessionsCount' => @user_sessions.count, | ||
| 'totalTime' => @user_sessions.map {|s| s['time']}.sum.to_s + ' min.', | ||
| 'longestSession' => @user_sessions.map {|s| s['time']}.max.to_s + ' min.', | ||
| 'browsers' => @user_sessions.map {|s| s['browser']}.sort!.join(', '), | ||
| 'usedIE' => @user_sessions.map{|s| s['browser']}.any? { |b| b =~ /INTERNET EXPLORER/ }, | ||
| 'alwaysUsedChrome' => @user_sessions.map{|s| s['browser']}.all? { |b| b =~ /CHROME/ }, | ||
| 'dates' => @user_sessions.map{|s| s['date']}.sort!.reverse! | ||
| } | ||
| ``` | ||
|
|
||
| - как изменилась метрика: снизилась до 18 МБ | ||
| - как изменился отчёт профилировщика: главная проблема не является главной точкой роста | ||
|
|
||
| ## Результаты | ||
| В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
| Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. | ||
| Удалось улучшить метрику системы с 80 МБ для файла размеров 20000 строк до 40 МБ на протяжении всей работы программы для основного файла и уложиться в заданный бюджет. | ||
|
|
||
| *Какими ещё результами можете поделиться* | ||
| Также хочу отметить, что программа сейчас работает быстрее, чем программа из первого домашнего задания. Удалось добиться результата в 27 секунд. | ||
|
Collaborator
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. 👍 |
||
|
|
||
| ## Защита от регрессии производительности | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы я написал performance-test, который проверяет, что программа потребляет менее 40 МБ памяти для файла data_large.txt | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| require 'oj' | ||
|
|
||
| class JsonWriter | ||
|
Collaborator
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. очень чистенько получилось |
||
| attr_reader :file_writer | ||
|
|
||
| def initialize(file_name) | ||
| @file_writer = Oj::StreamWriter.new(file_name) | ||
| end | ||
|
|
||
| def prepare_user_stats | ||
| @file_writer.push_object | ||
| @file_writer.push_key('usersStats') | ||
| @file_writer.push_object | ||
| end | ||
|
|
||
| def write_user_stats(user_key, user_data) | ||
| @file_writer.push_key(user_key) | ||
| @file_writer.push_value(user_data) | ||
| end | ||
|
|
||
| def write_common_stats(users_count, unique_browsers, sessions_count) | ||
| @file_writer.pop | ||
|
|
||
| @file_writer.push_key('totalUsers') | ||
| @file_writer.push_value(users_count) | ||
|
|
||
| @file_writer.push_key('uniqueBrowsersCount') | ||
| @file_writer.push_value(unique_browsers.count) | ||
|
|
||
| @file_writer.push_key('totalSessions') | ||
| @file_writer.push_value(sessions_count) | ||
|
|
||
| @file_writer.push_key('allBrowsers') | ||
| @file_writer.push_value(unique_browsers.sort.join(',')) | ||
|
|
||
| @file_writer.pop | ||
|
|
||
| @file_writer.flush | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| require_relative 'parser' | ||
| require 'benchmark' | ||
| require 'memory_profiler' | ||
|
|
||
| report = MemoryProfiler.report do | ||
| Parser.new.work(file_name: 'data20000.txt') | ||
| end | ||
|
|
||
| report.pretty_print(scale_bytes: true) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| require 'rspec-benchmark' | ||
| require 'rspec' | ||
| require 'bytesize' | ||
|
|
||
| require_relative 'parser' | ||
|
|
||
| RSpec.configure do |config| | ||
| config.include RSpec::Benchmark::Matchers | ||
| end | ||
|
|
||
| RSpec.describe 'parser' do | ||
| describe 'memory usage' do | ||
| let(:expected_memory_usage) { ByteSize.mb(40).to_bytes } | ||
|
|
||
| it 'uses less than expected memory' do | ||
| expect { | ||
| Parser.new.work(file_name: 'data_large.txt') | ||
| }.to perform_allocation(expected_memory_usage).bytes | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require_relative 'json_writer' | ||
|
|
||
| class Parser | ||
| attr_accessor :users_count, :sessions_count, :user, :user_sessions, :json_writer | ||
| attr_reader :unique_browsers | ||
|
|
||
| def initialize | ||
| @users_count = 0 | ||
| @unique_browsers = [] | ||
| @sessions_count = 0 | ||
| end | ||
|
|
||
| def work(file_name: "data.txt", disable_gc: false) | ||
| GC.disable if disable_gc | ||
| file_path = ENV['DATA_FILE'] || file_name | ||
|
|
||
| File.open('result.json', 'a') do |output_file| | ||
| @json_writer = JsonWriter.new(output_file) | ||
| @json_writer.prepare_user_stats | ||
|
|
||
| File.foreach(file_path, 'user') do |chunk| | ||
| chunk.split("\n") do |line| | ||
| cols = line.split(',') | ||
| case cols[0] | ||
| when 'user' | ||
| next | ||
| when '' | ||
| parse_user(cols) | ||
| when 'session' | ||
| parse_session(cols) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| collect_stats_for_previous_user | ||
|
|
||
| @json_writer.write_common_stats(@users_count, @unique_browsers, @sessions_count) | ||
|
|
||
| puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def collect_unique_browsers | ||
| @user_sessions.each do |session| | ||
| browser = session['browser'] | ||
| @unique_browsers.push(browser) unless @unique_browsers.include?(browser) | ||
|
Collaborator
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. Use Set instead |
||
| end | ||
| end | ||
|
|
||
| def parse_user(fields) | ||
| collect_stats_for_previous_user if @users_count > 0 | ||
| @users_count += 1 | ||
|
|
||
| @user_sessions = [] | ||
| @user = { | ||
| 'first_name' => fields[2], | ||
| 'last_name' => fields[3], | ||
| } | ||
| end | ||
|
|
||
| def parse_session(fields) | ||
| @sessions_count += 1 | ||
|
|
||
| @user_sessions.push( | ||
| { | ||
| 'browser' => fields[3].upcase!, | ||
| 'time' => fields[4].to_i, | ||
| 'date' => fields[5], | ||
| } | ||
| ) | ||
| end | ||
|
|
||
| def collect_stats_for_previous_user | ||
| collect_unique_browsers | ||
|
|
||
| user_key = "#{@user['first_name']} #{@user['last_name']}" | ||
| user_data = { | ||
| 'sessionsCount' => @user_sessions.count, | ||
| 'totalTime' => @user_sessions.map {|s| s['time']}.sum.to_s + ' min.', | ||
| 'longestSession' => @user_sessions.map {|s| s['time']}.max.to_s + ' min.', | ||
| 'browsers' => @user_sessions.map {|s| s['browser']}.sort!.join(', '), | ||
| 'usedIE' => @user_sessions.map{|s| s['browser']}.any? { |b| b =~ /INTERNET EXPLORER/ }, | ||
| 'alwaysUsedChrome' => @user_sessions.map{|s| s['browser']}.all? { |b| b =~ /CHROME/ }, | ||
| 'dates' => @user_sessions.map{|s| s['date']}.sort!.reverse! | ||
| } | ||
|
|
||
| @json_writer.write_user_stats(user_key, user_data) | ||
| end | ||
| end | ||
| 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_large.txt \ | ||
| spajic/docker-valgrind-massif \ | ||
| valgrind --tool=massif ruby work.rb |
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.
тут мы как-то пропустили шаг с переписыванием в потоковый вариант