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

dz2: memory #93

Open
wants to merge 1 commit 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.ruby-version
/tmp
/test_data/*.txt
59 changes: 59 additions & 0 deletions case-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Case-study оптимизации

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

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

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

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

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

## Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: тестовый файл на 5_000 строк, до оптимизации объем выделяемой памяти равен 2759Мб(по данным memory_profiler)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Как-то многовато для такого маленького файла конечно


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

## Feedback-Loop
Выполняю профилирование на набольшом объеме данных, оптимизирую код, проверяю на большем кол-ве данных и так по кругу, пока не добьюсь нужного результата

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

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

### При парсинге даты выделяется большое кол-во памяти и создается много объектов
- memory profiler и ruby prof
- Сократил парсинг даты до 1 вызова map
Copy link
Collaborator

Choose a reason for hiding this comment

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

парсинг вообще не нужен

- Объем потребляемой памяти сократился до 2074Мб
- Парсинг даты по прежнему потребляет много памяти, но на данном этапе считаю эту точку роста оптимизированной.

### Добавление информации о браузерах пользователя в сессии
- memory profiler
- Отрефакторить строку
- Потребление памяти незначительно сократилось до 2057Мб
- По количеству выделяемой памяти данная строка опустилась на 3 место, на данный момент считаем метод оптимизированным

### Добавление объектов в массив через сложение массивов
- memory profiler
- Изменил добавление элемента в массив через оператор <<
- Объем потребляемой памяти сократился до 1985Мб
- На данном этапе добавление элементов в массив не является точкой роста

### Select из массива сессий через сравнение с id пользователя
- memory profiler
- Сделаю из массива сессий хеш, а так же в блоке изменю добваление в массив через оператор <<
- Объем потребляемой памяти сократился до 1971Мб
- На данном этапе добавление элементов в массив не является точкой роста

### Большое количество памяти выделяется при добавлении информации о браузерах(эксплорер, хром)
- memory profiler
- В целом отрефакторю код
- Скорость программы увеличилась, объем выделяемой памяти уменьшился
- На данном этапе добавление элементов в массив не является точкой роста

## Результаты
В результате рефакторинга уменишьлось кол-во потребляемой памяти, а так же увеличилась скорость выполнения программы
Binary file added massif-visualizer.png
Copy link
Collaborator

Choose a reason for hiding this comment

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

Что-то пошло не так

У вас похоже программа не загрузилась даже нормально

Нормальная картина в данном случае - программа грузится и быстро выходит на плато где-то в 40 МБ на всём протяжении работы секунд в 20

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions profilers/mem_prof.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'memory_profiler'
require_relative '../task-2.rb'

report = MemoryProfiler.report do
work(file_path: 'test_data/data5000.txt')
end

report.pretty_print
17 changes: 17 additions & 0 deletions profilers/ruby_prof_alloc.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require 'ruby-prof'
require_relative '../task-2.rb'

RubyProf.measure_mode = RubyProf::ALLOCATIONS

result = RubyProf.profile do
work(file_path: 'test_data/data5000.txt')
end

printer = RubyProf::MultiPrinter.new(result)
printer.print(path: 'tmp/', profile: "rp_alloc_#{Time.now}")

File.open("tmp/rp_alloc_callstack_#{Time.now}.html", 'w') do |f|
RubyProf::CallStackPrinter.new(result).print(f)
end


15 changes: 15 additions & 0 deletions profilers/ruby_prof_mem.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'ruby-prof'
require_relative '../task-2.rb'

RubyProf.measure_mode = RubyProf::MEMORY

result = RubyProf.profile do
work(file_path: 'test_data/data5000.txt')
end

printer = RubyProf::MultiPrinter.new(result)
printer.print(path: 'tmp/', profile: "rp_mem_#{Time.now}")

File.open("tmp/rp_mem_callstack_#{Time.now}.html", 'w') do |f|
RubyProf::CallStackPrinter.new(result).print(f)
end
9 changes: 9 additions & 0 deletions profilers/stackprof.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require 'stackprof'
require 'json'
require_relative '../task-2.rb'

profile = StackProf.run(mode: :object, raw: true) do
work(file_path: 'test_data/data5000.txt')
end

File.write('tmp/stackprof-speedscore.json', JSON.generate(profile))
172 changes: 49 additions & 123 deletions task-2.rb
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
# Deoptimized version of homework task
# frozen_string_literal: true

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


class User
attr_reader :attributes, :sessions

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

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

def parse_session(session)
fields = session.split(',')
parsed_result = {
def parse_session(fields)
{
'user_id' => fields[1],
'session_id' => fields[2],
'browser' => fields[3],
Expand All @@ -35,25 +34,27 @@ def parse_session(session)
}
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
def collect_stats_from_users(user)
user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}"
browsers = user.sessions.map { |s| s['browser'].upcase }

@report['usersStats'] ||= {}
@report['usersStats'][user_key] = {
'sessionsCount' => user.sessions.count,
'totalTime' => user.sessions.map { |s| s['time'] }.map(&:to_i).sum.to_s + ' min.',
Copy link
Collaborator

Choose a reason for hiding this comment

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

map = куча лишних объектов каждый раз

'longestSession' => user.sessions.map { |s| s['time'] }.map(&:to_i).max.to_s + ' min.',
'browsers' => browsers.sort.join(', '),
'usedIE' => browsers.any? { |b| /INTERNET EXPLORER/.match?(b) },
'alwaysUsedChrome' => browsers.all? { |b| /CHROME/.match?(b) },
'dates' => user.sessions.map { |s| s['date'] }.sort.reverse
}
end

def work
file_lines = File.read('data.txt').split("\n")

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'
end
def work(file_path: 'data.txt')
current_user = nil
total_users_count = 0
total_sessions_count = 0
unique_browsers = Set.new

# Отчёт в json
# - Сколько всего юзеров +
Expand All @@ -70,108 +71,33 @@ 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['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
File.open('result.json', 'w') do |json|
@report = {}

# Собираем количество времени по пользователям
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.foreach(file_path, chomp: true) do |line|
cols = line.split(',')

# Выбираем самую длинную сессию пользователя
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
case cols[0]
when 'user'
collect_stats_from_users(current_user) if current_user
current_user = parse_user(cols)
total_users_count += 1
when 'session'
current_user.sessions << parse_session(cols)
unique_browsers << cols[3].upcase
total_sessions_count += 1
end
end

# Браузеры пользователя через запятую
collect_stats_from_users(report, users_objects) do |user|
{ 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') }
end
collect_stats_from_users(current_user) if current_user

# Хоть раз использовал IE?
collect_stats_from_users(report, users_objects) do |user|
{ 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } }
end
@report['totalUsers'] = total_users_count
@report['uniqueBrowsersCount'] = unique_browsers.count
@report['totalSessions'] = total_sessions_count
@report['allBrowsers'] = unique_browsers.sort.join(',')

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

File.write('result.json', "#{report.to_json}\n")
puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)
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
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
34 changes: 34 additions & 0 deletions test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require 'minitest/autorun'
require_relative 'task-2'

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
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
28 changes: 28 additions & 0 deletions test_data/generate_data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
string_count = 100000

File.open("data#{string_count}.txt", 'w+') do |file|
str = <<-HEREDOC
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
session,1,3,Internet Explorer 10,28,2017-05-29
session,1,4,Chrome 13,116,2016-11-28
HEREDOC

(string_count / 20).times { file.write(str) }
end
Loading