diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f0b32554 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +data_large.txt +data_50_000.txt +result.json \ No newline at end of file diff --git a/case-study-template.md b/case-study-template.md index c3279664..892c93e5 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -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-тестах, которые вы написали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написал тест соответствующий требованию бюджета \ No newline at end of file diff --git a/data_large.txt.gz b/data_large.txt.gz deleted file mode 100644 index 91c7e45e..00000000 Binary files a/data_large.txt.gz and /dev/null differ diff --git a/memory_profiler.rb b/memory_profiler.rb new file mode 100644 index 00000000..3672a5c8 --- /dev/null +++ b/memory_profiler.rb @@ -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) \ No newline at end of file diff --git a/performance_spec.rb b/performance_spec.rb new file mode 100644 index 00000000..64f7d34b --- /dev/null +++ b/performance_spec.rb @@ -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 \ No newline at end of file diff --git a/task-2.rb b/task-2.rb index 34e09a3c..0f427ba7 100644 --- a/task-2.rb +++ b/task-2.rb @@ -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 # - Сколько всего юзеров + @@ -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 diff --git a/test.rb b/test.rb new file mode 100644 index 00000000..b07b1712 --- /dev/null +++ b/test.rb @@ -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