-
Notifications
You must be signed in to change notification settings - Fork 24
task-1 #6
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?
task-1 #6
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,80 @@ | ||
| # Case-study оптимизации | ||
|
|
||
| ## Актуальная проблема | ||
| В нашем проекте возникла серьёзная проблема. | ||
|
|
||
| Необходимо было обработать файл с данными, чуть больше ста мегабайт. | ||
|
|
||
| У нас уже была программа на `ruby`, которая умела делать нужную обработку. | ||
|
|
||
| Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. | ||
|
|
||
| Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
|
||
| ## Формирование метрики | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: | ||
| Для 1мб файла: необходимо 156Мб памяти, парсинг проходит за 37 сек. | ||
|
|
||
| ## Гарантия корректности работы оптимизированной программы | ||
| Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. | ||
|
|
||
| ## Feedback-Loop | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений моментально | ||
|
|
||
| Вот как я построил `feedback_loop`: я проверял обратку файла на разные метрики, 1 изменение - 1 коммит, если изменение дало нужный результат | ||
|
|
||
| ## Вникаем в детали системы, чтобы найти 20% точек роста | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался memory_profiler, stackprof, ruby-prof, QCacheGrind | ||
|
|
||
| Вот какие проблемы удалось найти и решить | ||
|
|
||
| ### Ваша находка №1 | ||
| Большое выделение памяти при работе с классами: | ||
| * Array | ||
| * String | ||
| * Hash | ||
|
|
||
| ### Ваша находка №2 | ||
| Основное процессорное время уходило на обработку массивов (Array#each, Array#map), на сбор данных (Object#collect_stats_from_users) | ||
| и конкатенацию строк (String#split) | ||
|
|
||
| ### Ваша находка №3 | ||
| Долго происходил процесс обработки сессий для каждого пользователя | ||
|
Owner
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. Хорошо было бы добавить к каждому шагу больше конкретики.
|
||
|
|
||
| ## Результаты | ||
| В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
| Удалось улучшить метрику системы с: | ||
| * memory | ||
| allocated memory by class | ||
| ----------------------------------- | ||
| 293_586_874_4 Array | ||
| 436_974_43 String | ||
| 260_091_68 Hash | ||
| 867_291_2 MatchData | ||
| 162_813_6 Date | ||
| Total allocated: 301_605_315_2 bytes (1325648 objects) | ||
| * cpu | ||
| 100.00% 0.00% 1405386.00 11.00 0.00 1405375.00 1 Object#work | ||
| * time | ||
| 1mb: Finish in 37.34 | ||
| ~128mb: infinity | ||
|
|
||
| на: | ||
| * memory | ||
| allocated memory by class | ||
| ----------------------------------- | ||
| 326_126_88 String | ||
| 204_421_52 MatchData | ||
| 161_736_32 Hash | ||
| 768_498_4 Array | ||
| 162_813_6 Date | ||
| 163_120 User | ||
| Total allocated: 787_261_33 bytes (857028 objects) | ||
| * cpu | ||
| 100.00% 0.00% 857034.00 7.00 0.00 857027.00 1 Object#work | ||
| * time | ||
| 1mb: Finish in 0.6 | ||
| ~128Mb: Finish in 110.46 | ||
|
|
||
| ## Защита от регресса производительности | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *ничего* :) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,11 @@ | |
| require 'pry' | ||
| require 'date' | ||
| require 'minitest/autorun' | ||
| require 'benchmark' | ||
| require 'memory_profiler' | ||
| require 'ruby-prof' | ||
|
|
||
| RubyProf.measure_mode = RubyProf::ALLOCATIONS | ||
|
|
||
| class User | ||
| attr_reader :attributes, :sessions | ||
|
|
@@ -15,44 +20,45 @@ def initialize(attributes:, sessions:) | |
| end | ||
|
|
||
| def parse_user(user) | ||
| fields = user.split(',') | ||
| parsed_result = { | ||
| 'id' => fields[1], | ||
| 'first_name' => fields[2], | ||
| 'last_name' => fields[3], | ||
| 'age' => fields[4], | ||
| { | ||
| 'id' => user[1], | ||
| 'first_name' => user[2], | ||
| 'last_name' => user[3], | ||
| 'age' => user[4], | ||
| } | ||
| end | ||
|
|
||
| def parse_session(session) | ||
| fields = session.split(',') | ||
| parsed_result = { | ||
| 'user_id' => fields[1], | ||
| 'session_id' => fields[2], | ||
| 'browser' => fields[3], | ||
| 'time' => fields[4], | ||
| 'date' => fields[5], | ||
| { | ||
| 'session_id' => session[2], | ||
| 'browser' => session[3].upcase!, | ||
| 'time' => session[4].to_i, | ||
| 'date' => session[5], | ||
| } | ||
| end | ||
|
|
||
| def collect_stats_from_users(report, users_objects, &block) | ||
| users_objects.each do |user| | ||
| user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" | ||
| while users_objects.size > 0 do | ||
| user = users_objects.shift | ||
| user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" | ||
| report['usersStats'][user_key] ||= {} | ||
| report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) | ||
| report['usersStats'][user_key].merge!(block.call(user)) | ||
| end | ||
| end | ||
|
|
||
| def work | ||
| file_lines = File.read('data.txt').split("\n") | ||
|
|
||
| def work(file = 'data.txt') | ||
| users = [] | ||
| sessions = [] | ||
| sessions = Hash.new { |hash, key| hash[key] = [] } | ||
| total_sessions = 0 | ||
|
|
||
|
|
||
| file_lines.each do |line| | ||
| cols = line.split(',') | ||
| users = users + [parse_user(line)] if cols[0] == 'user' | ||
| sessions = sessions + [parse_session(line)] if cols[0] == 'session' | ||
| File.foreach(file) do |line| | ||
| line = line.split(',') | ||
| users << parse_user(line) if line[0] == 'user' | ||
| if line[0] == 'session' | ||
| sessions[line[1]] << parse_session(line) | ||
| total_sessions += 1 | ||
| end | ||
| end | ||
|
|
||
| # Отчёт в json | ||
|
|
@@ -75,69 +81,50 @@ def work | |
| report[:totalUsers] = users.count | ||
|
|
||
| # Подсчёт количества уникальных браузеров | ||
| uniqueBrowsers = [] | ||
| sessions.each do |session| | ||
| browser = session['browser'] | ||
| uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } | ||
| end | ||
| uniqueBrowsers = sessions.values.flatten.map! { |session| session['browser'] }.uniq! || [] | ||
|
|
||
| report['uniqueBrowsersCount'] = uniqueBrowsers.count | ||
|
|
||
| report['totalSessions'] = sessions.count | ||
| report['totalSessions'] = total_sessions | ||
|
|
||
| report['allBrowsers'] = | ||
| sessions | ||
| .map { |s| s['browser'] } | ||
| .map { |b| b.upcase } | ||
| .sort | ||
| .uniq | ||
| .join(',') | ||
| report['allBrowsers'] = uniqueBrowsers.sort!.join(',') | ||
|
|
||
| # Статистика по пользователям | ||
| users_objects = [] | ||
|
|
||
| users.each do |user| | ||
| attributes = user | ||
| user_sessions = sessions.select { |session| session['user_id'] == user['id'] } | ||
| user_object = User.new(attributes: attributes, sessions: user_sessions) | ||
| users_objects = users_objects + [user_object] | ||
| users.map! do |user| | ||
| user_id = user['id'] | ||
| user_sessions = sessions[user_id] | ||
| User.new(attributes: user, sessions: user_sessions) | ||
| end | ||
|
|
||
| report['usersStats'] = {} | ||
|
|
||
| # Собираем количество сессий по пользователям | ||
| collect_stats_from_users(report, users_objects) do |user| | ||
| { 'sessionsCount' => user.sessions.count } | ||
| end | ||
|
|
||
| # Собираем количество времени по пользователям | ||
| collect_stats_from_users(report, users_objects) do |user| | ||
| { 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' } | ||
| end | ||
|
|
||
| # Выбираем самую длинную сессию пользователя | ||
| collect_stats_from_users(report, users_objects) do |user| | ||
| { 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' } | ||
| end | ||
|
|
||
| # Браузеры пользователя через запятую | ||
| collect_stats_from_users(report, users_objects) do |user| | ||
| { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } | ||
| end | ||
|
|
||
| # Хоть раз использовал IE? | ||
| collect_stats_from_users(report, users_objects) do |user| | ||
| { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } | ||
| end | ||
|
|
||
| # Всегда использовал только Chrome? | ||
| collect_stats_from_users(report, users_objects) do |user| | ||
| { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } | ||
| end | ||
|
|
||
| # Даты сессий через запятую в обратном порядке в формате iso8601 | ||
| collect_stats_from_users(report, users_objects) do |user| | ||
| { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } | ||
|
|
||
| collect_stats_from_users(report, users) do |user| | ||
| users_times = user.sessions.map { |s| s['time'] } | ||
| users_browsers = user.sessions.map { |s| s['browser'] } | ||
| ie_counter = 0 | ||
| chrome_counter = 0 | ||
| users_browsers.each do |b| | ||
| ie_counter += 1 if ie_counter == 0 && /INTERNET EXPLORER/.match?(b) | ||
|
Owner
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. А зачем проверка
Author
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. я думал, что так можно сократить лишнюю проверку #match? |
||
| chrome_counter += 1 if /CHROME/.match?(b) | ||
| end | ||
|
|
||
| { | ||
| 'sessionsCount' => user.sessions.count, | ||
| 'totalTime' => "#{users_times.sum} min.", | ||
| 'longestSession' => "#{users_times.max} min.", | ||
| 'browsers' => users_browsers.sort!.join(', '), | ||
| 'usedIE' => ie_counter > 0, | ||
| 'alwaysUsedChrome' => chrome_counter == users_browsers.size, | ||
| 'dates' => user.sessions.map { |s| Date.parse(s['date']).iso8601 }.sort!.reverse! | ||
| } | ||
| end | ||
|
|
||
| File.write('result.json', "#{report.to_json}\n") | ||
|
|
||
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.
Ваша находка №1->Находка №1?