Skip to content
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

[Zero] create core classes and initial measure #164

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
12 changes: 12 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

source "https://rubygems.org"

gem 'stackprof'
gem 'ruby-prof'
gem 'vernier', '~> 1.0'
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

gem 'rspec'
gem 'rspec-benchmark'
gem 'byebug'
gem 'oj'
gem 'progressbar'
52 changes: 52 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
GEM
remote: https://rubygems.org/
specs:
benchmark-malloc (0.2.0)
benchmark-perf (0.6.0)
benchmark-trend (0.4.0)
bigdecimal (3.1.9)
byebug (11.1.3)
diff-lcs (1.6.0)
oj (3.16.9)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
ostruct (0.6.1)
progressbar (1.13.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)
vernier (1.5.0)

PLATFORMS
ruby
x86_64-linux

DEPENDENCIES
byebug
oj
progressbar
rspec
rspec-benchmark
ruby-prof
stackprof
vernier (~> 1.0)

BUNDLED WITH
2.5.18
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,17 @@ head -n N data_large.txt > dataN.txt # create smaller file from larger (take N f
## Checklist
Советую использовать все рассмотренные в лекции инструменты хотя бы по разу - попрактикуйтесь с ними, научитесь с ними работать.

- [ ] Прикинуть зависимость времени работы програмы от размера обрабатываемого файла
- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`;
- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`;
- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`;
- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`;
- [ ] Построить дамп `stackprof` и проанализировать его с помощью `CLI`
- [ ] Построить дамп `stackprof` в `json` и проанализировать его с помощью `speedscope.app`
- [ ] Профилировать работающий процесс `rbspy`;
- [ ] Добавить в программу `ProgressBar`;
- [x] Прикинуть зависимость времени работы програмы от размера обрабатываемого файла
- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`;
- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`;
- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`;
- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`;
- [x] Построить дамп `stackprof` и проанализировать его с помощью `CLI`
- [x] Построить дамп `stackprof` в `json` и проанализировать его с помощью `speedscope.app`
- [x] Профилировать работающий процесс `rbspy`;
- [x] Добавить в программу `ProgressBar`;
- [ ] Постараться довести асимптотику до линейной и проверить это тестом;
- [ ] Написать простой тест на время работы: когда вы придёте к оптимизированному решению, замерьте, сколько оно будет работать на тестовом объёме данных; и напишите тест на то, что это время не превышается (чтобы не было ложных срабатываний, задайте время с небольшим запасом);
- [x] Написать простой тест на время работы: когда вы придёте к оптимизированному решению, замерьте, сколько оно будет работать на тестовом объёме данных; и напишите тест на то, что это время не превышается (чтобы не было ложных срабатываний, задайте время с небольшим запасом);

### Главное
Нужно потренироваться методично работать по схеме с фидбек-лупом:
Expand Down
46 changes: 26 additions & 20 deletions case-study-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,51 @@
Я решил исправить эту проблему, оптимизировав эту программу.

## Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика*
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: BigO (measurer.rb) при помощи Benchmark. На каждой итерации оценивал уменьшение времени работы программы.

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

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

Вот как я построил `feedback_loop`: *как вы построили feedback_loop*
Вот как я построил `feedback_loop`:
1. Вынес всю бизнес-логику в отдельный сервисный класс с опциональными аргументами, такими как count_lines (необходимое кол-во строк для обработки, которое берётся из исходного файла), with_progress_bar (добавляет progress bar для визуальной оценки быстроты выполнения тех или иных участков)
2. Добавил класс Measurer, который ответственен за вывод метрики
3. Добавил класс Profiler, который ответственен за профилирование и вывод результатов в разных видах
4. В каждой итерации я запускал Measurer - оценивал метрику -> запускал Profiler - изучал bottlenecks -> выполнял итерацию по оптимизации -> запускал Measurer и опять оценивал метрику. И так по кругу.
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍


