From b0524de17eac27f0f2be1c74f4ad2bc0e78e16f3 Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Sun, 10 Mar 2019 13:02:52 +0300 Subject: [PATCH 01/12] Test and task separation --- task-1.rb | 33 --------------------------------- task_test.rb | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 33 deletions(-) create mode 100644 task_test.rb diff --git a/task-1.rb b/task-1.rb index 778672d..2746d23 100644 --- a/task-1.rb +++ b/task-1.rb @@ -3,7 +3,6 @@ require 'json' require 'pry' require 'date' -require 'minitest/autorun' class User attr_reader :attributes, :sessions @@ -142,35 +141,3 @@ def work 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 -end diff --git a/task_test.rb b/task_test.rb new file mode 100644 index 0000000..fa3d04d --- /dev/null +++ b/task_test.rb @@ -0,0 +1,34 @@ +require 'minitest/autorun' +require './task-1' + +class TaskTest < 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 +end From 8c4157d4b9b45dc9c2b0402831040cb273d2d6f2 Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Sun, 10 Mar 2019 20:17:33 +0300 Subject: [PATCH 02/12] Add metrics --- metrics.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 metrics.rb diff --git a/metrics.rb b/metrics.rb new file mode 100644 index 0000000..391ecc5 --- /dev/null +++ b/metrics.rb @@ -0,0 +1,22 @@ +require 'benchmark' +require './task-1' + +arr = [] + +from = 10_000 +to = 15_000 +step = 1000 + +(from..to).step(step) do |lines_num| + system "zcat data_large.txt.gz | head -n #{lines_num} > data.txt" + + time = Benchmark.realtime { work }.round(2) + + puts "[#{lines_num}/#{to}] performed in #{time} s." + + arr << time.round(2) +end + +avg = arr.reduce(:+) / arr.size + +puts "Average time between #{from} and #{to} lines with step #{step}:\n#{avg}" From e52bae68aa78ffffea585a8175e3bdab4d59b58b Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Sun, 10 Mar 2019 22:06:15 +0300 Subject: [PATCH 03/12] Add profilers --- profilers/gc_stat.rb | 5 +++++ profilers/memory_profiler.rb | 8 ++++++++ profilers/memory_usage.rb | 9 +++++++++ profilers/object_space_objects.rb | 11 +++++++++++ profilers/realtime.rb | 8 ++++++++ profilers/rubyprof.rb | 18 ++++++++++++++++++ profilers/stackprof.rb | 20 ++++++++++++++++++++ 7 files changed, 79 insertions(+) create mode 100644 profilers/gc_stat.rb create mode 100644 profilers/memory_profiler.rb create mode 100644 profilers/memory_usage.rb create mode 100644 profilers/object_space_objects.rb create mode 100644 profilers/realtime.rb create mode 100644 profilers/rubyprof.rb create mode 100644 profilers/stackprof.rb diff --git a/profilers/gc_stat.rb b/profilers/gc_stat.rb new file mode 100644 index 0000000..4eb2436 --- /dev/null +++ b/profilers/gc_stat.rb @@ -0,0 +1,5 @@ +require_relative '../task-1' + +puts "old GC stat:\n #{GC.stat}" +work +puts "new GC stat:\n #{GC.stat}" diff --git a/profilers/memory_profiler.rb b/profilers/memory_profiler.rb new file mode 100644 index 0000000..e1a83f2 --- /dev/null +++ b/profilers/memory_profiler.rb @@ -0,0 +1,8 @@ +require 'memory_profiler' +require_relative '../task-1.rb' + +report = MemoryProfiler.report(trace: [String]) do + work +end + +report.pretty_print(scale_bytes: true) diff --git a/profilers/memory_usage.rb b/profilers/memory_usage.rb new file mode 100644 index 0000000..d36cda0 --- /dev/null +++ b/profilers/memory_usage.rb @@ -0,0 +1,9 @@ +require_relative '../task-1.rb' + +def print_memory_usage + "%d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) +end + +puts "rss before: #{print_memory_usage}" +work +puts "rss after: #{print_memory_usage}" diff --git a/profilers/object_space_objects.rb b/profilers/object_space_objects.rb new file mode 100644 index 0000000..ec2b4f5 --- /dev/null +++ b/profilers/object_space_objects.rb @@ -0,0 +1,11 @@ +require_relative '../task-1' + +def print_object_space_delta(before, after) + puts "TOTAL: #{after[:TOTAL] - before[:TOTAL]}" + puts "T_STRING: #{after[:T_STRING] - before[:T_STRING]}" + puts "T_ARRAY: #{after[:T_ARRAY] - before[:T_ARRAY]}" +end + +object_space_before = ObjectSpace.count_objects +work +print_object_space_delta(object_space_before, ObjectSpace.count_objects) diff --git a/profilers/realtime.rb b/profilers/realtime.rb new file mode 100644 index 0000000..28ef573 --- /dev/null +++ b/profilers/realtime.rb @@ -0,0 +1,8 @@ +require 'benchmark' +require_relative '../task-1' + +time = Benchmark.realtime do + work +end + +puts "Finish in #{time.round(2)}" diff --git a/profilers/rubyprof.rb b/profilers/rubyprof.rb new file mode 100644 index 0000000..1d4b19e --- /dev/null +++ b/profilers/rubyprof.rb @@ -0,0 +1,18 @@ +require 'ruby-prof' +require_relative '../task-1' + +def profile(mode:) + puts "*** Measure mode #{mode} ***" + + RubyProf.measure_mode = Object.const_get("RubyProf::#{mode.upcase}") + + result = RubyProf.profile do + work + end + + printer = RubyProf::FlatPrinter.new(result) + printer.print(STDOUT) +end + +profile(mode: :allocations) +profile(mode: :wall_time) diff --git a/profilers/stackprof.rb b/profilers/stackprof.rb new file mode 100644 index 0000000..d05001c --- /dev/null +++ b/profilers/stackprof.rb @@ -0,0 +1,20 @@ +require 'stackprof' +require_relative '../task-1' + +def profile(mode:) + dump_file = "/tmp/stackprof_#{mode}.dump" + + StackProf.run(mode: mode, out: dump_file, raw: true) do + work + end + + puts "*** Stackprof #{mode} mode ***" + system "stackprof #{dump_file} --text --limit 3" + puts '=== Object#work ===' + system %(stackprof #{dump_file} --method 'Object#work') + puts '=== Object#collect_stats_from_users ===' + system %(stackprof #{dump_file} --method 'Object#collect_stats_from_users') +end + +profile(mode: :object) +profile(mode: :wall) From d5f6dce5ababfc68b6e523ce92130263e2da7fc8 Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Sun, 10 Mar 2019 22:29:44 +0300 Subject: [PATCH 04/12] Improve metrics --- metrics.rb | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/metrics.rb b/metrics.rb index 391ecc5..4638f97 100644 --- a/metrics.rb +++ b/metrics.rb @@ -1,22 +1,41 @@ require 'benchmark' require './task-1' -arr = [] +def allocated_memory + `ps -o rss= -p #{Process.pid}`.to_i / 1024 +end -from = 10_000 -to = 15_000 +from = 50_000 +to = 55_000 step = 1000 +times = [] +allocations = [] + (from..to).step(step) do |lines_num| system "zcat data_large.txt.gz | head -n #{lines_num} > data.txt" - time = Benchmark.realtime { work }.round(2) + time = Benchmark.realtime do + memory = allocated_memory + work + allocations << allocated_memory - memory + end.round(2) + + puts "#{lines_num} lines performed in #{time} s. + #{allocations.last}MB" + + times << time.round(2) +end + +deltas = [] - puts "[#{lines_num}/#{to}] performed in #{time} s." +times.each_index do |i| + break if times[i.next].nil? - arr << time.round(2) + deltas << times[i.next] - times[i] end -avg = arr.reduce(:+) / arr.size +avg_delta = deltas.reduce(:+) / deltas.size +mem_delta = (allocations.reduce(:+) / allocations.size.to_f).round(2) -puts "Average time between #{from} and #{to} lines with step #{step}:\n#{avg}" +puts "Average period for each #{step} lines: #{avg_delta}s." +puts "Average memory allocation for each #{step} lines: #{mem_delta}MB" From a90c8d4442c5fb14f0e26f00a35e2aa8bb77066c Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Sun, 10 Mar 2019 23:56:58 +0300 Subject: [PATCH 05/12] Improve task performance --- task-1.rb | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/task-1.rb b/task-1.rb index 2746d23..745b136 100644 --- a/task-1.rb +++ b/task-1.rb @@ -1,9 +1,11 @@ -# Deoptimized version of homework task +# frozen_string_literal: true require 'json' require 'pry' require 'date' +FIRST_COLUMN_REGEXP = /^\w+(?=,)/.freeze + class User attr_reader :attributes, :sessions @@ -13,9 +15,8 @@ def initialize(attributes:, sessions:) end end -def parse_user(user) - fields = user.split(',') - parsed_result = { +def parse_user(fields) + { 'id' => fields[1], 'first_name' => fields[2], 'last_name' => fields[3], @@ -23,9 +24,8 @@ def parse_user(user) } 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], @@ -48,10 +48,12 @@ def work users = [] sessions = [] + # File.open('data.txt', 'r').each do |line| + 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' + users << parse_user(cols) if cols[0] == 'user' + sessions << parse_session(cols) if cols[0] == 'session' end # Отчёт в json @@ -77,29 +79,26 @@ def work uniqueBrowsers = [] sessions.each do |session| browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } + next if uniqueBrowsers.include?(browser) + + uniqueBrowsers << browser end report['uniqueBrowsersCount'] = uniqueBrowsers.count - report['totalSessions'] = sessions.count - - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') + report['allBrowsers'] = uniqueBrowsers.map(&:upcase).sort.join(',') # Статистика по пользователям users_objects = [] + grouped_sessions_by_user_id = sessions.group_by do |session| + session['user_id'] + end + 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] + user_sessions = grouped_sessions_by_user_id[user['id']] + user_object = User.new(attributes: user, sessions: Array(user_sessions)) + users_objects << user_object end report['usersStats'] = {} From c214af9e33c980db3b90fab373585bdba9cc3d3c Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Sun, 10 Mar 2019 23:57:31 +0300 Subject: [PATCH 06/12] Add info to case study --- case-study-template.md | 92 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 9 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index e0eef00..4a0e653 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -12,33 +12,107 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: + +Среднее время выполнения и кол-во аллоцируемой памяти каждых 1000 строк для периода от 10 до 15 тыс. строк. ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за одну минуту. + +Вот как я построил `feedback_loop`: -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +Внесение изменений в код - Прогон тестов - Проверка улучшения метрик - Коммит изменений ## Вникаем в детали системы, чтобы найти 20% точек роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Для того, чтобы найти "точки роста" для оптимизации я воспользовался инструментами: + +* `GC.stat` +* `MemoryProfiler` +* `ps` +* `ObjectSpace.count_objects` +* `StackProf` +* `RubyProf` Вот какие проблемы удалось найти и решить ### Ваша находка №1 -О вашей находке №1 +MemoryProfiler + +Аллоцируется 400 Mb объектов класса Array и 16 Mb класса String. +Более всего памяти для класса Array выделяется в строках 53, 54, 100, 102. +Более всего памяти для класса String выделяется в строках 39,46, 52, 139, 142. +Аллоцируются одинаковые строки `" ", "session", ",", "user"` ### Ваша находка №2 -О вашей находке №2 +StackProf + +Object mode + +207 000 callees of `file_lines.each` +90 000 callees of `split` + +Object#collect_stats_from_users +250 000 callees of `users_objects.each` +190 000 callees of `report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user))` + +Wall mode +``` + 5822 (93.1%) | 98 | users.each do |user| + | 99 | attributes = user + 11584 (185.2%) / 5792 (92.6%) | 100 | user_sessions = sessions.select { |session| session['user_id'] == user['id'] } + ``` + + + +### Ваша находка №3 +RubyProf + +Allocations mode +``` + %self total self wait child calls name + 27.38 146929.000 146929.000 0.000 0.000 20001 String#split + 24.28 489625.000 130272.000 0.000 359353.000 10010 *Array#each + 14.20 93120.000 76192.000 0.000 16928.000 8464 #parse + ``` + +Wall mode +``` + %self total self wait child calls name + 91.03 7.694 7.694 0.000 0.000 1536 Array#select + 2.52 8.392 0.213 0.000 8.179 10010 *Array#each +``` -### Ваша находка №X -О вашей находке №X ## Результаты В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* +Удалось улучшить метрику системы + +Was +``` +10000 lines performed in 5.98 s. + 74MB +11000 lines performed in 7.97 s. + 52MB +12000 lines performed in 9.24 s. + 63MB +13000 lines performed in 11.27 s. + -12MB +14000 lines performed in 14.17 s. + 12MB +15000 lines performed in 16.11 s. + 12MB +Average period for each 1000 lines: 2.026s. +Average memory allocation for each 1000 lines: 33.5MB +``` + +Now +``` +10000 lines performed in 0.25 s. + 14MB +11000 lines performed in 0.34 s. + 1MB +12000 lines performed in 0.4 s. + 1MB +13000 lines performed in 0.41 s. + 5MB +14000 lines performed in 0.46 s. + 2MB +15000 lines performed in 0.48 s. + 1MB +Average period for each 1000 lines: 0.046s. +Average memory allocation for each 1000 lines: 4.0MB +``` *Какими ещё результами можете поделиться* From 062c67a313c4f4c10c41619d50d965b141941c4e Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Mon, 11 Mar 2019 00:57:58 +0300 Subject: [PATCH 07/12] wip --- case-study-template.md | 28 +++++++++++++++++++++++++++- task-1.rb | 19 ++++++++++--------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/case-study-template.md b/case-study-template.md index 4a0e653..4aba64d 100644 --- a/case-study-template.md +++ b/case-study-template.md @@ -90,6 +90,7 @@ Wall mode В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы +### Step 1 Was ``` 10000 lines performed in 5.98 s. + 74MB @@ -102,7 +103,7 @@ Average period for each 1000 lines: 2.026s. Average memory allocation for each 1000 lines: 33.5MB ``` -Now +Became ``` 10000 lines performed in 0.25 s. + 14MB 11000 lines performed in 0.34 s. + 1MB @@ -114,6 +115,31 @@ Average period for each 1000 lines: 0.046s. Average memory allocation for each 1000 lines: 4.0MB ``` +### Step 2 +Was +``` +50000 lines performed in 1.26 s. + 68MB +51000 lines performed in 1.45 s. + 6MB +52000 lines performed in 1.52 s. + 1MB +53000 lines performed in 1.58 s. + 0MB +54000 lines performed in 1.85 s. + -2MB +55000 lines performed in 1.58 s. + 64MB +Average period for each 1000 lines: 0.06400000000000002s. +Average memory allocation for each 1000 lines: 22.83MB +``` + +Became +``` +50000 lines performed in 1.09 s. + 66MB +51000 lines performed in 1.28 s. + 6MB +52000 lines performed in 1.37 s. + 2MB +53000 lines performed in 1.4 s. + 1MB +54000 lines performed in 1.52 s. + 0MB +55000 lines performed in 1.54 s. + 0MB +Average period for each 1000 lines: 0.09s. +Average memory allocation for each 1000 lines: 12.5MB +``` + *Какими ещё результами можете поделиться* ## Защита от регресса производительности diff --git a/task-1.rb b/task-1.rb index 745b136..740e6c0 100644 --- a/task-1.rb +++ b/task-1.rb @@ -4,7 +4,8 @@ require 'pry' require 'date' -FIRST_COLUMN_REGEXP = /^\w+(?=,)/.freeze +IE_REGEX = /INTERNET EXPLORER/i.freeze +CHROME_REGEX = /CHROME/i.freeze class User attr_reader :attributes, :sessions @@ -34,11 +35,11 @@ def parse_session(fields) } end -def collect_stats_from_users(report, users_objects, &block) +def collect_stats_from_users(report, users_objects) 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)) + report['usersStats'][user_key].merge!(yield(user)) end end @@ -110,32 +111,32 @@ def work # Собираем количество времени по пользователям 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.sum { |s| s['time'].to_i }} 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.max { |s| s['time'].to_i }} 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'].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.any? { |s| s['browser'] =~ IE_REGEX } } 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.all? { |s| s['browser'] =~ CHROME_REGEX } } 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| Date.parse(s['date'])}.sort.reverse_each.with_object([]) { |d, obj| obj << d.iso8601 } } end File.write('result.json', "#{report.to_json}\n") From bc542fc7ea26c9d42f98badcdc94d2f83c05d209 Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Mon, 11 Mar 2019 12:02:38 +0300 Subject: [PATCH 08/12] Make metrics MacOS friendly --- metrics.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/metrics.rb b/metrics.rb index 4638f97..c2e54d1 100644 --- a/metrics.rb +++ b/metrics.rb @@ -5,6 +5,10 @@ def allocated_memory `ps -o rss= -p #{Process.pid}`.to_i / 1024 end +def mac_os? + RUBY_PLATFORM.match?(/darwin/) +end + from = 50_000 to = 55_000 step = 1000 @@ -13,7 +17,12 @@ def allocated_memory allocations = [] (from..to).step(step) do |lines_num| - system "zcat data_large.txt.gz | head -n #{lines_num} > data.txt" + + if mac_os? + system "zcat < data_large.txt.gz | head -n #{lines_num} > data.txt" + else + system "zcat data_large.txt.gz | head -n #{lines_num} > data.txt" + end time = Benchmark.realtime do memory = allocated_memory From f83f3a322d90e4099c957a40867e8ddb97bb65a4 Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Mon, 11 Mar 2019 12:13:26 +0300 Subject: [PATCH 09/12] Fix test --- task-1.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/task-1.rb b/task-1.rb index 740e6c0..82a232c 100644 --- a/task-1.rb +++ b/task-1.rb @@ -116,7 +116,8 @@ def work # Выбираем самую длинную сессию пользователя collect_stats_from_users(report, users_objects) do |user| - { 'longestSession' => "#{user.sessions.max { |s| s['time'].to_i }} min." } + longest_session = user.sessions.max { |a,b| a['time'].to_i <=> b['time'].to_i } + { 'longestSession' => "#{longest_session['time']} min." } end # Браузеры пользователя через запятую From 984b22fabaf10e7d28a984d3527fbd59c839cd18 Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Mon, 11 Mar 2019 17:57:02 +0300 Subject: [PATCH 10/12] More optimization --- metrics.rb | 4 +-- task-1.rb | 80 ++++++++++++++++++------------------------------------ 2 files changed, 29 insertions(+), 55 deletions(-) diff --git a/metrics.rb b/metrics.rb index c2e54d1..2dc7ae2 100644 --- a/metrics.rb +++ b/metrics.rb @@ -9,8 +9,8 @@ def mac_os? RUBY_PLATFORM.match?(/darwin/) end -from = 50_000 -to = 55_000 +from = 550_000 +to = 551_000 step = 1000 times = [] diff --git a/task-1.rb b/task-1.rb index 82a232c..882080f 100644 --- a/task-1.rb +++ b/task-1.rb @@ -5,7 +5,7 @@ require 'date' IE_REGEX = /INTERNET EXPLORER/i.freeze -CHROME_REGEX = /CHROME/i.freeze +NOT_CHROME_REGEX = /(? fields[1], 'first_name' => fields[2], 'last_name' => fields[3], - 'age' => fields[4], + 'age' => fields[4] } end @@ -31,30 +31,18 @@ def parse_session(fields) 'session_id' => fields[2], 'browser' => fields[3], 'time' => fields[4], - 'date' => fields[5], + 'date' => fields[5] } end -def collect_stats_from_users(report, users_objects) - users_objects.each do |user| - user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" - report['usersStats'][user_key] ||= {} - report['usersStats'][user_key].merge!(yield(user)) - end -end - def work - file_lines = File.read('data.txt').split("\n") - users = [] sessions = [] - # File.open('data.txt', 'r').each do |line| - - file_lines.each do |line| + File.open('data.txt', 'r').each do |line| cols = line.split(',') - users << parse_user(cols) if cols[0] == 'user' - sessions << parse_session(cols) if cols[0] == 'session' + users << parse_user(cols) if line.start_with?('user') + sessions << parse_session(cols) if line.start_with?('session') end # Отчёт в json @@ -74,7 +62,7 @@ def work report = {} - report[:totalUsers] = users.count + report['totalUsers'] = users.count # Подсчёт количества уникальных браузеров uniqueBrowsers = [] @@ -102,42 +90,28 @@ def work users_objects << user_object end - 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.sum { |s| s['time'].to_i }} min." } - end + report['usersStats'] = users_objects.each.with_object({}) do |user, hash| + user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" - # Выбираем самую длинную сессию пользователя - collect_stats_from_users(report, users_objects) do |user| longest_session = user.sessions.max { |a,b| a['time'].to_i <=> b['time'].to_i } - { 'longestSession' => "#{longest_session['time']} min." } - end - - # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser'].upcase }.sort.join(', ') } # ??? - end - - # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.any? { |s| s['browser'] =~ IE_REGEX } } - end - - # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.all? { |s| s['browser'] =~ CHROME_REGEX } } - end - - # Даты сессий через запятую в обратном порядке в формате iso8601 - collect_stats_from_users(report, users_objects) do |user| - { 'dates' => user.sessions.map{|s| Date.parse(s['date'])}.sort.reverse_each.with_object([]) { |d, obj| obj << d.iso8601 } } + user_browsers = user.sessions.map {|s| s['browser'].upcase }.sort.join(', ') + + hash[user_key] = { + # Собираем количество сессий по пользователям + 'sessionsCount' => user.sessions.count, + # Собираем количество времени по пользователям + 'totalTime' => "#{user.sessions.sum { |s| s['time'].to_i }} min.", + # Выбираем самую длинную сессию пользователя + 'longestSession' => "#{longest_session['time']} min.", + # Браузеры пользователя через запятую + 'browsers' => user_browsers, + # Хоть раз использовал IE? + 'usedIE' => user_browsers.match?(IE_REGEX), + # Всегда использовал только Chrome? + 'alwaysUsedChrome' => !user_browsers.match?(NOT_CHROME_REGEX), + # Даты сессий через запятую в обратном порядке в формате iso8601 + 'dates' => user.sessions.map { |s| Date.iso8601(s['date']) }.sort.reverse_each.with_object([]) { |d, arr| arr << d } + } end File.write('result.json', "#{report.to_json}\n") From 6f2374aab60465a6e88bf4f8d8d6802702c057f7 Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Mon, 11 Mar 2019 23:36:21 +0300 Subject: [PATCH 11/12] update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a4c835 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +data.txt +result.json From 18312905fca9e99f53149508a66337d3f05c556d Mon Sep 17 00:00:00 2001 From: Stanislav Kravchenko Date: Tue, 12 Mar 2019 00:37:32 +0300 Subject: [PATCH 12/12] finalization --- bench_test.rb | 20 +++++++ case-study-template.md => case-study.md | 22 +++++++- metrics.rb | 4 +- task-1.rb | 74 ++++++++++++++----------- 4 files changed, 84 insertions(+), 36 deletions(-) create mode 100644 bench_test.rb rename case-study-template.md => case-study.md (91%) diff --git a/bench_test.rb b/bench_test.rb new file mode 100644 index 0000000..6662bcd --- /dev/null +++ b/bench_test.rb @@ -0,0 +1,20 @@ +require 'minitest/benchmark' +require 'minitest/autorun' +require './task-1' + +class BenchTest < MiniTest::Benchmark + def self.bench_range + [1_000, 10_000, 100_000] + end + + def bench_algorithm + assert_performance_linear 0.9999 do |n| + algorithm(n) + end + end + + def algorithm(lines_num) + system "zcat data_large.txt.gz | head -n #{lines_num} > data.txt" + work + end +end diff --git a/case-study-template.md b/case-study.md similarity index 91% rename from case-study-template.md rename to case-study.md index 4aba64d..e660eec 100644 --- a/case-study-template.md +++ b/case-study.md @@ -14,7 +14,7 @@ ## Формирование метрики Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: -Среднее время выполнения и кол-во аллоцируемой памяти каждых 1000 строк для периода от 10 до 15 тыс. строк. +Среднее время выполнения и кол-во аллоцируемой памяти каждых 1000 строк для периода от 10 до 15 тыс. строк и других периодов. ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. @@ -140,7 +140,23 @@ Average period for each 1000 lines: 0.09s. Average memory allocation for each 1000 lines: 12.5MB ``` -*Какими ещё результами можете поделиться* +### Final + +``` +50000 lines performed in 0.79 s. + 53MB +51000 lines performed in 0.95 s. + 11MB +52000 lines performed in 0.92 s. + 11MB +53000 lines performed in 0.97 s. + 1MB +54000 lines performed in 1.05 s. + 2MB +55000 lines performed in 0.98 s. + 23MB +Average period for each 1000 lines: 0.03799999999999999s. +Average memory allocation for each 1000 lines: 16.83MB +``` + +``` +3_000_000 lines performed in 77.23 s. + 2879MB +``` ## Защита от регресса производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы +написан тест `bench_test.rb`. diff --git a/metrics.rb b/metrics.rb index 2dc7ae2..c2e54d1 100644 --- a/metrics.rb +++ b/metrics.rb @@ -9,8 +9,8 @@ def mac_os? RUBY_PLATFORM.match?(/darwin/) end -from = 550_000 -to = 551_000 +from = 50_000 +to = 55_000 step = 1000 times = [] diff --git a/task-1.rb b/task-1.rb index 882080f..509dcaf 100644 --- a/task-1.rb +++ b/task-1.rb @@ -6,6 +6,8 @@ IE_REGEX = /INTERNET EXPLORER/i.freeze NOT_CHROME_REGEX = /(? fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4] + id: fields[0], + first_name: fields[1], + last_name: fields[2], + age: fields[3] } end def parse_session(fields) { - 'user_id' => fields[1], - 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[5] + user_id: fields[0], + session_id: fields[1], + browser: fields[2], + time: fields[3], + date: fields[4] } end @@ -40,9 +42,15 @@ def work sessions = [] File.open('data.txt', 'r').each do |line| - cols = line.split(',') - users << parse_user(cols) if line.start_with?('user') - sessions << parse_session(cols) if line.start_with?('session') + if line.start_with?('user') + line[USER_PREF] = '' + cols = line.split(',') + users << parse_user(cols) + else + line[SESSION_PREF] = '' + cols = line.split(',') + sessions << parse_session(cols) + end end # Отчёт в json @@ -62,57 +70,61 @@ def work report = {} - report['totalUsers'] = users.count + report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров uniqueBrowsers = [] + sessions.each do |session| - browser = session['browser'] + browser = session[:browser] next if uniqueBrowsers.include?(browser) uniqueBrowsers << browser end - report['uniqueBrowsersCount'] = uniqueBrowsers.count - report['totalSessions'] = sessions.count - report['allBrowsers'] = uniqueBrowsers.map(&:upcase).sort.join(',') + report[:uniqueBrowsersCount] = uniqueBrowsers.count + report[:totalSessions] = sessions.count + report[:allBrowsers] = uniqueBrowsers.map(&:upcase).sort.join(',') # Статистика по пользователям users_objects = [] grouped_sessions_by_user_id = sessions.group_by do |session| - session['user_id'] + session[:user_id] end users.each do |user| - user_sessions = grouped_sessions_by_user_id[user['id']] + user_sessions = grouped_sessions_by_user_id[user[:id]] user_object = User.new(attributes: user, sessions: Array(user_sessions)) users_objects << user_object end - report['usersStats'] = users_objects.each.with_object({}) do |user, hash| - user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + report[:usersStats] = users_objects.each.with_object({}) do |user, hash| + user_key = "#{user.attributes[:first_name]} #{user.attributes[:last_name]}" - longest_session = user.sessions.max { |a,b| a['time'].to_i <=> b['time'].to_i } - user_browsers = user.sessions.map {|s| s['browser'].upcase }.sort.join(', ') + longest_session = user.sessions.max { |a,b| a[:time].to_i <=> b[:time].to_i } + user_browsers = user.sessions.map {|s| s[:browser].upcase }.sort.join(', ') hash[user_key] = { # Собираем количество сессий по пользователям - 'sessionsCount' => user.sessions.count, + sessionsCount: user.sessions.count, # Собираем количество времени по пользователям - 'totalTime' => "#{user.sessions.sum { |s| s['time'].to_i }} min.", + totalTime: "#{user.sessions.sum { |s| s[:time].to_i }} min.", # Выбираем самую длинную сессию пользователя - 'longestSession' => "#{longest_session['time']} min.", + longestSession: "#{longest_session[:time]} min.", # Браузеры пользователя через запятую - 'browsers' => user_browsers, + browsers: user_browsers, # Хоть раз использовал IE? - 'usedIE' => user_browsers.match?(IE_REGEX), + usedIE: user_browsers.match?(IE_REGEX), # Всегда использовал только Chrome? - 'alwaysUsedChrome' => !user_browsers.match?(NOT_CHROME_REGEX), + alwaysUsedChrome: !user_browsers.match?(NOT_CHROME_REGEX), # Даты сессий через запятую в обратном порядке в формате iso8601 - 'dates' => user.sessions.map { |s| Date.iso8601(s['date']) }.sort.reverse_each.with_object([]) { |d, arr| arr << d } + dates: user.sessions.map { |s| Date.iso8601(s[:date]) }.sort.reverse_each.with_object([]) { |d, arr| arr << d } } end - File.write('result.json', "#{report.to_json}\n") + File.open('result.json', 'w') do |f| + f.write(report.to_json) + f.write("\n") + end end