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
161 changes: 148 additions & 13 deletions case-study-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
Я решил исправить эту проблему, оптимизировав эту программу.

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

Choose a reason for hiding this comment

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

👍

Хочу не потеряв в удобности поддержки кода оптимизировать код чтобы программа работала хотя бы в пределах минуты

## Гарантия корректности работы оптимизированной программы
Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации.
Expand All @@ -23,22 +24,156 @@
Вот как я построил `feedback_loop`: *как вы построили feedback_loop*

## Вникаем в детали системы, чтобы найти 20% точек роста
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались*
Первой очевидной точкой раста на первый взгляд кажется чтение файла, но даже в первоначальном состоянии это
занимает около 1,5 секунды что в рамках работы всей программы не так много.

Для того, чтобы найти "точки роста" для оптимизации я воспользовался
- Посмотрел с помощью benchmark как растет потребление памяти при обработке 100, 1000, 10000, 50000 строк из файла, без построения отчета
получились такие результаты:
100 - 0.0003s
1000 - 0.005s
10000 - 0.5s
50000 - 6.02s программа не отработала
100000 - 21.8945s программа не отработала
Окей, время работы растет очень сильно, но 6 секунд на парсинг строк и в несколько раз больше времени на построение отчета говорят что
основные проблемы не в парсинге строк. Этот момент тоже надо оптимизировать, но позже, пока приоритеты на оптимизацию выглядят так

1. ???
Copy link
Owner

Choose a reason for hiding this comment

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

👍

2. Парсинг строк
3. Потоковое чтение из файла

Проверяем каждый этап построения отчета на массиве в 10к строк
Больше всего времени уходит на построение массива из инстанос класса User
парсинг строк - 0.4936
поиск уникальных браузеров - 0.0604
Подсчёт количества уникальных браузеров - 0.0604
Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом - 0.005
сколько всего сессий + 0.1847
сколько всего времени + 0.0638
самая длинная сессия + 0.0067
браузеры через запятую + 0.0104
Хоть раз использовал IE? + 0.0075
Всегда использовал только Хром? + 0.006
даты сессий в порядке убывания через запятую + 0.0596

массив из User - 1.4759

Проверяем на 20000 строк
парсинг строк - 0.9174
поиск уникальных браузеров - 0.1074
Подсчёт количества уникальных браузеров - 0.0604
Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом - 0.009
сколько всего сессий + 0.0047
сколько всего времени + 0.0131
самая длинная сессия + 0.0128
браузеры через запятую + 0.0232
Хоть раз использовал IE? + 0.018
Всегда использовал только Хром? + 0.0135
даты сессий в порядке убывания через запятую + 0.1147

массив User = 9.5216


Очевидная проблема в генерации массива инстансов User.
Список кандидатов на оптимизацию выглядит так
1. Построение массива юзеров
2. Парсинг строк
3. Построчное чтение файла

Очевидная проблема внутри генерации массива это поиск сессий по юзеру, если заранее сделать
группировку по user['id'] то можно избавиться от большого количества поисков
проверяем на 20000 строк все становится намного лучше
массив строится за 0.0181 секунды.

Можно пробовать 50000 строк
Парсинг строк - 5.9629 сек
Построение массива - 0.2721

100000 строк
Парсинг строк - 22.8494 сек
Построение массива - 0.9219 сек

Убираем лимит строк
3250940 строк все еще не рабоает

В коде который парсит строки и разбивает их на 2 массива есть очевидная проблема в месте
где складываются массивы `users = users + [parse_user(line)]`, замена на `users.push(parse_user(line))`
явно улучшит ситуацию
пробуем

100000 строк
Парсинг строк - 0.3815 сек
Построение массива - 1.6567 сек

200000 строк
Парсинг строк - 1.079 сек
Построение массива - 4.9811 сек

500000 строк
Парсинг строк - 2.7817 сек
Построение массива - 27.1264 сек
Подсчет количества уникальных браузеров - 2.7305 сек

1 000 000 строк
Парсинг строк - 6.7363 сек
Построение массива - долго
Подсчет количества уникальных браузеров - 5.1087 сек

очевидно что надо улучшать построение массива юзеров
тоже меняем на `users_objects.push(user_object)`

1 000 000 строк
Парсинг строк - 5.4842 сек
Построение массива - 0.6537
Подсчет количества уникальных браузеров - 5.1087 сек

2 000 000 строк
Парсинг строк - 14.5464 сек
Построение массива - 1.5422
Подсчет количества уникальных браузеров - 11.0065 сек
Подсчет количества всех браузеров - 6.2511

Смотрим внимательно на подсчет браузеров по сессиям
Для подсчета уникальных браузеров нам не нужен сам массив браузеров, переписываем на array#count
получили уменьшение времени с 11 сек до 1.0845
Для вывода всех браузеров объединяем 2 подряд map в один и сортировку делаем после uniq
потребляемое время уменьшилось с 6 секунд до 2.9063
неидеально, но сейчас проблема в другом

