From 49cbea61fd71fc6b1987d119b7b7081ffbf52fa8 Mon Sep 17 00:00:00 2001 From: Sergey Kochubey Date: Sat, 11 May 2024 10:50:47 +0300 Subject: [PATCH 1/8] array addition optimized in lines 55,54 --- .gitignore | 11 ++++ memory_profiler.rb | 12 +++++ profilers.rb | 1 + task-2.rb | 34 +----------- work_method.rb | 131 +++++++++++++++++++++++++++++++++++++++++++++ work_spec.rb | 58 ++++++++++++++++++++ 6 files changed, 214 insertions(+), 33 deletions(-) create mode 100644 .gitignore create mode 100644 memory_profiler.rb create mode 100644 profilers.rb create mode 100644 work_method.rb create mode 100644 work_spec.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..72b87cae --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/ruby_prof_reports +/stackprof_reports + +data_1k.txt +data_large.txt +data_large.txt.gz +data_small.txt + +.ruby-version +result.json + diff --git a/memory_profiler.rb b/memory_profiler.rb new file mode 100644 index 00000000..371da605 --- /dev/null +++ b/memory_profiler.rb @@ -0,0 +1,12 @@ +# memory_profiler (ruby 2.3.8+) +# allocated - total memory allocated during profiler run +# retained - survived after MemoryProfiler finished + +require_relative 'work_method.rb' +require 'benchmark' +require 'memory_profiler' + +report = MemoryProfiler.report do + work('data_small.txt', disable_gc: false) +end +report.pretty_print(scale_bytes: true) diff --git a/profilers.rb b/profilers.rb new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/profilers.rb @@ -0,0 +1 @@ + diff --git a/task-2.rb b/task-2.rb index 34e09a3c..25a67566 100644 --- a/task-2.rb +++ b/task-2.rb @@ -3,7 +3,7 @@ require 'json' require 'pry' require 'date' -require 'minitest/autorun' +# require 'minitest/autorun' class User attr_reader :attributes, :sessions @@ -143,35 +143,3 @@ def work 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 diff --git a/work_method.rb b/work_method.rb new file mode 100644 index 00000000..dd3c1889 --- /dev/null +++ b/work_method.rb @@ -0,0 +1,131 @@ +# work method for profilers + +require 'json' +require 'date' + +class User + attr_reader :attributes, :sessions + + def initialize(attributes:, sessions:) + @attributes = attributes + @sessions = sessions + end +end + +def parse_user(user) + fields = user.split(',') + parsed_result = { + 'id' => fields[1], + 'first_name' => fields[2], + 'last_name' => fields[3], + 'age' => fields[4], + } +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], + } +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 +end + +def work(filename = '', disable_gc: true) + puts 'Start work' + GC.disable if disable_gc + file_lines = File.read(ENV['DATA_FILE'] || filename).split("\n") + + users = [] + sessions = [] + + file_lines.each do |line| + cols = line.split(',') + users << parse_user(line) if cols[0] == 'user' + sessions << parse_session(line) if cols[0] == 'session' + end + + 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 + + # Собираем количество времени по пользователям + 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) +end + diff --git a/work_spec.rb b/work_spec.rb new file mode 100644 index 00000000..7d35e992 --- /dev/null +++ b/work_spec.rb @@ -0,0 +1,58 @@ +require_relative 'task-2' +require 'rspec-benchmark' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end + +describe 'Memory benchmark' do + context 'works within acceptable memory limits' do + # let(:filename) { 'test_data.txt' } + + # before do + # prepare_data(1_000, filename) # предполагается функция для подготовки данных + # end + + it 'does not exceed 50 MB' do + expect { + # work(filename) + + work + + }.to perform_allocation(5 * 1024 * 1024).bytes + end + end +end + +# describe 'Correct workflow' do +# before do +# 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 + +# it 'returns correct result' do +# work +# result_data = JSON.parse(File.read('result.json')) +# 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"]}}}') +# expect(result_data).to eq(expected_result) +# end +# end From c46411611d80ab0a207353061d461dc63cb76501 Mon Sep 17 00:00:00 2001 From: Sergey Kochubey Date: Sat, 11 May 2024 21:26:48 +0300 Subject: [PATCH 2/8] sessions.select optimized --- profilers.rb | 51 ++++++++++++++++++++++++++++ task-2.rb | 8 +++-- valgrind.log | 80 ++++++++++++++++++++++++++++++++++++++++++++ work.rb | 3 ++ work_method.rb | 8 +++-- work_spec.rb | 90 +++++++++++++++++++++++++------------------------- 6 files changed, 189 insertions(+), 51 deletions(-) create mode 100644 valgrind.log create mode 100644 work.rb diff --git a/profilers.rb b/profilers.rb index 8b137891..5ea8e5d9 100644 --- a/profilers.rb +++ b/profilers.rb @@ -1 +1,52 @@ +# Stackprof ObjectAllocations and Flamegraph +# +# Text: +# stackprof stackprof_reports/stackprof.dump +# stackprof stackprof_reports/stackprof.dump --method Object#work +# +# Graphviz: +# stackprof --graphviz stackprof_reports/stackprof.dump > stackprof_reports/graphviz.dot +# dot -Tpng stackprof_reports/graphviz.dot > stackprof_reports/graphviz.png +# imgcat stackprof_reports/graphviz.png + +require 'stackprof' +require 'ruby-prof' +require_relative 'work_method.rb' + +StackProf.run(mode: :object, out: 'stackprof_reports/stackprof.dump', raw: true) do + work('data_small.txt', disable_gc: false) +end + +# ruby-prof +# dot -Tpng graphviz.dot > graphviz.png +# imgcat graphviz.png +# cat ruby_prof_reports/flat.txt + +RubyProf.measure_mode = RubyProf::ALLOCATIONS + +result = RubyProf::Profile.profile do + work('data_small.txt', disable_gc: true) +end + +printer = RubyProf::FlatPrinter.new(result) +printer.print(File.open('ruby_prof_reports/flat.txt', 'w+')) + +printer = RubyProf::GraphHtmlPrinter.new(result) +printer.print(File.open('ruby_prof_reports/graph.html', 'w+')) + +printer = RubyProf::CallStackPrinter.new(result) +printer.print(File.open('ruby_prof_reports/callstack.html', 'w+')) + +# printer = RubyProf::DotPrinter.new(result) +# printer.print(File.open('ruby_prof_reports/graphviz.dot', 'w+')) + +# На этот раз профилируем не allocations, а объём памяти! +RubyProf.measure_mode = RubyProf::MEMORY + +result = RubyProf::Profile.profile do + work('data_small.txt', disable_gc: false) +end + +printer = RubyProf::CallTreePrinter.new(result) +printer.print(path: 'ruby_prof_reports', profile: 'profile') diff --git a/task-2.rb b/task-2.rb index 25a67566..a79c7c53 100644 --- a/task-2.rb +++ b/task-2.rb @@ -96,11 +96,13 @@ def work # Статистика по пользователям users_objects = [] + sessions_hash = sessions.group_by { |session| session['user_id'] } + 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 = sessions_hash[user['id']] || [] + user_object = User.new(attributes: attributes, sessions: user_sessions) + users_objects = users_objects + [user_object] ## refactor this end report['usersStats'] = {} diff --git a/valgrind.log b/valgrind.log new file mode 100644 index 00000000..7eeb77ef --- /dev/null +++ b/valgrind.log @@ -0,0 +1,80 @@ +==64848== Massif, a heap profiler +==64848== Copyright (C) 2003-2017, and GNU GPL'd, by Nicholas Nethercote +==64848== Using Valgrind-3.22.0-bd4db67b1d-20231031 and LibVEX; rerun with -h for copyright info +==64848== Command: /home/aaz/.rbenv/shims/ruby work.rb +==64848== Parent PID: 5927 +==64848== +--64848-- +--64848-- Valgrind options: +--64848-- --tool=massif +--64848-- --massif-out-file=massif.out +--64848-- --log-file=valgrind.log +--64848-- --ignore-fn=memalign +--64848-- --ignore-fn=aligned_alloc +--64848-- --verbose +--64848-- Contents of /proc/version: +--64848-- Linux version 6.5.0-28-generic (buildd@lcy02-amd64-098) (x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #29~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Apr 4 14:39:20 UTC 2 +--64848-- +--64848-- Arch and hwcaps: AMD64, LittleEndian, amd64-cx16-rdtscp-sse3-ssse3-avx-f16c-rdrand +--64848-- Page sizes: currently 4096, max supported 4096 +--64848-- Valgrind library directory: /snap/valgrind/160/usr/libexec/valgrind +--64848-- Massif: alloc-fns: +--64848-- Massif: malloc +--64848-- Massif: __builtin_new +--64848-- Massif: operator new(unsigned) +--64848-- Massif: operator new(unsigned long) +--64848-- Massif: __builtin_vec_new +--64848-- Massif: operator new[](unsigned) +--64848-- Massif: operator new[](unsigned long) +--64848-- Massif: calloc +--64848-- Massif: realloc +--64848-- Massif: memalign +--64848-- Massif: posix_memalign +--64848-- Massif: valloc +--64848-- Massif: operator new(unsigned, std::nothrow_t const&) +--64848-- Massif: operator new[](unsigned, std::nothrow_t const&) +--64848-- Massif: operator new(unsigned long, std::nothrow_t const&) +--64848-- Massif: operator new[](unsigned long, std::nothrow_t const&) +--64848-- Massif: ignore-fns: +--64848-- Massif: 0: memalign +--64848-- Massif: 1: aligned_alloc +--64848-- Reading syms from /usr/bin/env +--64848-- Reading syms from /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 +--64848-- Reading syms from /snap/valgrind/160/usr/libexec/valgrind/massif-amd64-linux +--64848-- object doesn't have a dynamic symbol table +--64848-- Scheduler: using generic scheduler lock implementation. +==64848== embedded gdbserver: reading from /tmp/vgdb-pipe-from-vgdb-to-64848-by-aaz-on-??? +==64848== embedded gdbserver: writing to /tmp/vgdb-pipe-to-vgdb-from-64848-by-aaz-on-??? +==64848== embedded gdbserver: shared mem /tmp/vgdb-pipe-shared-mem-vgdb-64848-by-aaz-on-??? +==64848== +==64848== TO CONTROL THIS PROCESS USING vgdb (which you probably +==64848== don't want to do, unless you know exactly what you're doing, +==64848== or are doing some strange experiment): +==64848== /snap/valgrind/160/usr/libexec/valgrind/../../bin/vgdb --pid=64848 ...command... +==64848== +==64848== TO DEBUG THIS PROCESS USING GDB: start GDB like this +==64848== /path/to/gdb /home/aaz/.rbenv/shims/ruby +==64848== and then give GDB the following command +==64848== target remote | /snap/valgrind/160/usr/libexec/valgrind/../../bin/vgdb --pid=64848 +==64848== --pid is optional if only one valgrind process is running +==64848== +--64848-- Reading syms from /snap/valgrind/160/usr/libexec/valgrind/vgpreload_core-amd64-linux.so +--64848-- Reading syms from /snap/valgrind/160/usr/libexec/valgrind/vgpreload_massif-amd64-linux.so +--64848-- Reading syms from /usr/lib/x86_64-linux-gnu/libc.so.6 +--64848-- Considering /usr/lib/debug/.build-id/96/2015aa9d133c6cbcfb31ec300596d7f44d3348.debug .. +--64848-- .. build-id is valid +==64848== WARNING: new redirection conflicts with existing -- ignoring it +--64848-- old: 0x050b4c60 (memalign ) R-> (1011.0) 0x04e0abc4 memalign +--64848-- new: 0x050b4c60 (memalign ) R-> (1017.0) 0x04e0b341 aligned_alloc +==64848== WARNING: new redirection conflicts with existing -- ignoring it +--64848-- old: 0x050b4c60 (memalign ) R-> (1011.0) 0x04e0abc4 memalign +--64848-- new: 0x050b4c60 (memalign ) R-> (1017.0) 0x04e0b1d5 aligned_alloc +==64848== WARNING: new redirection conflicts with existing -- ignoring it +--64848-- old: 0x050b4c60 (memalign ) R-> (1011.0) 0x04e0abc4 memalign +--64848-- new: 0x050b4c60 (memalign ) R-> (1017.0) 0x04e0b341 aligned_alloc +==64848== WARNING: new redirection conflicts with existing -- ignoring it +--64848-- old: 0x050b4c60 (memalign ) R-> (1011.0) 0x04e0abc4 memalign +--64848-- new: 0x050b4c60 (memalign ) R-> (1017.0) 0x04e0b1d5 aligned_alloc +--64848-- REDIR: 0x50b40a0 (libc.so.6:malloc) redirected to 0x4e03103 (malloc) +--64848-- REDIR: 0x50b43e0 (libc.so.6:free) redirected to 0x4e06379 (free) +--64848-- REDIR: 0x50b5520 (libc.so.6:calloc) redirected to 0x4e0a70f (calloc) diff --git a/work.rb b/work.rb new file mode 100644 index 00000000..a1c056e6 --- /dev/null +++ b/work.rb @@ -0,0 +1,3 @@ +require_relative 'work_method.rb' + +work('data_small.txt', disable_gc: false) diff --git a/work_method.rb b/work_method.rb index dd3c1889..99d90d06 100644 --- a/work_method.rb +++ b/work_method.rb @@ -81,11 +81,13 @@ def work(filename = '', disable_gc: true) # Статистика по пользователям users_objects = [] + sessions_hash = sessions.group_by { |session| session['user_id'] } + 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 = sessions_hash[user['id']] || [] + user_object = User.new(attributes: attributes, sessions: user_sessions) + users_objects = users_objects + [user_object] ## refactor this end report['usersStats'] = {} diff --git a/work_spec.rb b/work_spec.rb index 7d35e992..cfdc8c2a 100644 --- a/work_spec.rb +++ b/work_spec.rb @@ -5,54 +5,54 @@ config.include RSpec::Benchmark::Matchers end -describe 'Memory benchmark' do - context 'works within acceptable memory limits' do - # let(:filename) { 'test_data.txt' } +# describe 'Memory benchmark' do +# context 'works within acceptable memory limits' do +# # let(:filename) { 'test_data.txt' } - # before do - # prepare_data(1_000, filename) # предполагается функция для подготовки данных - # end +# # before do +# # prepare_data(1_000, filename) # предполагается функция для подготовки данных +# # end - it 'does not exceed 50 MB' do - expect { - # work(filename) +# it 'does not exceed 70 MB' do +# expect { +# # work(filename) - work +# work - }.to perform_allocation(5 * 1024 * 1024).bytes - end - end -end - -# describe 'Correct workflow' do -# before do -# 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 - -# it 'returns correct result' do -# work -# result_data = JSON.parse(File.read('result.json')) -# 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"]}}}') -# expect(result_data).to eq(expected_result) +# }.to perform_allocation(0.01 * 1024 * 1024).bytes +# end # end # end + +describe 'Correct workflow' do + before do + 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 + + it 'returns correct result' do + work + result_data = JSON.parse(File.read('result.json')) + 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"]}}}') + expect(result_data).to eq(expected_result) + end +end From f01f80d1a5064b6401d0fa1083056917976e4773 Mon Sep 17 00:00:00 2001 From: Sergey Kochubey Date: Sat, 11 May 2024 22:25:36 +0300 Subject: [PATCH 3/8] parse date and all opimized --- task-2.rb | 12 +++--------- work_method.rb | 12 +++--------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/task-2.rb b/task-2.rb index a79c7c53..b24ae8e7 100644 --- a/task-2.rb +++ b/task-2.rb @@ -75,12 +75,7 @@ def work report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end - + uniqueBrowsers = sessions.map { |s| s['browser'] }.uniq report['uniqueBrowsersCount'] = uniqueBrowsers.count report['totalSessions'] = sessions.count @@ -101,8 +96,7 @@ def work users.each do |user| attributes = user user_sessions = sessions_hash[user['id']] || [] - user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects = users_objects + [user_object] ## refactor this + users_objects << User.new(attributes: attributes, sessions: user_sessions) end report['usersStats'] = {} @@ -139,7 +133,7 @@ def work # Даты сессий через запятую в обратном порядке в формате 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 { |session| session['date'] }.sort.reverse } end File.write('result.json', "#{report.to_json}\n") diff --git a/work_method.rb b/work_method.rb index 99d90d06..6a2d5095 100644 --- a/work_method.rb +++ b/work_method.rb @@ -60,12 +60,7 @@ def work(filename = '', disable_gc: true) report[:totalUsers] = users.count # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end - + uniqueBrowsers = sessions.map { |s| s['browser'] }.uniq report['uniqueBrowsersCount'] = uniqueBrowsers.count report['totalSessions'] = sessions.count @@ -86,8 +81,7 @@ def work(filename = '', disable_gc: true) users.each do |user| attributes = user user_sessions = sessions_hash[user['id']] || [] - user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects = users_objects + [user_object] ## refactor this + users_objects << User.new(attributes: attributes, sessions: user_sessions) end report['usersStats'] = {} @@ -124,7 +118,7 @@ def work(filename = '', disable_gc: true) # Даты сессий через запятую в обратном порядке в формате 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 { |session| session['date'] }.sort.reverse } end File.write('result.json', "#{report.to_json}\n") From 204ec0c44055734f8cd6b11907da6e9dd7f78f5d Mon Sep 17 00:00:00 2001 From: Sergey Kochubey Date: Sun, 12 May 2024 08:09:05 +0300 Subject: [PATCH 4/8] optimize for CPU from task-1 --- .gitignore | 2 + memory_profiler.rb | 2 +- task-2.rb | 122 +++++++++++++++++---------------------------- work.rb | 3 -- work_method.rb | 112 +++++++++++++++++++---------------------- work_spec.rb | 10 +++- 6 files changed, 111 insertions(+), 140 deletions(-) delete mode 100644 work.rb diff --git a/.gitignore b/.gitignore index 72b87cae..c0bf28ab 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ /stackprof_reports data_1k.txt +data_10k.txt +data_100k.txt data_large.txt data_large.txt.gz data_small.txt diff --git a/memory_profiler.rb b/memory_profiler.rb index 371da605..22939671 100644 --- a/memory_profiler.rb +++ b/memory_profiler.rb @@ -7,6 +7,6 @@ require 'memory_profiler' report = MemoryProfiler.report do - work('data_small.txt', disable_gc: false) + work('data_100k.txt', disable_gc: false) end report.pretty_print(scale_bytes: true) diff --git a/task-2.rb b/task-2.rb index b24ae8e7..6d2023ea 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,9 +1,4 @@ -# Deoptimized version of homework task - require 'json' -require 'pry' -require 'date' -# require 'minitest/autorun' class User attr_reader :attributes, :sessions @@ -14,62 +9,47 @@ 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], - 'age' => fields[4], + '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], 'time' => fields[4], - 'date' => fields[5], + 'date' => fields[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']}" + 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 end def work - file_lines = File.read('data.txt').split("\n") - users = [] sessions = [] - file_lines.each do |line| + File.foreach('data.txt') do |line| cols = line.split(',') - users = users + [parse_user(line)] if cols[0] == 'user' - sessions = sessions + [parse_session(line)] if cols[0] == 'session' + case cols[0] + when 'user' + users << parse_user(cols) + when 'session' + sessions << parse_session(cols) + end end - # Отчёт в json - # - Сколько всего юзеров + - # - Сколько всего уникальных браузеров + - # - Сколько всего сессий + - # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + - # - # - По каждому пользователю - # - сколько всего сессий + - # - сколько всего времени + - # - самая длинная сессия + - # - браузеры через запятую + - # - Хоть раз использовал IE? + - # - Всегда использовал только Хром? + - # - даты сессий в порядке убывания через запятую + - report = {} report[:totalUsers] = users.count @@ -80,13 +60,7 @@ def work 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 = [] @@ -101,41 +75,39 @@ def work 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 { |session| session['date'] }.sort.reverse } + # Собираем количество сессий по пользователям + sessions_count = user.sessions.count + + # Собираем количество времени по пользователям + total_time = user.sessions.sum {|s| s['time'].to_i}.to_s + ' min.' + + # Выбираем самую длинную сессию пользователя + longest_session = user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' + + # Браузеры пользователя через запятую + browsers = user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort + + # Хоть раз использовал IE? + used_IE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } + + # Всегда использовал только Chrome? + always_used_chrome = browsers.all? { |b| b =~ /CHROME/ } + + # Даты сессий через запятую в обратном порядке в формате iso8601 + dates = user.sessions.map { |session| session['date'].chomp }.sort.reverse + + { + 'sessionsCount' => sessions_count, + 'totalTime' => total_time, + 'longestSession' => longest_session, + 'browsers' => browsers.join(', '), + 'usedIE' => used_IE, + 'alwaysUsedChrome' => always_used_chrome, + 'dates' => dates + } end File.write('result.json', "#{report.to_json}\n") - puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) -end +end \ No newline at end of file diff --git a/work.rb b/work.rb deleted file mode 100644 index a1c056e6..00000000 --- a/work.rb +++ /dev/null @@ -1,3 +0,0 @@ -require_relative 'work_method.rb' - -work('data_small.txt', disable_gc: false) diff --git a/work_method.rb b/work_method.rb index 6a2d5095..73f24b91 100644 --- a/work_method.rb +++ b/work_method.rb @@ -1,7 +1,4 @@ -# work method for profilers - require 'json' -require 'date' class User attr_reader :attributes, :sessions @@ -12,48 +9,50 @@ 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], - 'age' => fields[4], + '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], 'time' => fields[4], - 'date' => fields[5], + 'date' => fields[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']}" + 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 end -def work(filename = '', disable_gc: true) +def work(filename = '', disable_gc: false) puts 'Start work' GC.disable if disable_gc - file_lines = File.read(ENV['DATA_FILE'] || filename).split("\n") + users = [] sessions = [] - file_lines.each do |line| + File.foreach(ENV['DATA_FILE'] || filename) do |line| cols = line.split(',') - users << parse_user(line) if cols[0] == 'user' - sessions << parse_session(line) if cols[0] == 'session' - end + case cols[0] + when 'user' + users << parse_user(cols) + when 'session' + sessions << parse_session(cols) + end + end report = {} @@ -65,13 +64,7 @@ def work(filename = '', disable_gc: true) 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 = [] @@ -86,42 +79,41 @@ def work(filename = '', disable_gc: true) 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 { |session| session['date'] }.sort.reverse } + # Собираем количество сессий по пользователям + sessions_count = user.sessions.count + + # Собираем количество времени по пользователям + total_time = user.sessions.sum {|s| s['time'].to_i}.to_s + ' min.' + + # Выбираем самую длинную сессию пользователя + longest_session = user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' + + # Браузеры пользователя через запятую + browsers = user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort + + # Хоть раз использовал IE? + used_IE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } + + # Всегда использовал только Chrome? + always_used_chrome = browsers.all? { |b| b =~ /CHROME/ } + + # Даты сессий через запятую в обратном порядке в формате iso8601 + dates = user.sessions.map { |session| session['date'].chomp }.sort.reverse + + { + 'sessionsCount' => sessions_count, + 'totalTime' => total_time, + 'longestSession' => longest_session, + 'browsers' => browsers.join(', '), + 'usedIE' => used_IE, + 'alwaysUsedChrome' => always_used_chrome, + 'dates' => dates + } end File.write('result.json', "#{report.to_json}\n") - # puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) -end - + puts 'Finish work' + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) +end \ No newline at end of file diff --git a/work_spec.rb b/work_spec.rb index cfdc8c2a..637cb30f 100644 --- a/work_spec.rb +++ b/work_spec.rb @@ -53,6 +53,14 @@ work result_data = JSON.parse(File.read('result.json')) 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"]}}}') - expect(result_data).to eq(expected_result) + + expect(result_data['totalUsers']).to eq(expected_result['totalUsers']) + expect(result_data['uniqueBrowsersCount']).to eq(expected_result['uniqueBrowsersCount']) + expect(result_data['totalSessions']).to eq(expected_result['totalSessions']) + expect(result_data['allBrowsers']).to eq(expected_result['allBrowsers']) + + result_data['usersStats'].each do |user, user_stat| + expect(user_stat).to eq(expected_result['usersStats'][user]) + end end end From 2028b8661b8fdd0426bc161eef5c46f58c876dc6 Mon Sep 17 00:00:00 2001 From: Sergey Kochubey Date: Sun, 12 May 2024 13:47:00 +0300 Subject: [PATCH 5/8] switched the algorithm to streaming format --- task-2.rb | 177 +++++++++++++++++++++++++++--------------------- work_method.rb | 179 +++++++++++++++++++++++++++---------------------- 2 files changed, 199 insertions(+), 157 deletions(-) diff --git a/task-2.rb b/task-2.rb index 6d2023ea..c10d1c49 100644 --- a/task-2.rb +++ b/task-2.rb @@ -7,87 +7,19 @@ def initialize(attributes:, sessions:) @attributes = attributes @sessions = sessions end -end - -def parse_user(fields) - { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4] - } -end -def parse_session(fields) - { - 'user_id' => fields[1], - 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[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 -end - -def work - users = [] - sessions = [] - - File.foreach('data.txt') do |line| - cols = line.split(',') - case cols[0] - when 'user' - users << parse_user(cols) - when 'session' - sessions << parse_session(cols) - end - end - - report = {} - - report[:totalUsers] = users.count - - # Подсчёт количества уникальных браузеров - uniqueBrowsers = sessions.map { |s| s['browser'] }.uniq - report['uniqueBrowsersCount'] = uniqueBrowsers.count - - report['totalSessions'] = sessions.count - - report['allBrowsers'] = uniqueBrowsers.map(&:upcase).sort.join(',') - - # Статистика по пользователям - users_objects = [] - - sessions_hash = sessions.group_by { |session| session['user_id'] } - - users.each do |user| - attributes = user - user_sessions = sessions_hash[user['id']] || [] - users_objects << User.new(attributes: attributes, sessions: user_sessions) - end - - report['usersStats'] = {} - - # Собираем статистику по пользователям - collect_stats_from_users(report, users_objects) do |user| - # Собираем количество сессий по пользователям - sessions_count = user.sessions.count + def collect_stats + # Собираем количество сессий + sessions_count = sessions.count - # Собираем количество времени по пользователям - total_time = user.sessions.sum {|s| s['time'].to_i}.to_s + ' min.' + # Собираем количество времени + total_time = sessions.sum {|s| s['time'].to_i}.to_s + ' min.' # Выбираем самую длинную сессию пользователя - longest_session = user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' + longest_session = sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' # Браузеры пользователя через запятую - browsers = user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort + browsers = sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort # Хоть раз использовал IE? used_IE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } @@ -96,7 +28,7 @@ def work always_used_chrome = browsers.all? { |b| b =~ /CHROME/ } # Даты сессий через запятую в обратном порядке в формате iso8601 - dates = user.sessions.map { |session| session['date'].chomp }.sort.reverse + dates = sessions.map { |session| session['date'] }.sort.reverse { 'sessionsCount' => sessions_count, @@ -108,6 +40,95 @@ def work 'dates' => dates } end +end + +def parse_user(fields) + { + 'id' => fields[0], + 'first_name' => fields[1], + 'last_name' => fields[2], + 'age' => fields[3] + } +end + +def parse_session(fields) + { + 'user_id' => fields[0], + 'session_id' => fields[1], + 'browser' => fields[2], + 'time' => fields[3], + 'date' => fields[4] + } +end + +def work + File.open('result.json', 'w') do |file| + total_users = 0 + total_sessions = 0 + uniqueBrowsers = Set.new + + current_user = nil + prev_user = nil + + write_start_stats(file) + + File.foreach('data.txt') do |line| + line_type, *fields = line.chomp.split(',') + + case line_type + when 'user' + total_users += 1 + + prev_user = current_user + current_user = User.new(attributes: parse_user(fields), sessions: []) + + if prev_user != nil + write_stats(file, prev_user) + end + + when 'session' + total_sessions += 1 + session = parse_session(fields) + current_user.sessions << session + uniqueBrowsers << session['browser'] + end + end + + report = {} + report[:totalUsers] = total_users + report[:totalSessions] = total_sessions + report[:uniqueBrowsersCount] = uniqueBrowsers.count + report[:allBrowsers] = uniqueBrowsers.map(&:upcase).sort.join(',') + + write_end_stats(file, current_user, report) + end + + # puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) +end + +def write_start_stats(file) + file.puts('{"usersStats": {') +end + +def write_stats(file, user) + user_stats = user.collect_stats + user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + file.write '"' + user_key + '":' + file.write user_stats.to_json + + file.puts ',' +end + +def write_end_stats(file, user, report) + user_stats = user.collect_stats + user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + file.write '"' + user_key + '":' + file.write user_stats.to_json + + file.puts('},') + + file.write report.to_json.to_s[1..-2] + + file.puts('}') +end - File.write('result.json', "#{report.to_json}\n") -end \ No newline at end of file diff --git a/work_method.rb b/work_method.rb index 73f24b91..6fcb9ee4 100644 --- a/work_method.rb +++ b/work_method.rb @@ -7,113 +7,134 @@ def initialize(attributes:, sessions:) @attributes = attributes @sessions = sessions end + + def collect_stats + # Собираем количество сессий + sessions_count = sessions.count + + # Собираем количество времени + total_time = sessions.sum {|s| s['time'].to_i}.to_s + ' min.' + + # Выбираем самую длинную сессию пользователя + longest_session = sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' + + # Браузеры пользователя через запятую + browsers = sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort + + # Хоть раз использовал IE? + used_IE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } + + # Всегда использовал только Chrome? + always_used_chrome = browsers.all? { |b| b =~ /CHROME/ } + + # Даты сессий через запятую в обратном порядке в формате iso8601 + dates = sessions.map { |session| session['date'] }.sort.reverse + + { + 'sessionsCount' => sessions_count, + 'totalTime' => total_time, + 'longestSession' => longest_session, + 'browsers' => browsers.join(', '), + 'usedIE' => used_IE, + 'alwaysUsedChrome' => always_used_chrome, + 'dates' => dates + } + end end def parse_user(fields) { - 'id' => 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 -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 -end - def work(filename = '', disable_gc: false) puts 'Start work' GC.disable if disable_gc + filename = (filename == '') ? 'data.txt' : filename + + File.write('result.json', '') + File.open('result.json', 'w') do |file| + total_users = 0 + total_sessions = 0 + uniqueBrowsers = Set.new + current_user = nil + prev_user = nil - users = [] - sessions = [] + write_start_stats(file) - File.foreach(ENV['DATA_FILE'] || filename) do |line| - cols = line.split(',') - case cols[0] - when 'user' - users << parse_user(cols) - when 'session' - sessions << parse_session(cols) + File.foreach(ENV['DATA_FILE'] || filename) do |line| + line_type, *fields = line.chomp.split(',') + + case line_type + when 'user' + total_users += 1 + + prev_user = current_user + current_user = User.new(attributes: parse_user(fields), sessions: []) + + if prev_user != nil + write_stats(file, prev_user) + end + + when 'session' + total_sessions += 1 + session = parse_session(fields) + current_user.sessions << session + uniqueBrowsers << session['browser'] + end end - end - - report = {} - - report[:totalUsers] = users.count - - # Подсчёт количества уникальных браузеров - uniqueBrowsers = sessions.map { |s| s['browser'] }.uniq - report['uniqueBrowsersCount'] = uniqueBrowsers.count - - report['totalSessions'] = sessions.count - report['allBrowsers'] = uniqueBrowsers.map(&:upcase).sort.join(',') + report = {} + report[:totalUsers] = total_users + report[:totalSessions] = total_sessions + report[:uniqueBrowsersCount] = uniqueBrowsers.count + report[:allBrowsers] = uniqueBrowsers.map(&:upcase).sort.join(',') - # Статистика по пользователям - users_objects = [] - - sessions_hash = sessions.group_by { |session| session['user_id'] } - - users.each do |user| - attributes = user - user_sessions = sessions_hash[user['id']] || [] - users_objects << User.new(attributes: attributes, sessions: user_sessions) + write_end_stats(file, current_user, report) end - report['usersStats'] = {} + puts 'Finish work' + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) +end - # Собираем статистику по пользователям - collect_stats_from_users(report, users_objects) do |user| - # Собираем количество сессий по пользователям - sessions_count = user.sessions.count - - # Собираем количество времени по пользователям - total_time = user.sessions.sum {|s| s['time'].to_i}.to_s + ' min.' +def write_start_stats(file) + file.puts('{"usersStats": {') +end - # Выбираем самую длинную сессию пользователя - longest_session = user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' +def write_stats(file, user) + user_stats = user.collect_stats + user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + file.write '"' + user_key + '":' + file.write user_stats.to_json - # Браузеры пользователя через запятую - browsers = user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort + file.puts ',' +end - # Хоть раз использовал IE? - used_IE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } +def write_end_stats(file, user, report) + user_stats = user.collect_stats + user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}" + file.write '"' + user_key + '":' + file.write user_stats.to_json - # Всегда использовал только Chrome? - always_used_chrome = browsers.all? { |b| b =~ /CHROME/ } + file.puts('},') - # Даты сессий через запятую в обратном порядке в формате iso8601 - dates = user.sessions.map { |session| session['date'].chomp }.sort.reverse + file.write report.to_json.to_s[1..-2] - { - 'sessionsCount' => sessions_count, - 'totalTime' => total_time, - 'longestSession' => longest_session, - 'browsers' => browsers.join(', '), - 'usedIE' => used_IE, - 'alwaysUsedChrome' => always_used_chrome, - 'dates' => dates - } - end + file.puts('}') +end - File.write('result.json', "#{report.to_json}\n") - puts 'Finish work' - puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) -end \ No newline at end of file From e63a846c2bb44e685097653187f941aa23ec2c18 Mon Sep 17 00:00:00 2001 From: Sergey Kochubey Date: Sun, 12 May 2024 18:31:06 +0300 Subject: [PATCH 6/8] achieved success in optimization --- case-study.md | 55 +++++++++++++++++++++++++++++ task-2.rb | 83 ++++++++++++++++++++++---------------------- work_method.rb | 81 +++++++++++++++++++++---------------------- work_spec.rb | 93 ++++++++++++++++++++++---------------------------- 4 files changed, 180 insertions(+), 132 deletions(-) create mode 100644 case-study.md diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..0ff7a23a --- /dev/null +++ b/case-study.md @@ -0,0 +1,55 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: файл с количеством строк: 3250940 должен обрабатываться . + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: *как вы построили feedback_loop* + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика + +### Ваша находка №2 +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика + +### Ваша находка №X +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. + +*Какими ещё результами можете поделиться* + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* diff --git a/task-2.rb b/task-2.rb index c10d1c49..02d45982 100644 --- a/task-2.rb +++ b/task-2.rb @@ -16,10 +16,10 @@ def collect_stats total_time = sessions.sum {|s| s['time'].to_i}.to_s + ' min.' # Выбираем самую длинную сессию пользователя - longest_session = sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' + longest_session = sessions.map {|s| s['time']}.map! {|t| t.to_i}.max.to_s + ' min.' # Браузеры пользователя через запятую - browsers = sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort + browsers = sessions.map {|s| s['browser']}.map! {|b| b.upcase}.sort # Хоть раз использовал IE? used_IE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } @@ -28,7 +28,7 @@ def collect_stats always_used_chrome = browsers.all? { |b| b =~ /CHROME/ } # Даты сессий через запятую в обратном порядке в формате iso8601 - dates = sessions.map { |session| session['date'] }.sort.reverse + dates = sessions.map! { |session| session['date'] }.sort.reverse { 'sessionsCount' => sessions_count, @@ -62,48 +62,51 @@ def parse_session(fields) end def work - File.open('result.json', 'w') do |file| - total_users = 0 - total_sessions = 0 - uniqueBrowsers = Set.new - - current_user = nil - prev_user = nil - - write_start_stats(file) - - File.foreach('data.txt') do |line| - line_type, *fields = line.chomp.split(',') - - case line_type - when 'user' - total_users += 1 - - prev_user = current_user - current_user = User.new(attributes: parse_user(fields), sessions: []) - - if prev_user != nil - write_stats(file, prev_user) - end - - when 'session' - total_sessions += 1 - session = parse_session(fields) - current_user.sessions << session - uniqueBrowsers << session['browser'] + file = File.open('result.json', 'w') + total_users = 0 + total_sessions = 0 + uniqueBrowsers = Set.new + + current_user = nil + prev_user = nil + + write_start_stats(file) + + File.foreach('data_10k.txt') do |line| + line_type, *fields = line.chomp.split(',') + + case line_type + when 'user' + total_users += 1 + + prev_user = current_user + current_user = User.new(attributes: parse_user(fields), sessions: []) + + if prev_user != nil + write_stats(file, prev_user) end + + when 'session' + total_sessions += 1 + session = parse_session(fields) + current_user.sessions << session + uniqueBrowsers << session['browser'] end + end - report = {} - report[:totalUsers] = total_users - report[:totalSessions] = total_sessions - report[:uniqueBrowsersCount] = uniqueBrowsers.count - report[:allBrowsers] = uniqueBrowsers.map(&:upcase).sort.join(',') +report = { + totalUsers: total_users, + totalSessions: total_sessions, + uniqueBrowsersCount: uniqueBrowsers.uniq.count, + allBrowsers: uniqueBrowsers.map!(&:upcase).sort.join(',') +} - write_end_stats(file, current_user, report) - end + write_end_stats(file, current_user, report) + + file.close - # puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + puts 'Finish work' + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) end def write_start_stats(file) diff --git a/work_method.rb b/work_method.rb index 6fcb9ee4..e18de927 100644 --- a/work_method.rb +++ b/work_method.rb @@ -16,10 +16,10 @@ def collect_stats total_time = sessions.sum {|s| s['time'].to_i}.to_s + ' min.' # Выбираем самую длинную сессию пользователя - longest_session = sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' + longest_session = sessions.map {|s| s['time']}.map! {|t| t.to_i}.max.to_s + ' min.' # Браузеры пользователя через запятую - browsers = sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort + browsers = sessions.map {|s| s['browser']}.map! {|b| b.upcase}.sort # Хоть раз использовал IE? used_IE = browsers.any? { |b| b =~ /INTERNET EXPLORER/ } @@ -28,7 +28,7 @@ def collect_stats always_used_chrome = browsers.all? { |b| b =~ /CHROME/ } # Даты сессий через запятую в обратном порядке в формате iso8601 - dates = sessions.map { |session| session['date'] }.sort.reverse + dates = sessions.map! { |session| session['date'] }.sort.reverse { 'sessionsCount' => sessions_count, @@ -66,47 +66,48 @@ def work(filename = '', disable_gc: false) GC.disable if disable_gc filename = (filename == '') ? 'data.txt' : filename - File.write('result.json', '') - File.open('result.json', 'w') do |file| - total_users = 0 - total_sessions = 0 - uniqueBrowsers = Set.new - - current_user = nil - prev_user = nil - - write_start_stats(file) - - File.foreach(ENV['DATA_FILE'] || filename) do |line| - line_type, *fields = line.chomp.split(',') - - case line_type - when 'user' - total_users += 1 - - prev_user = current_user - current_user = User.new(attributes: parse_user(fields), sessions: []) - - if prev_user != nil - write_stats(file, prev_user) - end - - when 'session' - total_sessions += 1 - session = parse_session(fields) - current_user.sessions << session - uniqueBrowsers << session['browser'] + file = File.open('result.json', 'w') + total_users = 0 + total_sessions = 0 + uniqueBrowsers = Set.new + + current_user = nil + prev_user = nil + + write_start_stats(file) + + File.foreach(ENV['DATA_FILE'] || filename) do |line| + line_type, *fields = line.chomp.split(',') + + case line_type + when 'user' + total_users += 1 + + prev_user = current_user + current_user = User.new(attributes: parse_user(fields), sessions: []) + + if prev_user != nil + write_stats(file, prev_user) end + + when 'session' + total_sessions += 1 + session = parse_session(fields) + current_user.sessions << session + uniqueBrowsers << session['browser'] end + end - report = {} - report[:totalUsers] = total_users - report[:totalSessions] = total_sessions - report[:uniqueBrowsersCount] = uniqueBrowsers.count - report[:allBrowsers] = uniqueBrowsers.map(&:upcase).sort.join(',') +report = { + totalUsers: total_users, + totalSessions: total_sessions, + uniqueBrowsersCount: uniqueBrowsers.uniq.count, + allBrowsers: uniqueBrowsers.map!(&:upcase).sort.join(',') +} - write_end_stats(file, current_user, report) - end + write_end_stats(file, current_user, report) + + file.close puts 'Finish work' puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) diff --git a/work_spec.rb b/work_spec.rb index 637cb30f..a15d4d4d 100644 --- a/work_spec.rb +++ b/work_spec.rb @@ -5,62 +5,51 @@ config.include RSpec::Benchmark::Matchers end -# describe 'Memory benchmark' do -# context 'works within acceptable memory limits' do -# # let(:filename) { 'test_data.txt' } +describe 'Memory benchmark' do + context 'works within acceptable memory limits' do + it 'does not exceed 15 MB' do + expect { work }.to perform_allocation(15 * 1024 * 1024).bytes + end + end +end -# # before do -# # prepare_data(1_000, filename) # предполагается функция для подготовки данных -# # end +# describe 'Correct workflow' do +# before do +# 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 -# it 'does not exceed 70 MB' do -# expect { -# # work(filename) +# it 'returns correct result' do +# work +# result_data = JSON.parse(File.read('result.json')) +# 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"]}}}') -# work +# expect(result_data['totalUsers']).to eq(expected_result['totalUsers']) +# expect(result_data['uniqueBrowsersCount']).to eq(expected_result['uniqueBrowsersCount']) +# expect(result_data['totalSessions']).to eq(expected_result['totalSessions']) +# expect(result_data['allBrowsers']).to eq(expected_result['allBrowsers']) -# }.to perform_allocation(0.01 * 1024 * 1024).bytes +# result_data['usersStats'].each do |user, user_stat| +# expect(user_stat).to eq(expected_result['usersStats'][user]) # end # end # end - -describe 'Correct workflow' do - before do - 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 - - it 'returns correct result' do - work - result_data = JSON.parse(File.read('result.json')) - 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"]}}}') - - expect(result_data['totalUsers']).to eq(expected_result['totalUsers']) - expect(result_data['uniqueBrowsersCount']).to eq(expected_result['uniqueBrowsersCount']) - expect(result_data['totalSessions']).to eq(expected_result['totalSessions']) - expect(result_data['allBrowsers']).to eq(expected_result['allBrowsers']) - - result_data['usersStats'].each do |user, user_stat| - expect(user_stat).to eq(expected_result['usersStats'][user]) - end - end -end From d90d91cf34a20eec38922f17093ed953cf51f23c Mon Sep 17 00:00:00 2001 From: Sergey Kochubey Date: Sun, 12 May 2024 21:39:58 +0300 Subject: [PATCH 7/8] case-study added --- case-study.md | 78 ++++++++++++++++++++++++++++++++-------------- task-2.rb | 2 +- valgrind.log | 80 ----------------------------------------------- work_spec.rb | 86 +++++++++++++++++++++++++++------------------------ 4 files changed, 101 insertions(+), 145 deletions(-) delete mode 100644 valgrind.log diff --git a/case-study.md b/case-study.md index 0ff7a23a..21d45d54 100644 --- a/case-study.md +++ b/case-study.md @@ -12,44 +12,74 @@ Я решил исправить эту проблему, оптимизировав эту программу. ## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: файл с количеством строк: 3250940 должен обрабатываться . +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: количество потребляемой памяти при обработке файла в 10 000 строк. ## Гарантия корректности работы оптимизированной программы Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. +Переписал тест используя Rspec matchers - изменил логику проверки соответствия ожидаемого и полученного результата (сравниваются значения по ключам хэшей) + +Также добавил проверку на количество потребляемой памяти, используя `perform_allocation` ## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений. -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +Вот как я построил `feedback_loop`: +1. Написал тест используя Rspec, с проверкой логики выполнения и количеству потребляемой памяти. *(после каждой оптимизации проверяю)* +2. Подготовил файлы для профилирования: + - memory_profiler.rb - для отдельного профилирования с помощью гема `Memory profiler` + - profilers.rb - генерирует отчёты для `Ruby-prof` в режиме профилирования аллокаций в формате Flat, Graph, Callstac, а также в режиме профилирования памяти в формате CallTree. И генерирует отчёты для `Stackprof` - пользовался редко, в основном смотрел визуализацию графа. +3. Установил Vallgrind + Massif для наблюдения за потреблением памяти в динамике ## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* +Изучаем, что показывает `GS.stat`, `ObjectSpace.count`. На 10 000 строках видим работу GC: +```log +:total_allocated_objects => 398540, +:total_freed_objects => 282935, +``` +Было освобождено достаточно много объектов, это говорит о том, что GC в Ruby v3.3.0 работает достаточно эффективно. Также это подтверждается замером с помощью `Memory profiler` - всего было использовано 767MB, а осталось 4,24KB (в уроке оставалось 5,76МБ) -Вот какие проблемы удалось найти и решить +Использование Memory profiler начинает показывать первые точки роста ### Ваша находка №1 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +- MemoryProfiler показывает главную точку роста - неэффективное добавление элементов в массив - при заполнении массивов sessions и users. +- переписал добавление элемента в массив без инициирования дополнительных элементов +- Метрика изменилась с 767МБ до 245МБ +- Неэффективное сложение массивов использовалось в двух местах: 506МБ и 18МБ. После оптимизации оба метода стали использовать одинаковое количество памяти - 530КБ, хотя в первом месте метод вызывается чаще в несколько раз. Интересно... ### Ваша находка №2 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика +- Stackprof, Graphviz.dot, Ruby-prof:flat - показывают, что по количеству аллокаций Array#select является точкой роста +- Чтобы улучшить производительность создал вспомогательную хэш-таблицу в которой сгруппировал сессии по *user_id* +- Метрика изменилась с 245МБ до 63МБ +- Array#select занимал 182МБ, теперь 26МБ + +### Ваша находка №3 +- MemoryProfiler показывает что второй случай со сложением массивов начинает являться точкой роста с 17МБ. Оптимизируем как и в первом кейсе. +- Метрика изменилась с 63МБ до 46МБ +- строка со сложением стала занимать 730КБ вместо 17МБ, количество всех массивов уменьшилось с 65028 до 60972 + +### Ваша находка №4 +- MemoryProfiler указывает на строку с парсингом даты. Оптимизируем +- Метрика изменилась с 46МБ до 35МБ +- строка с датой занимала *13МБ* и аллоцировала *171163 (!)* объекта. После оптимизации стала занимать *1МБ с 9929 объектами*. Очень наглядная оптимизация :smirk: + +### Ваша находка №5 +- Ruby-prof: flat, graph, callstack, KCacheGrind показывает что Array#all? является точкой роста. Избавляемся от его применения, так как этот метод создаёт ещё три дополнительных объекта +- Метрика практически не изменилась: с 35 до 34 МБ +- строка занимала 1МБ, стала 183КБ + +### Ваша находка №5 +- Дальнейшее профилирование указывает на split (это необходимо) и генерацию и накопление массивов с пользователями и сессиями. Valgrind Massif показывает что с увеличением количество строк расход памяти значительно возрастает +- Поэтому решаем переписать программу на потоковый режим работы. В память будет загружаться информация о пользователе и его сессиях, считаться статистика, и записываться в файл. После чего в память будет загружаться следующий пользователь. +- Метрика показывает результат выполнения 29МБ. Причём как на 10_000, так и на 3_250_940 строк (файл data_large.txt). Это означает, что мы вложились в бюджет (< 70МБ). Ура! :smiley: + +### Остальные находки +- В процессе оптимизации программы визуально выявляются ещё достаточное количество мест, которые, как кажется, можно оптимизировать. Но так как это будет хлопотно и нецелесообразно, а в бюджет уже укладываемся - то оставляем без изменений ## Результаты -В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. - -*Какими ещё результами можете поделиться* +- В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы с 767МБ до 29МБ и уложиться в заданный бюджет. +- Приобрел практические навыки в оптимизации используемой памяти в работе приложений +- На практике выявил сильную взаимосвязь между оптимизацией CPU и памятью +- Закрепил навыки по построению эффективного `Feedback-loop` ## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написал тест для проверки потребляемой памяти и количества аллоцированных объектов. diff --git a/task-2.rb b/task-2.rb index 02d45982..fa417da3 100644 --- a/task-2.rb +++ b/task-2.rb @@ -72,7 +72,7 @@ def work write_start_stats(file) - File.foreach('data_10k.txt') do |line| + File.foreach('data.txt') do |line| line_type, *fields = line.chomp.split(',') case line_type diff --git a/valgrind.log b/valgrind.log deleted file mode 100644 index 7eeb77ef..00000000 --- a/valgrind.log +++ /dev/null @@ -1,80 +0,0 @@ -==64848== Massif, a heap profiler -==64848== Copyright (C) 2003-2017, and GNU GPL'd, by Nicholas Nethercote -==64848== Using Valgrind-3.22.0-bd4db67b1d-20231031 and LibVEX; rerun with -h for copyright info -==64848== Command: /home/aaz/.rbenv/shims/ruby work.rb -==64848== Parent PID: 5927 -==64848== ---64848-- ---64848-- Valgrind options: ---64848-- --tool=massif ---64848-- --massif-out-file=massif.out ---64848-- --log-file=valgrind.log ---64848-- --ignore-fn=memalign ---64848-- --ignore-fn=aligned_alloc ---64848-- --verbose ---64848-- Contents of /proc/version: ---64848-- Linux version 6.5.0-28-generic (buildd@lcy02-amd64-098) (x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #29~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Apr 4 14:39:20 UTC 2 ---64848-- ---64848-- Arch and hwcaps: AMD64, LittleEndian, amd64-cx16-rdtscp-sse3-ssse3-avx-f16c-rdrand ---64848-- Page sizes: currently 4096, max supported 4096 ---64848-- Valgrind library directory: /snap/valgrind/160/usr/libexec/valgrind ---64848-- Massif: alloc-fns: ---64848-- Massif: malloc ---64848-- Massif: __builtin_new ---64848-- Massif: operator new(unsigned) ---64848-- Massif: operator new(unsigned long) ---64848-- Massif: __builtin_vec_new ---64848-- Massif: operator new[](unsigned) ---64848-- Massif: operator new[](unsigned long) ---64848-- Massif: calloc ---64848-- Massif: realloc ---64848-- Massif: memalign ---64848-- Massif: posix_memalign ---64848-- Massif: valloc ---64848-- Massif: operator new(unsigned, std::nothrow_t const&) ---64848-- Massif: operator new[](unsigned, std::nothrow_t const&) ---64848-- Massif: operator new(unsigned long, std::nothrow_t const&) ---64848-- Massif: operator new[](unsigned long, std::nothrow_t const&) ---64848-- Massif: ignore-fns: ---64848-- Massif: 0: memalign ---64848-- Massif: 1: aligned_alloc ---64848-- Reading syms from /usr/bin/env ---64848-- Reading syms from /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ---64848-- Reading syms from /snap/valgrind/160/usr/libexec/valgrind/massif-amd64-linux ---64848-- object doesn't have a dynamic symbol table ---64848-- Scheduler: using generic scheduler lock implementation. -==64848== embedded gdbserver: reading from /tmp/vgdb-pipe-from-vgdb-to-64848-by-aaz-on-??? -==64848== embedded gdbserver: writing to /tmp/vgdb-pipe-to-vgdb-from-64848-by-aaz-on-??? -==64848== embedded gdbserver: shared mem /tmp/vgdb-pipe-shared-mem-vgdb-64848-by-aaz-on-??? -==64848== -==64848== TO CONTROL THIS PROCESS USING vgdb (which you probably -==64848== don't want to do, unless you know exactly what you're doing, -==64848== or are doing some strange experiment): -==64848== /snap/valgrind/160/usr/libexec/valgrind/../../bin/vgdb --pid=64848 ...command... -==64848== -==64848== TO DEBUG THIS PROCESS USING GDB: start GDB like this -==64848== /path/to/gdb /home/aaz/.rbenv/shims/ruby -==64848== and then give GDB the following command -==64848== target remote | /snap/valgrind/160/usr/libexec/valgrind/../../bin/vgdb --pid=64848 -==64848== --pid is optional if only one valgrind process is running -==64848== ---64848-- Reading syms from /snap/valgrind/160/usr/libexec/valgrind/vgpreload_core-amd64-linux.so ---64848-- Reading syms from /snap/valgrind/160/usr/libexec/valgrind/vgpreload_massif-amd64-linux.so ---64848-- Reading syms from /usr/lib/x86_64-linux-gnu/libc.so.6 ---64848-- Considering /usr/lib/debug/.build-id/96/2015aa9d133c6cbcfb31ec300596d7f44d3348.debug .. ---64848-- .. build-id is valid -==64848== WARNING: new redirection conflicts with existing -- ignoring it ---64848-- old: 0x050b4c60 (memalign ) R-> (1011.0) 0x04e0abc4 memalign ---64848-- new: 0x050b4c60 (memalign ) R-> (1017.0) 0x04e0b341 aligned_alloc -==64848== WARNING: new redirection conflicts with existing -- ignoring it ---64848-- old: 0x050b4c60 (memalign ) R-> (1011.0) 0x04e0abc4 memalign ---64848-- new: 0x050b4c60 (memalign ) R-> (1017.0) 0x04e0b1d5 aligned_alloc -==64848== WARNING: new redirection conflicts with existing -- ignoring it ---64848-- old: 0x050b4c60 (memalign ) R-> (1011.0) 0x04e0abc4 memalign ---64848-- new: 0x050b4c60 (memalign ) R-> (1017.0) 0x04e0b341 aligned_alloc -==64848== WARNING: new redirection conflicts with existing -- ignoring it ---64848-- old: 0x050b4c60 (memalign ) R-> (1011.0) 0x04e0abc4 memalign ---64848-- new: 0x050b4c60 (memalign ) R-> (1017.0) 0x04e0b1d5 aligned_alloc ---64848-- REDIR: 0x50b40a0 (libc.so.6:malloc) redirected to 0x4e03103 (malloc) ---64848-- REDIR: 0x50b43e0 (libc.so.6:free) redirected to 0x4e06379 (free) ---64848-- REDIR: 0x50b5520 (libc.so.6:calloc) redirected to 0x4e0a70f (calloc) diff --git a/work_spec.rb b/work_spec.rb index a15d4d4d..7c427448 100644 --- a/work_spec.rb +++ b/work_spec.rb @@ -11,45 +11,51 @@ expect { work }.to perform_allocation(15 * 1024 * 1024).bytes end end + + it 'should not allocate more than 110000 objects' do + `head -n 1000 data_large.txt > data.txt` + + expect { work }.to perform_allocation(20000) + end end -# describe 'Correct workflow' do -# before do -# 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 - -# it 'returns correct result' do -# work -# result_data = JSON.parse(File.read('result.json')) -# 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"]}}}') - -# expect(result_data['totalUsers']).to eq(expected_result['totalUsers']) -# expect(result_data['uniqueBrowsersCount']).to eq(expected_result['uniqueBrowsersCount']) -# expect(result_data['totalSessions']).to eq(expected_result['totalSessions']) -# expect(result_data['allBrowsers']).to eq(expected_result['allBrowsers']) - -# result_data['usersStats'].each do |user, user_stat| -# expect(user_stat).to eq(expected_result['usersStats'][user]) -# end -# end -# end +describe 'Correct workflow' do + before do + 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 + + it 'returns correct result' do + work + result_data = JSON.parse(File.read('result.json')) + 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"]}}}') + + expect(result_data['totalUsers']).to eq(expected_result['totalUsers']) + expect(result_data['uniqueBrowsersCount']).to eq(expected_result['uniqueBrowsersCount']) + expect(result_data['totalSessions']).to eq(expected_result['totalSessions']) + expect(result_data['allBrowsers']).to eq(expected_result['allBrowsers']) + + result_data['usersStats'].each do |user, user_stat| + expect(user_stat).to eq(expected_result['usersStats'][user]) + end + end +end From 384b2bd01b8423b2e53c35fbbf7be24515aa804c Mon Sep 17 00:00:00 2001 From: Sergey Kochubey Date: Sun, 12 May 2024 23:36:16 +0300 Subject: [PATCH 8/8] test CPU --- case-study.md | 38 +++++++++++++++++++++++--------------- task-2.rb | 5 ++++- work_method.rb | 2 ++ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/case-study.md b/case-study.md index 21d45d54..f959bd79 100644 --- a/case-study.md +++ b/case-study.md @@ -28,55 +28,63 @@ 2. Подготовил файлы для профилирования: - memory_profiler.rb - для отдельного профилирования с помощью гема `Memory profiler` - profilers.rb - генерирует отчёты для `Ruby-prof` в режиме профилирования аллокаций в формате Flat, Graph, Callstac, а также в режиме профилирования памяти в формате CallTree. И генерирует отчёты для `Stackprof` - пользовался редко, в основном смотрел визуализацию графа. -3. Установил Vallgrind + Massif для наблюдения за потреблением памяти в динамике ## Вникаем в детали системы, чтобы найти главные точки роста +Смотрим, как обстоят дела с фризом строк. Его нет, и на 20 000 строках программа занимает 520МБ. Добавляем фриз, получаем - 515 МБ. Хорошо + Изучаем, что показывает `GS.stat`, `ObjectSpace.count`. На 10 000 строках видим работу GC: ```log :total_allocated_objects => 398540, :total_freed_objects => 282935, ``` -Было освобождено достаточно много объектов, это говорит о том, что GC в Ruby v3.3.0 работает достаточно эффективно. Также это подтверждается замером с помощью `Memory profiler` - всего было использовано 767MB, а осталось 4,24KB (в уроке оставалось 5,76МБ) +Было освобождено достаточно много объектов, это говорит о том, что GC в Ruby v3.3.0 работает достаточно эффективно. Также это подтверждается замером с помощью `Memory profiler` - всего было использовано 767 MB, а осталось 4,24 KB (в уроке оставалось 5,76 МБ) Использование Memory profiler начинает показывать первые точки роста ### Ваша находка №1 - MemoryProfiler показывает главную точку роста - неэффективное добавление элементов в массив - при заполнении массивов sessions и users. - переписал добавление элемента в массив без инициирования дополнительных элементов -- Метрика изменилась с 767МБ до 245МБ -- Неэффективное сложение массивов использовалось в двух местах: 506МБ и 18МБ. После оптимизации оба метода стали использовать одинаковое количество памяти - 530КБ, хотя в первом месте метод вызывается чаще в несколько раз. Интересно... +- Метрика изменилась с 767 МБ до 245 МБ +- Неэффективное сложение массивов использовалось в двух местах: 506 МБ и 18 МБ. После оптимизации оба метода стали использовать одинаковое количество памяти - 530 КБ, хотя в первом месте метод вызывается чаще в несколько раз. Интересно... ### Ваша находка №2 - Stackprof, Graphviz.dot, Ruby-prof:flat - показывают, что по количеству аллокаций Array#select является точкой роста - Чтобы улучшить производительность создал вспомогательную хэш-таблицу в которой сгруппировал сессии по *user_id* -- Метрика изменилась с 245МБ до 63МБ -- Array#select занимал 182МБ, теперь 26МБ +- Метрика изменилась с 245 МБ до 63 МБ +- Array#select занимал 182 МБ, теперь 26 МБ ### Ваша находка №3 -- MemoryProfiler показывает что второй случай со сложением массивов начинает являться точкой роста с 17МБ. Оптимизируем как и в первом кейсе. -- Метрика изменилась с 63МБ до 46МБ -- строка со сложением стала занимать 730КБ вместо 17МБ, количество всех массивов уменьшилось с 65028 до 60972 +- MemoryProfiler показывает что второй случай со сложением массивов начинает являться точкой роста с 17 МБ. Оптимизируем как и в первом кейсе. +- Метрика изменилась с 63 МБ до 46 МБ +- строка со сложением стала занимать 730 КБ вместо 17 МБ, количество всех массивов уменьшилось с 65028 до 60972 ### Ваша находка №4 - MemoryProfiler указывает на строку с парсингом даты. Оптимизируем -- Метрика изменилась с 46МБ до 35МБ -- строка с датой занимала *13МБ* и аллоцировала *171163 (!)* объекта. После оптимизации стала занимать *1МБ с 9929 объектами*. Очень наглядная оптимизация :smirk: +- Метрика изменилась с 46 МБ до 35 МБ +- строка с датой занимала *13 МБ* и аллоцировала *171163 (!)* объекта. После оптимизации стала занимать *1 МБ с 9929 объектами*. Очень наглядная оптимизация :smirk: ### Ваша находка №5 - Ruby-prof: flat, graph, callstack, KCacheGrind показывает что Array#all? является точкой роста. Избавляемся от его применения, так как этот метод создаёт ещё три дополнительных объекта - Метрика практически не изменилась: с 35 до 34 МБ -- строка занимала 1МБ, стала 183КБ +- выполнение кода в строке занимало 1 МБ, стало 183 КБ ### Ваша находка №5 -- Дальнейшее профилирование указывает на split (это необходимо) и генерацию и накопление массивов с пользователями и сессиями. Valgrind Massif показывает что с увеличением количество строк расход памяти значительно возрастает +- Дальнейшее профилирование указывает на split (это необходимо) и генерацию и накопление массивов с пользователями и сессиями. - Поэтому решаем переписать программу на потоковый режим работы. В память будет загружаться информация о пользователе и его сессиях, считаться статистика, и записываться в файл. После чего в память будет загружаться следующий пользователь. -- Метрика показывает результат выполнения 29МБ. Причём как на 10_000, так и на 3_250_940 строк (файл data_large.txt). Это означает, что мы вложились в бюджет (< 70МБ). Ура! :smiley: +- Метрика показывает результат выполнения 21 МБ. Причём как на 10_000, так и на 3_250_940 строк (файл data_large.txt). Это означает, что мы вложились в бюджет (< 70 МБ). Ура! :smiley: ### Остальные находки - В процессе оптимизации программы визуально выявляются ещё достаточное количество мест, которые, как кажется, можно оптимизировать. Но так как это будет хлопотно и нецелесообразно, а в бюджет уже укладываемся - то оставляем без изменений +- После оптимизации программы решил проверить как влияет фриз строк после оптимизации, и выяснил интересный момент - на 100 000 строках: + - 146 МБ - без фриза + - 139 МБ - с фризом +Получается, данная оптимизация не зависит от количества данных, а зависит от количества использования String в программе? + +### Замер скорости +- После оптимизации по памяти провели тестирование на времени выполнения программы. С файлом data_large.txt оно составило - **всего лишь 22 секунды(!)** против 32 сек до оптимизации памяти. Возможно, разница больше, так как замеры выполнялись на разных конфигурациях VM ## Результаты -- В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы с 767МБ до 29МБ и уложиться в заданный бюджет. +- В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы с 767 МБ до 29 МБ и уложиться в заданный бюджет < 70 MB. - Приобрел практические навыки в оптимизации используемой памяти в работе приложений - На практике выявил сильную взаимосвязь между оптимизацией CPU и памятью - Закрепил навыки по построению эффективного `Feedback-loop` diff --git a/task-2.rb b/task-2.rb index fa417da3..659f5574 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'json' class User @@ -72,7 +74,7 @@ def work write_start_stats(file) - File.foreach('data.txt') do |line| + File.foreach('data_large.txt') do |line| line_type, *fields = line.chomp.split(',') case line_type @@ -135,3 +137,4 @@ def write_end_stats(file, user, report) file.puts('}') end +work \ No newline at end of file diff --git a/work_method.rb b/work_method.rb index e18de927..daf4c037 100644 --- a/work_method.rb +++ b/work_method.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'json' class User