## Вникаем в детали системы, чтобы найти главные точки роста
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались*
Для того, чтобы найти "точки роста" для оптимизации я воспользовался stackprof, QCachegrind, rbspy

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

### Ваша находка №1
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
- Главную точку роста я изначально выявил в flat отчёте ruby_prof - формирование списка уникальных браузеров
Copy link
Collaborator

Choose a reason for hiding this comment

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

там главное изменение - в группировке сессий по юзерам в hash user_sessions, а не формирование уникальных браузеров

- Решил воспользоваться связкой map и to_set
- Метрика изменилась с 31.43 для 60_000 строк до обработки полного файла - 36.7
- исправленная проблема перестала быть главной точкой роста, общее время выполнения уменьшилось

### Ваша находка №2
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
- Одну из главных точек роста я также изначально выявил в flat отчёте ruby_prof - парсинг даты
- Решил воспользоваться strptime, чтобы не тратилось время на дополнительный парсинг
Copy link
Collaborator

Choose a reason for hiding this comment

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

с датой можно ничего не делать, она сразу в нужном формате

- Метрика изменилась с 36.7 для полной обработки файла до 24.58
- исправленная проблема перестала быть одной из главных точек роста, общее время выполнения уменьшилось (в лимит уложились)

### Ваша находка №X
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
### Ваша находка №3
- Изначально двумя главными точками роста - была обработка файла (чтение -> разбор строки -> формирование и сохранение необходимых структур данных) и формирование отчёта (вложенные циклы each -> map + sort).
- Решил перенести основной объём формирования отчёта на этап обработки файла. На этапе формирования отчёта мы лишь брали необходимые данных из хэша отчётов по каждому пользователю.
- Метрика не сильно изменилась с 24.58 для полной обработки файла до 19.65. Если бы не было сортировки дат, то время выполнения снизилось бы в среднем до 14 секунд.
- как были 2 точки роста, так и остались

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

*Какими ещё результами можете поделиться*
- Попробовал rbspy
- Попробовал работу с progress bars
- Попробовал формировать и исследовать различные отчёты профайлеров

## Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали*
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы был добавлен тест, проверяющий, что время выполнения программы укладывается в заданные ограничения

99 changes: 99 additions & 0 deletions main.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
require 'progressbar'

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]
@with_gc = options[:with_gc] || true
@process_file_progress_bar = options[:with_progress_bar] ? ProgressBar.create(title: "Process file") : nil
@collect_report_progress_bar = options[:with_progress_bar] ? ProgressBar.create(title: "Collect report") : nil
@write_to_result_file_progress_bar = options[:with_progress_bar] ? ProgressBar.create(title: "Write to result file") : nil
end

def call
GC.disable unless with_gc
work
GC.start
end

private

attr_reader :source_file_name, :count_lines, :with_gc, :process_file_progress_bar, :collect_report_progress_bar, :write_to_result_file_progress_bar

def work
`head -n #{count_lines} #{source_file_name} > #{PARTIAL_VOLUME_FILE_NAME}` if count_lines

user_id_users = {}
user_id_stats = {}
total_sessions = 0
uniq_browsers = Set.new

with_progress_bar(process_file_progress_bar) do
File.foreach(count_lines ? PARTIAL_VOLUME_FILE_NAME : source_file_name).each do |line|
fields = line.split(',')
case fields[0]
when 'user'
user = { 'id' => fields[1], 'first_name' => fields[2], 'last_name' => fields[3], 'age' => fields[4] }
user_id_users[user['id']] = user
when 'session'
session = { 'user_id' => fields[1], 'session_id' => fields[2], 'browser' => fields[3], 'time' => fields[4], 'date' => fields[5] }
user_stat = user_id_stats[session['user_id']]
sessions_count = user_stat&.dig('sessions_count') || 0
total_time = user_stat&.dig('total_time') || 0
longest_session = user_stat&.dig('longest_session') || 0
browsers = user_stat&.dig('browsers') || []
dates = user_stat&.dig('dates') || []
is_used_ie = user_stat&.dig('used_ie') || false
is_always_used_chrome = user_stat&.dig('is_used_chrome') || false

