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
Binary file added Screenshot 2022-05-31 at 11.40.52.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 67 additions & 0 deletions case_study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Case-study оптимизации

## Актуальная проблема
В нашем проекте возникла серьёзная проблема.

Необходимо было обработать файл с данными, чуть больше ста мегабайт.

У нас уже была программа на `ruby`, которая умела делать нужную обработку.

Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время.

Я решил исправить эту проблему, оптимизировав эту программу.

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

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

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

Вот как я построил `feedback_loop`:
Я разбил файл data_large.txt на несколько файлов с 1%, 5%, 10% и 50% от общего объема данных
Замеры начинались на 1% данных, что позволяло уложиться в 10 сек.
При видимом улучшении показателя потребляемой памяти изменения коммитились
При значительном снижении потребляемой памяти и времени работы программы постеменно увеличивался объем данных
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👍


## Вникаем в детали системы, чтобы найти главные точки роста
Для того, чтобы найти "точки роста" для оптимизации я воспользовался отчетом ruby-prof в формате cachegrind

Первые замеры (1% данных):
- время выполнения программы: 10.38 сек
- потребление памяти: 420мб

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

### Ваша находка №1
- Из отчета стало очевидно, что считывать файл в память, фомировать отчеты и уже после записывать в файл - слишком "дорого"
- Было решено переделать программу на формирование отчета по каждому пользователю, записи этого отчета в файл, обнуления пользователя в памяти и переход к следующему
Так же для отчета по статистике были использованы переменные-счетчики, которые обновлялись по мере чтения информации по каждому пользователю, а для хранения уникальных названий браузеров был выбран `SortedSet`
Отчет по пользователям писался в отдельный файл, после чего в финальный отчет записывался отчет по статистике, считывался файл с отчетами по каждому пользователю и последовательно дописывался в финальный отчет
- В результате время выполнения программы и потребление памяти значительно сократились, но до попадания в бюджет было еще далеко

Замеры во итогу (полный объем данных):
- время выполнения программы: 6.65 сек
- потребление памяти: 169 мб

### Ваша находка №2
- отчет rubyprof показал, что основное потребление памяти связано с повторным чтением из файла (`each_line`), в который писались отчеты по пользователям
стало очевидно, что использование временного вспомогательного файла хоть и дало результаты, но их явно недостаточно
- было решено писать отчеты по пользователям сразу в итоговый отчет, так же использовать счетчики для отчета по статистике и записывать статистику уже в конец файла. Это позволило бы избавиться от лишних операций с дополнительным файлом
- после некоторых "мучений" с формированием обоих отчетов в один синтаксически корректный .json файл результат был достигнут
- повторные запуски теста с выводом потребления памяти системным процессом показывали разные результаты, но каждый раз укладывались в бюджет
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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


Замеры по итогу (полный объем данных)
- время работы программы: 6.4 сек
- потребление памяти от 19 до 36 мб
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Судя по картинке из Valgrind там программа на самом деле не работает

Может быть файл не пробрасывается или что-то в таком роде

"Правильная картинка" - примерно ровная линия в районе 30-40MB

Главное, что потребление памяти практически не зависит от объёма входных данных и не растёт по мере работы программы

Copy link
Copy Markdown
Author

@vsav vsav May 31, 2022

Choose a reason for hiding this comment

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

Сейчас перепроверил еще раз, отключил тест, чтобы файл отчета не перезатирался. На выходе получил json на 151 мб. Это может как-то быть связано с тем, что у меня М1? может он как-то хитро с памятью работает через своп и профилировщик это не отслеживает? При этом пик по памяти показывает 4мб


## Результаты
В результате проделанной оптимизации наконец удалось обработать файл с данными.
Удалось улучшить метрику системы с 420 мб на 1% данных, до ~30 мб на полном объеме и уложиться в заданный бюджет.

Время выполнения программы так же значительно сократилось.

## Защита от регрессии производительности
Так как отчеты по потребляемой памяти разнились от запуска к запуску, для защиты от потери достигнутого прогресса при дальнейших изменениях программы использовались максимальные из полученных значения по памяти, а так же простой тест на корректность формирования самого отчета
177 changes: 0 additions & 177 deletions task-2.rb

This file was deleted.

132 changes: 132 additions & 0 deletions task_2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# frozen_string_literal: true