2 000 000 записей
парсинг строк - 14.2793
поиск уникальных браузеров - 1.0389
Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом - 2.9063
сколько всего сессий + 2.9591
сколько всего времени + 2.3199
самая длинная сессия + 2.6779
браузеры через запятую + 7.2522
Хоть раз использовал IE? + 2.5836
Всегда использовал только Хром? + 4.4826
даты сессий в порядке убывания через запятую + 18.4876

Очень много времени занимает вывод дат сессий, смотрим подробнее
если сделать допущение что все даты сессий в файле изначально записаны в формате
iso8601 (а если это лог нашей программы то мы можем на это влиять), то можно
Copy link
Owner

Choose a reason for hiding this comment

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

👍

сразу сортировать строки.
Сделав так получаем время работы этой статистики - 4.3849

Пробуем убрать лимит на количество строк
программа отработала за 1 мин 49 сек 💪, 🤟, 🍾
Copy link
Owner

Choose a reason for hiding this comment

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

👍 👍

Окей, отработала и уже хорошо, но надо найти способы еще ее ускорить
Больше всего времени сейчас занимает разбор считанных строк из файла, пробуем считывать файл построчно
28,4398 лучше не стало
Пробуем убрать дополнительный split внутри parse_session и parse_user
19.6525 очевидно что лучше.
Совсем очевидных проблем кажется что нет, надо запускать профайлеры памяти

RubyProf подсказал еще места которые позволили довести время работы программы до 80-90 секунд

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

### Ваша находка №1
О вашей находке №1

### Ваша находка №2
О вашей находке №2

### Ваша находка №X
О вашей находке №X

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

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

Expand Down
151 changes: 18 additions & 133 deletions task-1.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,147 +5,32 @@
require 'date'
require 'minitest/autorun'

class User
attr_reader :attributes, :sessions
require 'ruby-prof'
require 'benchmark'

def initialize(attributes:, sessions:)
@attributes = attributes
@sessions = sessions
end
end

def parse_user(user)
fields = user.split(',')
parsed_result = {
'id' => fields[1],
'first_name' => fields[2],
'last_name' => fields[3],
'age' => fields[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],
}
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']}"
report['usersStats'][user_key] ||= {}
report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user))
end
end
require './user'
require './work'

def work
file_lines = File.read('data.txt').split("\n")
if ARGV[0] == 'run'
profiling_result = nil

users = []
sessions = []

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'
before_mem = `ps -o rss= -p #{Process.pid}`.to_i
total_time = Benchmark.realtime do
profiling_result = RubyProf.profile { Work.work('data_large.txt', 'large_result.json') }
end
after_mem = `ps -o rss= -p #{Process.pid}`.to_i

# Отчёт в json
# - Сколько всего юзеров +
# - Сколько всего уникальных браузеров +
# - Сколько всего сессий +
# - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом +
#
# - По каждому пользователю
# - сколько всего сессий +
# - сколько всего времени +
# - самая длинная сессия +
# - браузеры через запятую +
# - Хоть раз использовал IE? +
# - Всегда использовал только Хром? +
# - даты сессий в порядке убывания через запятую +

report = {}

report[:totalUsers] = users.count

# Подсчёт количества уникальных браузеров
uniqueBrowsers = []
sessions.each do |session|
browser = session['browser']
uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser }
end

report['uniqueBrowsersCount'] = uniqueBrowsers.count

report['totalSessions'] = sessions.count

report['allBrowsers'] =
sessions
.map { |s| s['browser'] }
.map { |b| b.upcase }
.sort
.uniq
.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]
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 } }
end
mem_diff = (after_mem - before_mem) / 1024

File.write('result.json', "#{report.to_json}\n")
puts "Total time #{total_time.round(4)}"
puts "Mem diff #{mem_diff}"
printer = RubyProf::CallTreePrinter.new(profiling_result)
printer.print(path: '.', profile: "profile_memory")
end

class TestMe < Minitest::Test
def setup
File.write('result.json', '')
File.write('test_result.json', '')
File.write('data.txt',
'user,0,Leida,Cira,0
session,0,0,Safari 29,87,2016-10-23
Expand All @@ -169,8 +54,8 @@ def setup
end

def test_result
work
Work.work('data.txt', 'test_result.json')
expected_result = '{"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"]}}}' + "\n"
assert_equal expected_result, File.read('result.json')
assert_equal expected_result, File.read('test_result.json')
end
end
8 changes: 8 additions & 0 deletions user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class User
attr_reader :attributes, :sessions

def initialize(attributes:, sessions:)
@attributes = attributes
@sessions = sessions
end
end
Loading