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

scherbakov task #136

Open
wants to merge 2 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
64 changes: 64 additions & 0 deletions .byebug_history
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
continue
report
report['usersStats'][user_key] ||= {}
user_key = "#{user['first_name']} #{user['last_name']}"
report
stats
user
continue
report
report['usersStats'][user_key] = report['usersStats'][user_key].merge(stats)
report
report['usersStats'][user_key] ||= {}
report
user_key = "#{user['first_name']} #{user['last_name']}"
stats
sessions.select('pluck')
sessions.pluck('time')
time_sessions = sessions.map { |s| s['time'] }.map(&:to_i)
sessions.count
sessions
continue
report
collect_stats_for_user(report, u, sessions[u['id']])
report
collect_stats_for_user(report, u, sessions[u['id']])
u = users.first
users
pp sessions["0"]
sessions["0"]
sessions
continue
sessions["0"]
sessions
line
continue
line
continue
sessions
line
continue
line
sessions
continue
sessions
current_user
continue
current_user
user
sessions["0"]
sessions
line
continue
line
continue
sessions
line
continue
line
continue
line
continue
line
continue
line
71 changes: 51 additions & 20 deletions case-study-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,76 @@
Я решил исправить эту проблему, оптимизировав эту программу.

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

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

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

Вот как я построил `feedback_loop`: *как вы построили feedback_loop*
Вот как я построил `feedback_loop`: сначала выделить тест в отдельный файл. запустить профилировщики, посмотреть на самые тяжеловесные вычисления,
попробовать их оптимизировать, запустить тест, проверить метрики.

## Вникаем в детали системы, чтобы найти главные точки роста
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались*
Для того, чтобы найти "точки роста" для оптимизации я воспользовался профилировщиками. rbspy установить не получилось,
какие то проблемы с зависимостями glibc на mint 20.3, stackprof выдал пустой результат,
а ruby-prof оказался полезен. особо callgrind

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

### Ваша находка №1
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
- Больше всего времени(21%) занимает метод collect_stats_from_users, в частности Date.parse все тормозит.
Copy link
Collaborator

Choose a reason for hiding this comment

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

На самом деле самое проблемное место Array.select, где N^2 проходов по массиву в поисках сессий

- Посмотрел входные данные - увидел, что формат даты всегда одинаковый. значит вместо Date.parse можно использовать парсер, которому не нужно угадывать формат.
Заменил Date.parse(d) на Date.new(*d.split('-'))
Copy link
Collaborator

Choose a reason for hiding this comment

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

с датой можно вообще ничего не делать

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

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

### Ваша находка №X
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
- теперь больше всего времени занимают IO операции, read(16%) и write(14%)
- для вывода данных выбран формат json. можно попробовать сделать это с помощью библиотеки oj.
для записи данных использовал Oj.to_file('result.json', report, mode: :compat)
для чтения попробую использовать readlines
- значительных изменений нет
- IO::read осталось то же, IO::write даже увеличилось. мне кажется это нельзя изменить, поэтому нужно выбрать другую точку роста

### Ваша находка №3
- collect_stats_from_users вызывается 7 раз и занимает 33% времени. с помощью kcachegrind увидел, что в нем много раз вызывается map
- кажется, что метод много раз вызывается на одних и тех же данных. это похоже на n+1. порефакторим.
- доля array#each уменьшилась с 13% до 6,
- незначительно уменьшил долю each в общем выполнении

### Ваша находка №4
- что если не нужно ждать окончания readlines, после которого выполнять обработку данных, а сразу выполнять обработку?
получается читаем строку, сразу же ее обрабатываем - не нужно будет позже подгружать пользователя и все сессии.
когда блок сессий закончится, эти же данные можно записать в файл, но это выглядит сложно, и проблема ведь не в памяти?
можно начать с оптимизации подсчета статистик. например убрать .all? , использовать set для проверки уникальности браузеров
- почему-то во flat.txt не указывается IO::write. время выполнения уменьшилось на 300мс. но тк из 500 прошлых 24% занимал IO::write, а это около 100,
то можно считать, что улучшилось на 200. set реально работает

### Ваша находка №5
- помимо метода readlines оставшееся время съедает некий Array.each... их в общем то в коде много, надо подумать как уменьшить проходы по спискам
Copy link
Collaborator

Choose a reason for hiding this comment

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

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

- each это линейная операция, значит нужно взять что-то лучше. бинпоиск не поможет тк сначала данные нужно отсортировать, у нас нет на это времени.
можно взять хеш, у него время доступа O(1). осталось придумать только в каких местах можно его применить.
на первый взгляд можно убрать отдельный цикл sessions.each для подсчета браузеров. сделать это внутри readlines
- доля времени write выросла, each уменьшилась. большой файл все еще не читается

### Ваша находка №6
- в цикле users.each происходит еще один цикл users.select. это я глазом заметил и тут я начинаю подозревать, что 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.

возможно у вас слишком маленький объём данных был для профилирования? практически все первым же делом видят, что array.select на первом месте с большим отрывом

потому что за последние несколько раз метрики то пропадали, но давали странные значения. и у всех абсолютное время 0
- формирование сессий можно вынести так же в цикл чтения. заменил список sessions на хеш. тогда нам вообще не нужен класс user и список users_objects
- как изменилась метрика? а я не понял, потому что она не изменилась, но теперь большой файл возможно обработать. надо мне пошаманить с профилировщиками
единственное полезное на что мне указал ruby_prof это Date.parse, а то что Array.each занимает много времени мне ни о чем не говорит

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

*Какими ещё результами можете поделиться*

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

Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавил тест, проверяющий выполняется ли программа менее чем за минуту.
Copy link
Collaborator

Choose a reason for hiding this comment

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

да, если сделать алгоритм линейным, то можно аппроксимировать и сказать, что тест будет ок в случае если он обрабатывает 1/100 данных за 0.3с.

сначала думал что норм тест, а потом подождал - не норм. представил проект, в котором все тесты такие. надо подумать как уменьшить объем данных
чисто для теста. но это попозже, сейчас я устал
1 change: 1 addition & 0 deletions result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}
11 changes: 11 additions & 0 deletions ruby-prof-callgrind.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'ruby-prof'
require_relative 'task-1'

RubyProf.measure_mode = RubyProf::WALL_TIME

result = RubyProf.profile do
work(path: 'data.txt', disable_gc: true)
end

printer4 = RubyProf::CallTreePrinter.new(result)
printer4.print(:path => "ruby_prof_reports", :profile => 'callgrind')
11 changes: 11 additions & 0 deletions ruby-prof-callstack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'ruby-prof'
require_relative 'task-1'

RubyProf.measure_mode = RubyProf::WALL_TIME

result = RubyProf.profile do
work(path: 'data.txt', disable_gc: true)
end

printer = RubyProf::CallStackPrinter.new(result)
printer.print(File.open('ruby_prof_reports/callstack.html', 'w+'))
10 changes: 10 additions & 0 deletions ruby-prof-flat.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require 'ruby-prof'
require_relative 'task-1'

RubyProf.measure_mode = RubyProf::WALL_TIME

result = RubyProf.profile do
work(path: 'data.txt', disable_gc: true)
end
printer = RubyProf::FlatPrinter.new(result)
printer.print(File.open("ruby_prof_reports/flat.txt", "w+"))
Loading