diff --git a/asymptotics.rb b/asymptotics.rb new file mode 100644 index 0000000..d2c97ae --- /dev/null +++ b/asymptotics.rb @@ -0,0 +1,60 @@ +require './task-1' +require 'benchmark/ips' +require 'ruby-prof' +require 'fileutils' + +folder = ENV['STEP'] ? "metrics/optimization/#{ENV['STEP']}" : 'metrics/deoptimized' +desc = ENV['DESC'] + +FileUtils.mkdir_p folder + +# old_stdout = $stdout +# $stdout = StringIO.new +# Benchmark.ips do |bench| +# bench.warmup = 0 +# %w(80000 200000 1000000 2000000).each do |lines| +# bench.report("Process #{lines}") { work(lines) } +# bench.compare! +# end +# end + +# result = $stdout.string +# File.write("#{folder}/README.md", "#{desc}\n Benchmarks\n #{result}") +# $stdout = old_stdout + +# # CPU + +# GC.disable +# RubyProf.measure_mode = RubyProf::WALL_TIME +# result = RubyProf.profile do +# work('80000') +# end + +# # printer = RubyProf::FlatPrinter.new(result) +# # printer.print(File.open("#{folder}/ruby_prof_cpu_flat.txt", "w+")) + +# printer2 = RubyProf::GraphHtmlPrinter.new(result) +# printer2.print(File.open("#{folder}/ruby_prof_cpu_graph.html", "w+")) + +# # printer3 = RubyProf::CallStackPrinter.new(result) +# # printer3.print(File.open("#{folder}/ruby_prof_cpu_callstack.html", "w+")) + +# # printer4 = RubyProf::CallTreePrinter.new(result) +# # printer4.print(:path => "#{folder}/", :profile => 'callgrind') +# GC.enable + +# # Run rbspy +# `LINES=80000 GS_DISABLE=1 rbspy record ruby task-1.rb --file #{folder}/rbspy` + +#Memory +%w(80000 1000000 2000000).each do |lines| + next if lines == '80000' || lines == '2000000' + `LINES=#{lines} valgrind --tool=massif --massif-out-file="#{folder}/massif_#{lines}.out" 'ruby' task-1.rb` +end + +# RubyProf.measure_mode = RubyProf::ALLOCATIONS +# result = RubyProf.profile do +# work('80000') +# end +# printer = RubyProf::CallTreePrinter.new(result) +# printer.print(path: "#{folder}/", profile: 'callgrind') diff --git a/case-study-template.md b/case-study-template.md index e0eef00..30dd60c 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,35 +12,50 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: +Calculating ------------------------------------- + Process 2500 4.440 (± 0.0%) i/s - 23.000 in 5.181137s + Process 5000 1.295 (± 0.0%) i/s - 7.000 in 5.406855s + Process 10000 0.363 (± 0.0%) i/s - 2.000 in 5.512669s + Process 20000 0.069 (± 0.0%) i/s - 1.000 in 14.437365s + Process 40000 0.012 (± 0.0%) i/s - 1.000 in 81.002661s + +Comparison: + Process 2500: 4.4 i/s + Process 5000: 1.3 i/s - 3.43x slower + Process 10000: 0.4 i/s - 12.23x slower + Process 20000: 0.1 i/s - 64.11x slower + Process 40000: 0.0 i/s - 359.68x slower ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +Вот как я построил `feedback_loop`: создал из исходного файла с данными, файлы меньшим размером, ориентируясь на количество строк +`head -n N data_large.txt > dataN.txt` +Подготовил файл asymptotics.rb который автоматически собирает все метрики и делает некоторые замеры. ## Вникаем в детали системы, чтобы найти 20% точек роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался rubyprof (WALL_TIME, ALLOCATIONS), rbspy, valgrind Вот какие проблемы удалось найти и решить ### Ваша находка №1 -О вашей находке №1 - -### Ваша находка №2 -О вашей находке №2 - -### Ваша находка №X -О вашей находке №X +При увеличении количества обрабатываемых строк исходных данных в два раза, время выполнения программы увеличивается в пять раз, количество потребляемой памяти в четыре раза. +Было обнаружено, что на выполнение следующей строки кода уходит 93% времени +`user_sessions = sessions.select { |session| session['user_id'] == user['id'] }` +В этой строке происходит выборка сессий пользователя, после чего создается новый объект Пользователь с выбранными сессиями и остальными атрибутами. +Для оптимизации этой логики было принято решение избавится от объекта Пользователя в пользу Хэша, с аналогичными параметрами, которые можно собирать в момент обработки исходного файла. ## Результаты -В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* +Результат оптимизации в папке metrics/optimizations +Получилось избавиться от кратной регрессии - увеличение объема данных в два раза, в приблизительно в два раза увеличивает время исполнения программы и в два раза увеличивает потребляемый объем оперативной памяти -*Какими ещё результами можете поделиться* +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с того, что нельзя было дождаться исполнения скрипта, до того, что 1_000_000 строк обрабатывается за 36 секунд и потребляет 1Gb оперативной памяти +Так как параметры необходимой оптимизации не устанавливались, оптимизацию на этом прекратил. ## Защита от регресса производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавлен тест на производительность diff --git a/metrics/deoptimized/README.md b/metrics/deoptimized/README.md new file mode 100644 index 0000000..22a1631 --- /dev/null +++ b/metrics/deoptimized/README.md @@ -0,0 +1,16 @@ +Metrics of deoptimized version 20_000 lines +Benchmarks +Calculating ------------------------------------- + Process 2500 4.440 (± 0.0%) i/s - 23.000 in 5.181137s + Process 5000 1.295 (± 0.0%) i/s - 7.000 in 5.406855s + Process 10000 0.363 (± 0.0%) i/s - 2.000 in 5.512669s + Process 20000 0.069 (± 0.0%) i/s - 1.000 in 14.437365s + Process 40000 0.012 (± 0.0%) i/s - 1.000 in 81.002661s + +Comparison: + Process 2500: 4.4 i/s + Process 5000: 1.3 i/s - 3.43x slower + Process 10000: 0.4 i/s - 12.23x slower + Process 20000: 0.1 i/s - 64.11x slower + Process 40000: 0.0 i/s - 359.68x slower + diff --git a/metrics/deoptimized/callgrind.png b/metrics/deoptimized/callgrind.png new file mode 100644 index 0000000..e9a63e4 Binary files /dev/null and b/metrics/deoptimized/callgrind.png differ diff --git a/metrics/deoptimized/massif_10000.png b/metrics/deoptimized/massif_10000.png new file mode 100644 index 0000000..81156ca Binary files /dev/null and b/metrics/deoptimized/massif_10000.png differ diff --git a/metrics/deoptimized/massif_20000.png b/metrics/deoptimized/massif_20000.png new file mode 100644 index 0000000..9d80228 Binary files /dev/null and b/metrics/deoptimized/massif_20000.png differ diff --git a/metrics/deoptimized/massif_40000.png b/metrics/deoptimized/massif_40000.png new file mode 100644 index 0000000..4d324a7 Binary files /dev/null and b/metrics/deoptimized/massif_40000.png differ diff --git a/metrics/deoptimized/sources.tar.xz b/metrics/deoptimized/sources.tar.xz new file mode 100644 index 0000000..479a9f3 Binary files /dev/null and b/metrics/deoptimized/sources.tar.xz differ diff --git a/metrics/optimization/1/README.md b/metrics/optimization/1/README.md new file mode 100644 index 0000000..99c6f38 --- /dev/null +++ b/metrics/optimization/1/README.md @@ -0,0 +1,18 @@ +Change User object to Hash and prepare it during parsing of data file + Benchmarks +Calculating ------------------------------------- + Process 2500 15.172 (± 0.0%) i/s - 76.000 in 5.013664s + Process 5000 7.297 (± 0.0%) i/s - 37.000 in 5.073320s + Process 10000 3.434 (± 0.0%) i/s - 18.000 in 5.248135s + Process 20000 1.709 (± 0.0%) i/s - 9.000 in 5.271037s + Process 40000 0.837 (± 0.0%) i/s - 5.000 in 5.983866s + Process 80000 0.402 (± 0.0%) i/s - 3.000 in 7.485948s + +Comparison: + Process 2500: 15.2 i/s + Process 5000: 7.3 i/s - 2.08x slower + Process 10000: 3.4 i/s - 4.42x slower + Process 20000: 1.7 i/s - 8.88x slower + Process 40000: 0.8 i/s - 18.12x slower + Process 80000: 0.4 i/s - 37.70x slower + diff --git a/metrics/optimization/1/callgrind_allocations_80000.png b/metrics/optimization/1/callgrind_allocations_80000.png new file mode 100644 index 0000000..63d5a86 Binary files /dev/null and b/metrics/optimization/1/callgrind_allocations_80000.png differ diff --git a/metrics/optimization/1/massiv_1000000.png b/metrics/optimization/1/massiv_1000000.png new file mode 100644 index 0000000..95c07d0 Binary files /dev/null and b/metrics/optimization/1/massiv_1000000.png differ diff --git a/metrics/optimization/1/sources.tar.xz b/metrics/optimization/1/sources.tar.xz new file mode 100644 index 0000000..611439d Binary files /dev/null and b/metrics/optimization/1/sources.tar.xz differ diff --git a/task-1.rb b/task-1.rb index 778672d..57e8681 100644 --- a/task-1.rb +++ b/task-1.rb @@ -3,16 +3,7 @@ require 'json' require 'pry' require 'date' -require 'minitest/autorun' - -class User - attr_reader :attributes, :sessions - - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions - end -end +require 'ruby-progressbar' def parse_user(user) fields = user.split(',') @@ -36,23 +27,38 @@ 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']}" + users_objects.each do |k, user| + user_key = "#{user['first_name']}" + ' ' + "#{user['last_name']}" report['usersStats'][user_key] ||= {} report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) end end -def work - file_lines = File.read('data.txt').split("\n") +def work(lines='') + total = lines.empty? ? `wc -l data#{lines}.txt`.strip.to_i : lines.to_i + progressbar = ProgressBar.create(total: total, format: '%a, %J, %E, %B') + + file_lines = File.read("data#{lines}.txt").split("\n") - users = [] + 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' + if cols[0] == 'user' + attrs = parse_user(line) + users[attrs['id']] = attrs + users[attrs['id']][:sessions] = [] + end + + if cols[0] == 'session' + session = parse_session(line) + user = users[session['user_id']] + user[:sessions] << session + sessions << session + end + + progressbar.increment end # Отчёт в json @@ -94,83 +100,51 @@ def work .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 + users_objects = users report['usersStats'] = {} # Собираем количество сессий по пользователям collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } + { '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.' } + { '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.' } + { '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(', ') } + { '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/ } } + { '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/ } } + { '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 } } + { '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") 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 = '{"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"]}}}' + "\n" - assert_equal expected_result, File.read('result.json') - end +if ENV['LINES'] + GC.disable if ENV['GC_DISABLE'] + start_time = Time.now + work(ENV['LINES']) + puts "time in sec: #{Time.now - start_time}" end diff --git a/test.rb b/test.rb new file mode 100644 index 0000000..b061e4f --- /dev/null +++ b/test.rb @@ -0,0 +1,41 @@ +require './task-1' +require 'minitest/autorun' + +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 = '{"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"]}}}' + "\n" + assert_equal expected_result, File.read('result.json') + end + + def test_performance + `head -n 1000000 data_large.txt > data1000000.txt` unless File.file?('data1000000.txt') + start_time = Time.now + work '1000000' + assert (Time.now - start_time) < 60 + end +end