Skip to content

[Draft][HW-2] Add homework 2 baggage #104

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

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 @@
data_large.txt
data_50_000.txt
result.json
54 changes: 32 additions & 22 deletions case-study-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,54 @@
Я решил исправить эту проблему, оптимизировав эту программу.

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

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

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

Вот как я построил `feedback_loop`: *как вы построили feedback_loop*
Вот как я построил `feedback_loop`: замер -> изменения в точках роста -> проверка работоспособности -> замер

## Вникаем в детали системы, чтобы найти главные точки роста
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались*
Для того, чтобы найти "точки роста" для оптимизации я воспользовался профайлерами памяти, в первую очередь гемом memory_profiler, файлом с 50 000 строк

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

### Ваша находка №1
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика
- гем memory_profiler показал строчку `sessions = sessions + [parse_session(line)] if cols[0] == 'session'`
- убрал клонирование массива и начал использовать оператор <<
- метроика улучшилась с 8 ГБ до 3.5 ГБ
- теперь показывает другую строку

### Ваша находка №2
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика

### Ваша находка №X
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика
- гем memory_profiler показал строчку `user_sessions = sessions.select { |session| session['user_id'] == user['id'] }`
- использовать ту же оптимизацию что использовал в прошлом задании
- метроика улучшилась с 3.5 ГБ до 772 МБ
- теперь показывает другую строку

### Ваша находка №3
- гем memory_profiler показал строчку `users = users + [parse_user(line)] if cols[0] == 'user'`
- так же как и в первой находке
- метроика улучшилась с 772 МБ до 517 МБ
- теперь показывает другую строку

### Ваша находка №3
- гем memory_profiler показал строчку `{ 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } }`
- использовать ту же оптимизацию что использовал в прошлом задании
- метроика улучшилась с 517 МБ до 295 МБ
- теперь показывает другую строку

### Ваша находка №4
- прочитал в задании что программу надо переписать в потоковом стиле
- попробовал переписать читая по строчке за раз
- метроика улучшилась с 295 МБ до 35 МБ
- теперь показывает другую строку

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

*Какими ещё результами можете поделиться*
Удалось улучшить метрику системы с более 8000+ мегабайт с оригинальным кодом и файлом данными на 50000 строк до 35 МБ с исправленым кодом и файлом data_large.txt и уложиться в заданный бюджет.

## Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали*
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написал тест соответствующий требованию бюджета
Binary file removed data_large.txt.gz
Binary file not shown.
8 changes: 8 additions & 0 deletions memory_profiler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require_relative 'task-2'
require 'memory_profiler'

report = MemoryProfiler.report do
work('data_50_000.txt')
end

report.pretty_print(scale_bytes: true)
13 changes: 13 additions & 0 deletions performance_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require_relative 'task-2'
require 'rspec'
require 'rspec-benchmark'

RSpec.configure do |config|
config.include RSpec::Benchmark::Matchers
end

describe 'memory limits' do
it 'limits memory usage' do
expect(work('data_large.txt')).to be <= 70
end
end
227 changes: 90 additions & 137 deletions task-2.rb
Original file line number Diff line number Diff line change
@@ -1,59 +1,80 @@
# Deoptimized version of homework task

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

class User
attr_reader :attributes, :sessions
def append_to_file_json_start
@report_file.write("{\"usersStats\":{")
end

def initialize(attributes:, sessions:)
@attributes = attributes
@sessions = sessions
end
def user_data?(data_array)
data_array[0] == 'user'
end

def parse_user(user)
fields = user.split(',')
parsed_result = {
'id' => fields[1],
'first_name' => fields[2],
'last_name' => fields[3],
'age' => fields[4],
def parse_new_user(data_array)
@report[:totalUsers] += 1
@user = {
id: data_array[1],
name: "#{data_array[2]} #{data_array[3]}",
sessions: []
}
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],
def parse_session(data_array)
@report[:totalSessions] += 1
@user_sessions << {
browser: data_array[3].upcase,
time: data_array[4].to_i,
date: data_array[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
def append_previous_user
@user_data = { @user[:name] => collect_statisticss_from_sessions }
@report_file.write("#{@user_data.to_json[1..-2]}", ',')
@user_sessions = []
@user_data = {}
end

def append_last_user
@user_data = { @user[:name] => collect_statisticss_from_sessions }
@report_file.write("#{@user_data.to_json[1..-2]}", '}')
end

def work
file_lines = File.read('data.txt').split("\n")
def sessions_data_present?
!@user_sessions.empty?
end

users = []
sessions = []
def append_total_data
@report[:allBrowsers] = @report[:allBrowsers].sort!.join(',')
@report_file.write(',', "#{@report.to_json[1..-1]}")
end

def work(filename = 'data_large.txt')
file = File.open(filename)

@user_sessions = []
@user = {}
@report_file = File.open('result.json', 'a')
@report = {
totalUsers: 0,
uniqueBrowsersCount: 0,
totalSessions: 0,
allBrowsers: []
}

file_lines.each do |line|
append_to_file_json_start
file.each_line(chomp: true) do |line|
cols = line.split(',')
users = users + [parse_user(line)] if cols[0] == 'user'
sessions = sessions + [parse_session(line)] if cols[0] == 'session'
if user_data?(cols) && sessions_data_present?
append_previous_user
parse_new_user(cols)
elsif user_data?(cols)
parse_new_user(cols)
else
parse_session(cols)
end
end
append_last_user

# Отчёт в json
# - Сколько всего юзеров +
Expand All @@ -70,108 +91,40 @@ 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
append_total_data
@report_file.close

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

File.write('result.json', "#{report.to_json}\n")
puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)
`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'))
def collect_statisticss_from_sessions
result = {
sessionsCount: 0,
totalTime: 0,
longestSession: 0,
browsers: [],
usedIE: false,
alwaysUsedChrome: false,
dates: []
}
@user_sessions.each do |session|
time = session[:time]
result[:totalTime] += time
result[:longestSession] = time if time > result[:longestSession]
browser = session[:browser]
unless @report[:allBrowsers].include?(browser)
@report[:allBrowsers] << browser
@report[:uniqueBrowsersCount] += 1
end
result[:browsers] << browser
result[:usedIE] = true if !(browser =~ /INTERNET EXPLORER/).nil?
result[:alwaysUsedChrome] = !(browser =~ /CHROME/).nil? && (result[:sessionsCount] == 0 || result[:alwaysUsedChrome])
result[:dates] << session[:date]
result[:sessionsCount] += 1
end
result[:totalTime] = result[:totalTime].to_s + ' min.'
result[:longestSession] = result[:longestSession].to_s + ' min.'
result[:browsers] = result[:browsers].sort!.join(', ')
result[:dates].sort!.reverse!
result
end
Loading