-
Notifications
You must be signed in to change notification settings - Fork 131
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
base: master
Are you sure you want to change the base?
dz2: memory #93
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,3 @@ | ||
.ruby-version | ||
/tmp | ||
/test_data/*.txt |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# Case-study оптимизации | ||
|
||
## Актуальная проблема | ||
В нашем проекте возникла серьёзная проблема. | ||
|
||
Необходимо было обработать файл с данными, чуть больше ста мегабайт. | ||
|
||
У нас уже была программа на `ruby`, которая умела делать нужную обработку. | ||
|
||
Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. | ||
|
||
Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
||
## Формирование метрики | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: тестовый файл на 5_000 строк, до оптимизации объем выделяемой памяти равен 2759Мб(по данным memory_profiler) | ||
|
||
## Гарантия корректности работы оптимизированной программы | ||
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
||
## Feedback-Loop | ||
Выполняю профилирование на набольшом объеме данных, оптимизирую код, проверяю на большем кол-ве данных и так по кругу, пока не добьюсь нужного результата | ||
|
||
## Вникаем в детали системы, чтобы найти главные точки роста | ||
Для того, чтобы найти "точки роста" для оптимизации я воспользовался memory_profiler, ruby-prof, stackprof | ||
|
||
Вот какие проблемы удалось найти и решить | ||
|
||
### При парсинге даты выделяется большое кол-во памяти и создается много объектов | ||
- memory profiler и ruby prof | ||
- Сократил парсинг даты до 1 вызова map | ||
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. парсинг вообще не нужен |
||
- Объем потребляемой памяти сократился до 2074Мб | ||
- Парсинг даты по прежнему потребляет много памяти, но на данном этапе считаю эту точку роста оптимизированной. | ||
|
||
### Добавление информации о браузерах пользователя в сессии | ||
- memory profiler | ||
- Отрефакторить строку | ||
- Потребление памяти незначительно сократилось до 2057Мб | ||
- По количеству выделяемой памяти данная строка опустилась на 3 место, на данный момент считаем метод оптимизированным | ||
|
||
### Добавление объектов в массив через сложение массивов | ||
- memory profiler | ||
- Изменил добавление элемента в массив через оператор << | ||
- Объем потребляемой памяти сократился до 1985Мб | ||
- На данном этапе добавление элементов в массив не является точкой роста | ||
|
||
### Select из массива сессий через сравнение с id пользователя | ||
- memory profiler | ||
- Сделаю из массива сессий хеш, а так же в блоке изменю добваление в массив через оператор << | ||
- Объем потребляемой памяти сократился до 1971Мб | ||
- На данном этапе добавление элементов в массив не является точкой роста | ||
|
||
### Большое количество памяти выделяется при добавлении информации о браузерах(эксплорер, хром) | ||
- memory profiler | ||
- В целом отрефакторю код | ||
- Скорость программы увеличилась, объем выделяемой памяти уменьшился | ||
- На данном этапе добавление элементов в массив не является точкой роста | ||
|
||
## Результаты | ||
В результате рефакторинга уменишьлось кол-во потребляемой памяти, а так же увеличилась скорость выполнения программы |
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. Что-то пошло не так У вас похоже программа не загрузилась даже нормально Нормальная картина в данном случае - программа грузится и быстро выходит на плато где-то в 40 МБ на всём протяжении работы секунд в 20 |
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 |
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 | ||
|
||
|
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 |
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)) |
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], | ||
|
@@ -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.', | ||
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. 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 | ||
# - Сколько всего юзеров + | ||
|
@@ -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 |
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 |
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 |
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.
Как-то многовато для такого маленького файла конечно