# Optimized version of homework task

require 'json'
require 'pry'
require 'set'
require 'minitest/autorun'

DELIMITER = ','
WITH_SPACE = ', '
IE = 'I'
CR = 'C'

def work(path)
file = File.open(path)
@report_file = File.open('result.json', 'w')
@report_file.write('{"usersStats":{')
@user_report = {}
@users_count = 0
@sessions_count = 0
@all_browsers = SortedSet.new

file.each_line(chomp: true) do |line|
cols = line.split(DELIMITER)
if line.start_with?('u')
@users_count += 1
write_user_report(false) unless @user_report == {}
@user_name = "#{cols[2]} #{cols[3]}"
@user_report = {}
@user_report[@user_name] = initialize_user_report
elsif line.start_with?('s')
@sessions_count += 1
@user_report[@user_name]['sessionsCount'] += 1
@user_report[@user_name]['totalTime'] += cols[4].to_i
@user_report[@user_name]['longestSession'] = longest_session(cols[4].to_i)
@user_report[@user_name]['browsers'] << cols[3].upcase!
@user_report[@user_name]['usedIE'] = true if used_ie?(cols[3])
@user_report[@user_name]['alwaysUsedChrome'] = only_chrome?(@user_report[@user_name]['browsers'])
@user_report[@user_name]['dates'] << cols[5]
@all_browsers << cols[3]
end
end
write_user_report(true)
@report_file.write(DELIMITER, summary_report.to_json[1..-1])
@report_file.close

puts 'MEMORY USAGE: %d MB' % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)
end

def longest_session(time)
@user_report[@user_name]['longestSession'] < time ? time : @user_report[@user_name]['longestSession']
end

def used_ie?(browser)
browser.start_with?(IE)
end

def only_chrome?(browsers)
return false if @user_report[@user_name]['usedIE']

browsers.all? { |b| b.start_with?(CR) }
end

def initialize_user_report
{
'sessionsCount' => 0,
'totalTime' => 0,
'longestSession' => 0,
'browsers' => [],
'usedIE' => false,
'alwaysUsedChrome' => false,
'dates' => []
}
end

def write_user_report(last)
prepare_report
if last
@report_file.write(@user_report.to_json[1..-2], '}')
else
@report_file.write(@user_report.to_json[1..-2], DELIMITER)
end
end

def summary_report
{
'totalUsers' => @users_count,
'uniqueBrowsersCount' => @all_browsers.size,
'totalSessions' => @sessions_count,
'allBrowsers' => @all_browsers.to_a.join(DELIMITER)
}
end

def prepare_report
@user_report[@user_name]['totalTime'] = "#{@user_report[@user_name]['totalTime']} min."
@user_report[@user_name]['longestSession'] = "#{@user_report[@user_name]['longestSession']} min."
@user_report[@user_name]['browsers'] = @user_report[@user_name]['browsers'].sort!.join(WITH_SPACE)
@user_report[@user_name]['dates'] = @user_report[@user_name]['dates'].sort!.reverse!
end

class TestMe < Minitest::Test
def setup
File.write('result.json', '')
File.write('data.txt',
'user,0,Leida,Cira,0
session,0,0,Safari 29,87,2016-10-23
session,0,1,Firefox 12,118,2017-02-27
session,0,2,Internet Explorer 28,31,2017-03-28
session,0,3,Internet Explorer 28,109,2016-09-15
session,0,4,Safari 39,104,2017-09-27
session,0,5,Internet Explorer 35,6,2016-09-01
user,1,Palmer,Katrina,65
session,1,0,Safari 17,12,2016-10-21
session,1,1,Firefox 32,3,2016-12-20
session,1,2,Chrome 6,59,2016-11-11
session,1,3,Internet Explorer 10,28,2017-04-29
session,1,4,Chrome 13,116,2016-12-28
user,2,Gregory,Santos,86
session,2,0,Chrome 35,6,2018-09-21
session,2,1,Safari 49,85,2017-05-22
session,2,2,Firefox 47,17,2018-02-02
session,2,3,Chrome 20,84,2016-11-25
')
end

def test_result
work('data.txt')
expected_result = JSON.parse('{"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"]}}}')
assert_equal expected_result, JSON.parse(File.read('result.json'))
end
end