user_id_stats[session['user_id']] = (user_id_stats[session['user_id']] || {})
user_id_stats[session['user_id']] = {
'sessions_count' => sessions_count + 1,
'total_time' => total_time + session['time'].to_i,
'longest_session' => session['time'].to_i > longest_session ? session['time'].to_i : longest_session,
'browsers' => browsers << session['browser'].upcase,
'used_ie' => is_used_ie ? true : !!(session['browser'].upcase =~ /INTERNET EXPLORER/),
'always_used_chrome' => is_always_used_chrome ? !!(session['browser'].upcase =~ /CHROME/) : false,
'dates' => dates << Date.strptime(session['date'], '%Y-%m-%d').iso8601
}
total_sessions += 1
uniq_browsers.add(session['browser'].upcase)
end
end
end

report = {}

with_progress_bar(collect_report_progress_bar) do
report['totalUsers'] = user_id_users.keys.count
report['uniqueBrowsersCount'] = uniq_browsers.count
report['totalSessions'] = total_sessions
report['allBrowsers'] = uniq_browsers.sort.join(',')
report['usersStats'] = {}

user_id_stats.each do |user_id, stat|
user = user_id_users[user_id]
user_key = "#{user['first_name']} #{user['last_name']}"
report['usersStats'][user_key] = {
'sessionsCount' => stat['sessions_count'],
'totalTime' => "#{stat['total_time']} min.",
'longestSession' => "#{stat['longest_session']} min.",
'browsers' => stat['browsers'].sort.join(', '),
'usedIE' => stat['used_ie'],
'alwaysUsedChrome' => stat['always_used_chrome'],
'dates' => stat['dates'].sort.reverse
}
end
end

with_progress_bar(write_to_result_file_progress_bar) { File.write('result.json', "#{report.to_json}\n") }
end

def with_progress_bar(bar)
bar&.progress = 0
yield
bar&.progress = 100
Copy link
Collaborator

Choose a reason for hiding this comment

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

не понял, какой смысл в таком прогресс-баре, который с нуля прыгает сразу на 100
в таком случае наверно проще puts сделать один или два раза

end
end
16 changes: 16 additions & 0 deletions measurer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require_relative 'main'

class Measurer
def call
Benchmark.bm do |x|
x.report("input: 120_000") { Main.new(options: { count_lines: 120_000 }).call }
x.report("input: 500_000") { Main.new(options: { count_lines: 500_000 }).call }
x.report("input: 1_000_000") { Main.new(options: { count_lines: 1_000_000 }).call }
x.report("input: 2_000_000") { Main.new(options: { count_lines: 2_000_000 }).call }
x.report("input: full") { Main.new.call }
end
end
end

# Zero iteration:
# full - 19.65
47 changes: 47 additions & 0 deletions profiler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require_relative 'main'
require 'stackprof'
require 'ruby-prof'
require 'vernier'
require 'oj'

class Profiler
def initialize(count_lines:)
@count_lines = count_lines
end

def call
profile_by_stack_prof
profile_by_stack_prof_raw
profile_by_ruby_prof
profile_by_vernier
end

private

attr_reader :count_lines

def profile_by_stack_prof = StackProf.run(mode: :wall, out: 'profiles/stackprof.dump') do
action
end

def profile_by_stack_prof_raw
result = StackProf.run(mode: :wall, raw: true) do
action
end
File.write('profiles/stackprof.json', Oj.dump(result, mode: :compat))
end

def profile_by_ruby_prof
result = RubyProf.profile do
action
end
printer = RubyProf::MultiPrinter.new(result, [:flat, :graph, :tree, :call_tree, :stack, :graph_html])
printer.print(:path => ".", :profile => "profiles/ruby_prof_profile")
end

def profile_by_vernier = Vernier.profile(out: "profiles/time_profile.json") do
action
end

def action = Main.new(options: { count_lines: }).call
end
Loading