-
Notifications
You must be signed in to change notification settings - Fork 139
[zero] create core classes and initial measure #128
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
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 @@ | ||
| --require spec_helper |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| source "https://rubygems.org" | ||
|
|
||
|
|
||
| gem 'stackprof' | ||
| gem 'ruby-prof' | ||
| gem 'memory_profiler' | ||
| gem 'rspec' | ||
| gem 'byebug' | ||
| gem 'rspec-benchmark' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| GEM | ||
| remote: https://rubygems.org/ | ||
| specs: | ||
| benchmark-malloc (0.2.0) | ||
| benchmark-perf (0.6.0) | ||
| benchmark-trend (0.4.0) | ||
| byebug (11.1.3) | ||
| diff-lcs (1.6.0) | ||
| memory_profiler (1.1.0) | ||
| rspec (3.13.0) | ||
| rspec-core (~> 3.13.0) | ||
| rspec-expectations (~> 3.13.0) | ||
| rspec-mocks (~> 3.13.0) | ||
| rspec-benchmark (0.6.0) | ||
| benchmark-malloc (~> 0.2) | ||
| benchmark-perf (~> 0.6) | ||
| benchmark-trend (~> 0.4) | ||
| rspec (>= 3.0) | ||
| rspec-core (3.13.3) | ||
| rspec-support (~> 3.13.0) | ||
| rspec-expectations (3.13.3) | ||
| diff-lcs (>= 1.2.0, < 2.0) | ||
| rspec-support (~> 3.13.0) | ||
| rspec-mocks (3.13.2) | ||
| diff-lcs (>= 1.2.0, < 2.0) | ||
| rspec-support (~> 3.13.0) | ||
| rspec-support (3.13.2) | ||
| ruby-prof (1.7.1) | ||
| stackprof (0.2.27) | ||
|
|
||
| PLATFORMS | ||
| ruby | ||
| x86_64-linux | ||
|
|
||
| DEPENDENCIES | ||
| byebug | ||
| memory_profiler | ||
| rspec | ||
| rspec-benchmark | ||
| ruby-prof | ||
| stackprof | ||
|
|
||
| BUNDLED WITH | ||
| 2.5.18 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,44 +12,38 @@ | |
| Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
|
||
| ## Формирование метрики | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: количество потребляемой памяти процессом | ||
|
|
||
| ## Гарантия корректности работы оптимизированной программы | ||
| Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
|
||
| ## Feedback-Loop | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за несколько секунд (запуск measurer -> запуск profiler -> анализ) | ||
|
|
||
| Вот как я построил `feedback_loop`: *как вы построили feedback_loop* | ||
| Вот как я построил `feedback_loop`: | ||
| - Вынес вычисление метрики в Measurer, в котором запускаются 2 потока - один выполняет бизнес логику, а второй выступает предохранителем при использовании процессом памяти - больше заданного лимита | ||
|
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. лайк за предохранитель |
||
| - Вынес профилирование в Profiler | ||
|
|
||
| ## Вникаем в детали системы, чтобы найти главные точки роста | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался memory_profiler, ruby-prof, stackprof | ||
|
|
||
| Вот какие проблемы удалось найти и решить | ||
|
|
||
| ### Ваша находка №1 | ||
| - какой отчёт показал главную точку роста | ||
| - как вы решили её оптимизировать | ||
| - как изменилась метрика | ||
| - как изменился отчёт профилировщика | ||
| - Одну из главных точек роста я выявил в отчёте memory_profile - модификация массива не in_place | ||
| - Убрал лишние аллокации | ||
| - Метрика изменилась, но не сильно с 80 МБ до 57 для 15_000 строк | ||
| - Количество общих аллокаций по Array стало меньше, но не сильно | ||
|
|
||
| ### Ваша находка №2 | ||
| - какой отчёт показал главную точку роста | ||
| - как вы решили её оптимизировать | ||
| - как изменилась метрика | ||
| - как изменился отчёт профилировщика | ||
|
|
||
| ### Ваша находка №X | ||
| - какой отчёт показал главную точку роста | ||
| - как вы решили её оптимизировать | ||
| - как изменилась метрика | ||
| - как изменился отчёт профилировщика | ||
| - Понял (по подсказке + отчёту memory_profiler), что для значительного буста по памяти необходимо составлять отчёт на лету, без хранения сессий и пользователей, то есть в каждый момент времени будет обрабатываться только один пользователь. | ||
| - Добавил составление отчёта на лету при чтении каждой строки | ||
| - Метрика изменилась с 57 для 15_000 МБ до 38 для полного объёма | ||
| - Значительно уменьшилось количество общий аллокаций по Array и другим классам | ||
|
|
||
| ## Результаты | ||
| В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
| Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. | ||
|
|
||
| *Какими ещё результами можете поделиться* | ||
| Удалось улучшить метрику системы с 15_000 lines: 57 MB, до полной обработки: 38 MB и уложиться в заданный бюджет. | ||
|
|
||
| ## Защита от регрессии производительности | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы были добавлены тесты, проверяющие корректность бизнес-логики и укладывание в заданные лимиты | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| class Main | ||
| PARTIAL_VOLUME_FILE_NAME = "dataN.txt" | ||
|
|
||
| def initialize(options: {}) | ||
| @source_file_name = options[:source_file_name] || 'data_large.txt' | ||
| @count_lines = options[:count_lines] | ||
| @current_user = nil | ||
| @current_stat = nil | ||
| end | ||
|
|
||
| def call = work | ||
|
|
||
| private | ||
|
|
||
| attr_reader :source_file_name, :count_lines | ||
|
|
||
| def work | ||
| `head -n #{count_lines} #{source_file_name} > #{PARTIAL_VOLUME_FILE_NAME}` if count_lines | ||
|
|
||
| report = {} | ||
| sessions_count = 0 | ||
| users_count = 0 | ||
| uniq_browsers = Set.new | ||
|
|
||
| File.open('result.json', 'w') do |result_file| | ||
| result_file.write('{"usersStats":{') | ||
|
|
||
| File.foreach(count_lines ? PARTIAL_VOLUME_FILE_NAME : source_file_name).each do |line| | ||
| fields = line.split(',') | ||
| case fields[0] | ||
| when 'session' | ||
| session = parse_session(fields) | ||
| sessions_count += 1 | ||
| uniq_browsers.add(session['browser']) | ||
| @current_stat = build_current_stat(@current_user, session) | ||
| when 'user' | ||
| write_user_stat(result_file) | ||
| @current_user = parse_user(fields) | ||
| @current_stat = build_initial_stat | ||
| users_count += 1 | ||
| end | ||
| end | ||
|
|
||
| write_user_stat(result_file, with_delimeter: false) | ||
|
|
||
| result_file.write("},\"totalUsers\":#{users_count},\"uniqueBrowsersCount\":#{uniq_browsers.count},\"totalSessions\":#{sessions_count},\"allBrowsers\":\"#{uniq_browsers.sort.join(',')}\"}\n") | ||
| end | ||
| end | ||
|
|
||
| def write_user_stat(file, with_delimeter: true) | ||
| return unless @current_user | ||
|
|
||
| file.write("\"#{@current_user['key']}\":#{build_final_stat.to_json}") | ||
| file.write(",") if with_delimeter | ||
|
|
||
| @current_user = nil | ||
| end | ||
|
|
||
| def build_final_stat | ||
| { | ||
| 'sessionsCount' => @current_stat['sessionsCount'], | ||
| 'totalTime' => "#{@current_stat['totalTime']} min.", | ||
| 'longestSession' => "#{@current_stat['longestSession']} min.", | ||
| 'browsers' => @current_stat['browsers'].sort.join(', '), | ||
| 'usedIE' => @current_stat['usedIE'], | ||
| 'alwaysUsedChrome' => @current_stat['alwaysUsedChrome'], | ||
| 'dates' => @current_stat['dates'].sort.reverse | ||
| } | ||
| end | ||
|
|
||
| def build_current_stat(user, session) | ||
| { | ||
| 'sessionsCount' => @current_stat['sessionsCount'] + 1, | ||
| 'totalTime' => @current_stat['totalTime'] + session['time'].to_i, | ||
| 'longestSession' => @current_stat['longestSession'] > session['time'] ? @current_stat['longestSession'] : session['time'], | ||
| 'browsers' => @current_stat['browsers'] << session['browser'], | ||
| 'usedIE' => @current_stat['usedIE'] ? true : !!(session['browser'] =~ /INTERNET EXPLORER/), | ||
| 'alwaysUsedChrome' => @current_stat['alwaysUsedChrome'] ? !!(session['browser'].upcase =~ /CHROME/) : false, | ||
| 'dates' => @current_stat['dates'] << Date.strptime(session['date'], '%Y-%m-%d').iso8601 | ||
|
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. даты сразу ок |
||
| } | ||
| end | ||
|
|
||
| def build_initial_stat | ||
| { | ||
| 'sessionsCount' => 0, | ||
| 'totalTime' => 0, | ||
| 'longestSession' => 0, | ||
| 'browsers' => [], | ||
| 'usedIE' => false, | ||
| 'alwaysUsedChrome' => true, | ||
| 'dates' => [] | ||
| } | ||
| end | ||
|
|
||
| def parse_user(fields) | ||
| { | ||
| 'id' => fields[1], | ||
| 'first_name' => fields[2], | ||
| 'last_name' => fields[3], | ||
| 'age' => fields[4], | ||
| 'key' => "#{fields[2]} #{fields[3]}" | ||
| } | ||
| end | ||
|
|
||
| def parse_session(fields) | ||
| { | ||
| 'user_id' => fields[1], | ||
| 'session_id' => fields[2], | ||
| 'browser' => fields[3].upcase, | ||
| 'time' => fields[4].to_i, | ||
| 'date' => fields[5], | ||
| } | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| require_relative 'main' | ||
|
|
||
| class Measurer | ||
| MEMORY_USAGE_LIMIT_MB = 70 | ||
| CHECK_USAGE_INTERVAL_SEC = 1 | ||
|
|
||
| def initialize(count_lines: nil) | ||
| @count_lines = count_lines | ||
| end | ||
|
|
||
| def call = [memory_measurer_thread, work_thread].map(&:join) | ||
|
|
||
| private | ||
|
|
||
| attr_reader :count_lines | ||
|
|
||
| def work_thread = @work_thread ||= Thread.new { Main.new(options: { count_lines: }).call } | ||
|
|
||
| def memory_measurer_thread = Thread.new do | ||
| while work_thread.alive? | ||
| sleep CHECK_USAGE_INTERVAL_SEC | ||
| current_memory_usage_mb = `ps -o rss= -p #{Process.pid}`.to_i / 1024 | ||
| if current_memory_usage_mb > MEMORY_USAGE_LIMIT_MB | ||
| puts "MEMORY USAGE: #{current_memory_usage_mb} MB, BUT LIMIT: #{MEMORY_USAGE_LIMIT_MB}. TERMINATING." | ||
|
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. 👍 |
||
| exit | ||
| else | ||
| puts "MEMORY USAGE: #{current_memory_usage_mb} MB" | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| # Zero iteration: | ||
| # 15_000 lines: 57 MB (optimizing users and sessions collecting) | ||
| # First iteration: | ||
| # full: 38 MB (10 sec) (optimizing file stream processing) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| require 'memory_profiler' | ||
| require 'stackprof' | ||
| require 'ruby-prof' | ||
|
|
||
| require_relative 'main' | ||
|
|
||
| class Profiler | ||
| def initialize(count_lines:) | ||
| @count_lines = count_lines | ||
| end | ||
|
|
||
| def call | ||
| profile_by_memory_profiler | ||
| profile_memory_by_ruby_prof | ||
| profile_allocations_by_ruby_prof | ||
| profile_allocations_by_stack_prof | ||
| end | ||
|
|
||
| private | ||
|
|
||
| attr_reader :count_lines | ||
|
|
||
| def profile_by_memory_profiler | ||
| report = MemoryProfiler.report { action } | ||
| report.pretty_print(to_file: './profiles/memory_profiler') | ||
| end | ||
|
|
||
| def profile_memory_by_ruby_prof | ||
| RubyProf.measure_mode = RubyProf::MEMORY | ||
| result = RubyProf.profile { action } | ||
| printer = RubyProf::CallTreePrinter.new(result) | ||
| printer.print(:path => ".", :profile => "profiles/ruby_prof_memory_profile") | ||
| end | ||
|
|
||
| def profile_allocations_by_ruby_prof | ||
| RubyProf.measure_mode = RubyProf::ALLOCATIONS | ||
| result = RubyProf.profile { action } | ||
| printer = RubyProf::CallTreePrinter.new(result) | ||
| printer.print(:path => ".", :profile => "profiles/ruby_prof_allocations_profile") | ||
| end | ||
|
|
||
| def profile_allocations_by_stack_prof | ||
| StackProf.run(mode: :object, out: 'profiles/stackprof.dump') do | ||
| action | ||
| end | ||
| end | ||
|
|
||
| def action = Main.new(options: { count_lines: }).call | ||
| end |
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.
тут есть нюанс, что это кол-во меняется со временем (можно указать момент в который идёт замер чтоб было чётко, и ещё в идеале и способ замера чтоб было совсем чётко)