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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
data.txt
data_1MB.txt
data_20MB.txt
data_5MB.txt
data_large.txt
result.json
115 changes: 104 additions & 11 deletions case-study-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,128 @@
Я решил исправить эту проблему, оптимизировав эту программу.

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

- Измерять общее время выполнения скрипта с помощью `Benchmark.realtime`
- Использовать профилировщие памяти `memory_profiler`
Copy link
Owner

Choose a reason for hiding this comment

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

Опечатка профилировщие


Для оптимизации был использован тестовый файл 25000 строк (объемом около 1МБ), перед началом оптимизации были следующие показатели:

- Расход памяти: 1655 MB
- Время выполнения: 25.61 sec

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

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

Вот как я построил `feedback_loop`:

Вот как я построил `feedback_loop`: *как вы построили feedback_loop*
- подготовка тестового файла на 25 000 строк
- замеры метрик
- анализ метрик
- исправление самого узкого места
- оценка полученного результата

## Вникаем в детали системы, чтобы найти 20% точек роста
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались*
Для того, чтобы найти "точки роста" для оптимизации я воспользовался `benchmark` и `memory_profiler`

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

### Ваша находка №1
О вашей находке №1
Большего всего памяти расходовалось при формировании массивов `users` и `sessions`
Copy link
Owner

Choose a reason for hiding this comment

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

Большего -> Больше

Изменил формирование на модификации массивов.
Это позволило снизить расход памяти и немного уменьшить время выполнения.

``` shellsession
rss after concatenation: 1655 MB
Finish in 25.61

rss after concatenation: 491 MB
Finish in 18.09

```

### Ваша находка №2
О вашей находке №2
Следующим узким местом был сам принцип формирования массивов.
Copy link
Owner

Choose a reason for hiding this comment

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

А как это нашли?

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

``` shellsession
rss after concatenation: 491 MB
Finish in 18.09

rss after concatenation: 418 MB
Finish in 4.57

```

### Ваша находка №3
Следующее узкое место формирование статистики юзера.
Copy link
Owner

Choose a reason for hiding this comment

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

Как нашли?

Объединил формирование в статистики в один цикл.
Уменьшилось количество потребляемой памяти и время выполнения

``` shellsession
rss after concatenation: 418 MB
Finish in 4.57

rss after concatenation: 370 MB
Finish in 3.68

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

### Ваша находка №4
По метрикам и по моему имеющемуся опыту следущее узкое место это парсинг дат.
Copy link
Owner

Choose a reason for hiding this comment

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

По каким метрикам?

Отрефакторил заменив `Date.parse` на `Date.strptime`
В итоге уменьшилось количество потребляемой памяти и время выполнения

``` shellsession
rss after concatenation: 370 MB
Finish in 3.68

rss after concatenation: 314 MB
Finish in 2.87

```

### Ваша находка №5
По метрикам было видно что чаще всего используются стринги ` ` и `, `.
Вынес их в константы, уменьшив потребление памяти и времени.

``` shellsession
rss after concatenation: 314 MB
Finish in 2.87

rss after concatenation: 308 MB
Finish in 2.67

```

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


``` shellsession
Start
rss before concatenation: 24 MB
rss after concatenation: 50 MB
Finish in 0.26

```

Файл `data_large.txt` в итоге стал обрабатываться за время 46.93 секунды

``` shellsession
Start
rss before concatenation: 23 MB
rss after concatenation: 2530 MB
Finish in 46.93

*Какими ещё результами можете поделиться*
```
Также есть дальнейший путь оптимизации, к примеру замена регулярок на `match`,
изменить формирование хешей с `stringify keys` на `symbolize keys`

## Защита от регресса производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали*
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сохрнял среднее значение минимального времени выполения и потребления памяти.
129 changes: 58 additions & 71 deletions task-1.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
require 'pry'
require 'date'
require 'minitest/autorun'
require 'benchmark'
require 'memory_profiler'

class User
attr_reader :attributes, :sessions
Expand All @@ -14,45 +16,54 @@ def initialize(attributes:, sessions:)
end
end

IE_REG = /INTERNET EXPLORER/.freeze
CHROME_REG = /CHROME/.freeze
COMMA = ','.freeze

def parse_user(user)
fields = user.split(',')
parsed_result = {
fields = user.split(COMMA)
{
'id' => fields[1],
'first_name' => fields[2],
'last_name' => fields[3],
'age' => fields[4],
'age' => fields[4]
}
end

def parse_session(session)
fields = session.split(',')
parsed_result = {
fields = session.split(COMMA)
{
'user_id' => fields[1],
'session_id' => fields[2],
'browser' => fields[3],
'time' => fields[4],
'date' => fields[5],
'browser' => fields[3].upcase,
'time' => fields[4].to_i,
'date' => Date.strptime(fields[5]).iso8601
}
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']}"
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 process_user_data(line)
User.new(attributes: parse_user(line), sessions: [])
end

def work(file = 'data.txt')
unique_browsers = []
users_objects = []

users = []
sessions = []
File.read(file).split("\n").each do |line|
next users_objects << process_user_data(line) if line.start_with?('user')

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'
users_objects.last.sessions << parse_session(line)
next if unique_browsers.include?(users_objects.last.sessions.last['browser'])

unique_browsers << users_objects.last.sessions.last['browser']
end

# Отчёт в json
Expand All @@ -72,77 +83,53 @@ def work

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[:totalUsers] = users_objects.count

report['allBrowsers'] =
sessions
.map { |s| s['browser'] }
.map { |b| b.upcase }
.sort
.uniq
.join(',')
report['uniqueBrowsersCount'] = unique_browsers.count

# Статистика по пользователям
users_objects = []
report['totalSessions'] = users_objects.flat_map(&:sessions).count

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['allBrowsers'] = unique_browsers.sort.join(',')

report['usersStats'] = {}

# Собираем количество сессий по пользователям
collect_stats_from_users(report, users_objects) do |user|
{ 'sessionsCount' => user.sessions.count }
tme = user.sessions.map { |s| s['time'] }
brw = user.sessions.map { |s| s['browser'] }
{
'sessionsCount' => user.sessions.count,
'totalTime' => tme.sum.to_s << ' min.',
'longestSession' => tme.max.to_s << ' min.',
'browsers' => brw.sort.join(', '),
'usedIE' => brw.any? { |b| b =~ IE_REG },
'alwaysUsedChrome' => brw.all? { |b| b =~ CHROME_REG },
'dates' => user.sessions.map! { |s| s['date'] }.sort.reverse
}
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
File.write('result.json', "#{report.to_json}\n")
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
puts 'Start'

# Браузеры пользователя через запятую
collect_stats_from_users(report, users_objects) do |user|
{ 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') }
end
def print_memory_usage
"%d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)
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
time = Benchmark.realtime do
puts "rss before concatenation: #{print_memory_usage}"

# Всегда использовал только Chrome?
collect_stats_from_users(report, users_objects) do |user|
{ 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } }
report = MemoryProfiler.report do
work('data_1MB.txt')
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
report.pretty_print(detailed_report: true)

File.write('result.json', "#{report.to_json}\n")
puts "rss after concatenation: #{print_memory_usage}"
end

puts "Finish in #{time.round(2)}"

class TestMe < Minitest::Test
def setup
File.write('result.json', '')
Expand Down