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
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
11 changes: 11 additions & 0 deletions Gemfile
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'
44 changes: 44 additions & 0 deletions Gemfile.lock
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)"
- файл `case-study.md` с описанием проделанной оптимизации;

## Checklist
- [ ] Потренироваться с `memory_profiler`
- [ ] Потренироваться с `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`;
- [ ] Потренироваться с `stackprof` + `CLI` и `Speedscope`
- [ ] Потренироваться со вторым тредом для мониторинга памяти
- [x] Потренироваться с `memory_profiler`
- [x] Потренироваться с `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`;
- [x] Потренироваться с `stackprof` + `CLI` и `Speedscope`
- [x] Потренироваться со вторым тредом для мониторинга памяти

## Формат шагов case-study
Каждый шаг оптимизации в `case-study` должен содержать четыре составляющих:
Expand Down
38 changes: 16 additions & 22 deletions case-study-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,38 @@
Я решил исправить эту проблему, оптимизировав эту программу.

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

Choose a reason for hiding this comment

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

тут есть нюанс, что это кол-во меняется со временем (можно указать момент в который идёт замер чтоб было чётко, и ещё в идеале и способ замера чтоб было совсем чётко)


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

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

Вот как я построил `feedback_loop`: *как вы построили feedback_loop*
Вот как я построил `feedback_loop`:
- Вынес вычисление метрики в Measurer, в котором запускаются 2 потока - один выполняет бизнес логику, а второй выступает предохранителем при использовании процессом памяти - больше заданного лимита
Copy link
Collaborator

Choose a reason for hiding this comment

The 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-тестах, которые вы написали*
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы были добавлены тесты, проверяющие корректность бизнес-логики и укладывание в заданные лимиты
114 changes: 114 additions & 0 deletions main.rb
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
36 changes: 36 additions & 0 deletions measurer.rb
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."
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
49 changes: 49 additions & 0 deletions profiler.rb
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
Loading