Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
9c0d6ad
Decomposed logic to separate files(logic, test);
Apr 6, 2019
0aeb760
Moved test data files to spec/support;
Apr 6, 2019
f83af7a
refactored preconditions p. I
Apr 6, 2019
d353c7a
refactored preconditions p. II
Apr 6, 2019
dfbc0b2
generated test files of different sizes(65kb,125kb,250kb,0.5mb,1mb) a…
Apr 6, 2019
cbb844c
refactored work to accept filename;
Apr 6, 2019
40b5e18
regression check;
Apr 6, 2019
0635a28
Refactoring of asymptotic(changing storing dir and naming);
Apr 6, 2019
b2018d9
Replacing $stdout as a template method for flexibility of using in di…
Apr 7, 2019
19a96c7
Moved stackprof WALL measure to step1 dir;
Apr 7, 2019
f6c65ce
RubyProf wall measure Step1;
Apr 7, 2019
10b2ed9
fix README
Apr 7, 2019
aff7bb5
Refactored algorithm complexity(Got rid from unoptimized select searc…
Apr 10, 2019
7208966
Commited asymptotic after step1 optimization;
Apr 10, 2019
3367e3d
rubyprof and stackprof measures;
Apr 10, 2019
a662afe
step2 - replaced array.all? for unique elements with set;
Apr 10, 2019
8db9cb4
step3 metrics;
Apr 10, 2019
e8c26ea
step3 removed redundant logic for parsing Date;
Apr 10, 2019
95d6198
step4 metrics;
Apr 10, 2019
fc0fe35
step4 stackprof object and rubyprof alloc metrics;
Apr 10, 2019
cf89f45
step4 valgrind massif measure;
Apr 10, 2019
73e4947
step4 qcachegrind + rubyprof patched;
Apr 10, 2019
1febd73
step5 removed redundant arrays
Apr 10, 2019
6f786c9
step6 frozen_string_literal: true;
Apr 10, 2019
3333e42
step6 frozen_string_literal: true;
Apr 10, 2019
3784047
step7 not finished yet but on the finish line;
Apr 11, 2019
fad6568
Fix of arrays bug;
Apr 13, 2019
ae2cc7c
refactoring;
Apr 13, 2019
b7caa1c
refactoring;
Apr 13, 2019
6389abf
refactoring;
Apr 13, 2019
eee7923
refactoring;
Apr 13, 2019
99b9ab8
refactoring;
Apr 13, 2019
d219ac8
refactoring;
Apr 13, 2019
a51c148
refactoring;
Apr 13, 2019
718d4cc
step8
Apr 14, 2019
439d48d
step8
Apr 14, 2019
6d12ee7
step8 array.concat problem;
Apr 14, 2019
2adb3af
step8 array.concat problem;
Apr 14, 2019
91e3823
step9 used set for reducing memory usage with allocating memory for c…
Apr 18, 2019
5e2a295
refactoring;
Apr 19, 2019
7c541c7
refactoring;
Apr 20, 2019
51f6958
great refactoring;
Apr 20, 2019
18a919b
Final step results;
Apr 20, 2019
8c1e5ce
Final step results;
Apr 20, 2019
a576a98
Final step results;
Apr 20, 2019
57c2786
Final step results;
Apr 20, 2019
b9afe72
Final step results;
Apr 20, 2019
c2a6919
Final step results;
Apr 20, 2019
08c72f1
Final step results;
Apr 20, 2019
48c1fe4
Final step results;
Apr 20, 2019
d5d8282
Final step results;
Apr 20, 2019
105a469
Final step results;
Apr 20, 2019
f0cf3a4
Final step results;
Apr 20, 2019
1ead1a2
Decomposed by files;
Apr 20, 2019
bd996eb
Decomposed by files;
Apr 20, 2019
6d0bdf2
frozen_string_literal: true
Apr 21, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.idea/
data.txt
result.json
data_large.txt
187 changes: 102 additions & 85 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,85 +1,102 @@
### Note
*Для работы скрипта требуется Ruby 2.4+*

# Задание №1
В файле `task-1.rb` находится ruby-программа, которая выполняет обработку данных из файла.

В файл встроен тест, который показывает, как программа должна работать.

С помощью этой программы нужно обработать файл данных `data_large.txt`.

**Проблема в том, что это происходит слишком долго, дождаться пока никому не удавалось.**


## Задача
- Оптимизировать эту программу, выстроив процесс согласно "общему фреймворку оптимизации" из первой лекции;
- Профилировать программу с помощью инструментов, с которыми мы познакомились в первой лекции;
- Добиться того, чтобы программа корректно обработала файл `data_large.txt`;
- Написать кейс-стади о вашей оптимизации по шаблону `case-study-template.md`.

## Сдача задания
Для сдачи задания нужно сделать `PR` в этот репозиторий.

В `PR`
- должны быть внесены оптимизации в `task-1.rb`;
- должен быть файл `case-study.md` с описанием проделанной оптимизации;


# Комментарии

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

С какими искушениями вы сталкиваететь:
- вы “с ходу” видите проблемы в коде и у вас возникает импульс потратить время на их рефакторинг;
- вы “с ходу” замечаете какие-то непроизводительные идиомы, и у вас возникает соблазн их сразу исправить;

Эти искушения типичны и часто возникают в моделируемой ситуации.

Их риски:
- перед рефакторингом “очевидных” косяков не написать тестов и незаметно внести регрессию;
- потратить время на рефакторинг, хотя время было только на оптимизацию;
- исправить все очевидные на глаз проблемы производительности, не получить заметного результата, решить что наверное просто Ruby слишком медленный для этой задачи

## Советы
- Найдите объём данных, на которых программа отрабатывает достаточно быстро - это позволит вам выстроить фидбек-луп; если улучшите метрику для части данных, то улучшите и для полного объёма данных;
- Попробуйте прикинуть ассимтотику роста времени работы в зависимости от объёма входных данных (попробуйте объём x, 2x, 4x, 8x)
- Оцените, как долго программа будет обрабатывать полный обём данных
- Оцените, сколько времени занимает работа GC

Возможно, что оптимизаций только памяти вам не хватит, чтобы довести производительность системы до приемлемой.

Если вы придёте к такому выводу, то возможны такие варианты:
- попробуйте использовать какой-нибудь из рассмотренных нами профилировщиков в режиме профилирования CPU (wall), найти точки роста и оптимизировать их;
- подойдёт и вариант, если вы с помощью профилирования найдёте несколько главных точек роста, оптимизируете их, покажете, что дальнейшие оптимизации только памяти не приведут к желаемому результату, и опишете это в case-study.

## Что можно делать
- рефакторить код
- рефакторить/дописывать тесты
- разбивать скрипт на несколько файлов

## Что нужно делать
- исследовать предложенную вам на рассмотрение систему
- построить фидбек-луп, который позволит вам быстро тестировать гипотезы и измерять их эффект
- применить инструменты профилирования памяти, интроспекции GC, чтобы найти самые горячие проблемы по памяти, оценить кол-во времени которое уходит на сборку мусора
- выписывать в case-study несколько пунктов: каким профилировщиком вы нашли точку роста, как её оптимизировали, какой получили прирост метрики;

## Что не нужно делать
- переписывать с нуля
- забивать на выстраивание фидбек-лупа
- вносить оптимизации по наитию, без профилировщика и без оценки эффективности

## Основная польза задания
Главная польза этого задания - попрактиковаться в применении грамотного подхода к оптимизации, почуствовать этот процесс:
- как взяли незнакомую систему и исследовали её
- как выстроили фидбек луп
- как с помощью профилировщиков нашли что именно даст вам наибольший эффект
- как быстро протестировали гипотезу, получили измеримый результат и зафиксировали его
- как в итоге написали небольшой отчёт об успешных шагах этого процесса

## PS
Обсудим с Виталием, возможно уделю время на 2й лекции разбору подобной ситуации, а заданием 2й недели будет дооптимизировать эту программу, уже используя инструментарий оптимизации CPU.

## PPS
Делайте задание, изучайте предложенную систему, смотрите на неё под разными углами с помощью разных инстурментов. Оптимизируйте. Попрактикуйтесь в этой работе
# Users statistics calculator

## Problem
In our project we have a problem of calculating statistics for users. Report works with a large files without any signs
of work could be done in reasonable time. That is why we think that we should check this report for bottlenecks and fix
them if they are exist.

## Warranty of correctness of work
We have tests that verify correct logic of the algorithm and assure that we won't go wrong.

## Feedback loop
We implemented logic that measures our algorithm by iterations per time on a sample of data of 65kb size. So after
evaluation of this metric we will seek for bottlenecks with help of various cpu and memory profilers. When we'll find any
of these bottlenecks and fix them we'll reevaluate this metric and repeat these steps until we'll have algorithm that don't
bother us.

## Step 1
I tried to turn off GC in our sample test and check whether memory issues could cause such problems with the speed.
Metrics didn't change a lot hence i understood that main problem for now is in algorithm by itself and we should seek
where CPU works mostly. Maybe we could apply certain optimization there. Additionally we disabled GC to be focused only
in algorithm issue.

After all measures we figured out the problem in sequential looping over an array without any using of ids for fast search.

## Step 2
After refactoring in step1 where i replaced sequential looping through array with using of ids i reduced algorithm
complexity at least x3. I applied stackprof and rubyprof once more for checking what we've got for now and next problem we
saw using of all? method for checking of existing sessions in presaved array.

## Step 3
We applied set data structure for making unique assembling for us. After next measures we saw problem with date parsing
in the final part of the report.

## Step 4
Exactly in our case this part of algorithm do nothing so we excluded it without any breaking of law. Finally we are at
the point where we can't gain much of performance with simple refactorings. It's a good moment for capturing our memory
situation in terms of waisting it. So after measures i see that we have a lot of redundant array allocations.

## Step 5
After removing all obvious places of redundant array allocations i used frozen_string_literal for avoiding allocations of
redundant strings

## Step 6 - Final result
All my next steps were connected with looking why memory using grows lineally to 400mb score and stops on that mark. The
problem was that profilers didn't tell much about that just were showing that number of allocating object were extremely
high. Finally i have figured out with massif-visualizer that all allocating memory related with collecting browsers and
i fixed it with using set. After that i used refactoring for decomposing all logic by domains area.

Before refactoring i had result with using 37mb total for large file
![Before refactoring](/optimizations/step10/before.png)
After refactoring memory usage grew but i think that in this case we don't need to dig deeper. This result is ok for us.
![After refactoring](/optimizations/step10/after.png)

Assuming in the result we have parser that handles the task in `9` sec with `46 mb` used.

Final asymptotic

Warming up --------------------------------------
65kb 15.000 i/100ms
125kb 8.000 i/100ms
250kb 4.000 i/100ms
0.5m 2.000 i/100ms
1m 1.000 i/100ms
Calculating -------------------------------------
65kb 159.165 (± 4.3%) i/s - 330.000 in 2.078575s
125kb 81.522 (± 8.3%) i/s - 168.000 in 2.095915s
250kb 45.831 (± 3.6%) i/s - 92.000 in 2.013734s
0.5m 24.744 (± 2.9%) i/s - 50.000 in 2.023426s
1m 12.841 (± 3.3%) i/s - 26.000 in 2.028261s
with 100.0% confidence

Comparison:
65kb: 159.2 i/s
125kb: 81.5 i/s - 1.95x (± 0.19) slower
250kb: 45.8 i/s - 3.47x (± 0.20) slower
0.5m: 24.7 i/s - 6.43x (± 0.34) slower
1m: 12.8 i/s - 12.39x (± 0.65) slower
with 100.0% confidence

Comparing with what we had in the beginning

Warming up --------------------------------------
65kb 1.000 i/100ms
125kb 1.000 i/100ms
250kb 1.000 i/100ms
0.5m 1.000 i/100ms
1m 1.000 i/100ms
Calculating -------------------------------------
65kb 16.952 (± 4.2%) i/s - 34.000 in 2.015597s
125kb 5.178 (± 2.1%) i/s - 11.000 in 2.125509s
250kb 1.333 (± 2.0%) i/s - 3.000 in 2.251281s
0.5m 0.370 (± 0.0%) i/s - 1.000 in 2.703453s
1m 0.077 (± 0.0%) i/s - 1.000 in 13.051390s
with 100.0% confidence

Comparison:
65kb: 17.0 i/s
125kb: 5.2 i/s - 3.27x (± 0.15) slower
250kb: 1.3 i/s - 12.72x (± 0.68) slower
0.5m: 0.4 i/s - 45.84x (± 1.94) slower
1m: 0.1 i/s - 221.22x (± 9.44) slower
with 100.0% confidence
32 changes: 32 additions & 0 deletions lib/_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require 'forwardable'
require_relative 'user'
require_relative 'report'

class Parser
@parsed_user = User.new

class << self
extend Forwardable

attr_reader :parsed_user

def parse_user(first_name, last_name)
@parsed_user.name = "#{first_name} #{last_name}"
end

def parse_session(browser, time, date)
@parsed_user.sessions_count += 1

@parsed_user.total_time += time
@parsed_user.longest_session = time if @parsed_user.longest_session < time
@parsed_user.browsers << browser
Report.unique_browsers << browser

@parsed_user.dates << date
end

private
def_delegator :@parsed_user, :name, :parsed_exists?
def_delegator :@parsed_user, :reset, :clear_cache
end
end
33 changes: 33 additions & 0 deletions lib/parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

require_relative '_parser'
require 'pry-byebug'

$support_dir = File.expand_path('../../spec/support', __FILE__ )
$optimizations_dir = File.expand_path('../../optimizations', __FILE__ )

def work(filename)
File.open("#{$support_dir}/result.json", 'w') do |f|
Report.prepare(f)

IO.foreach("#{$support_dir}/#{filename}") do |cols|
row = cols.split(',')

if cols.start_with?('user')
if Parser.parsed_exists?
Report.add_parsed(Parser.parsed_user)

Parser.clear_cache
end

Parser.parse_user(row[2], row[3])
else
Parser.parse_session(row[3], row[4].to_i, row[5].strip)
end
end

Report.add_parsed(Parser.parsed_user)

Report.add_analyse
end
end
49 changes: 49 additions & 0 deletions lib/report.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

require 'oj'
require 'set'
require 'json'

class Report
class << self
attr_reader :unique_browsers, :total_sessions, :total_users

def prepare(file)
@file = file
@unique_browsers = Set.new
@total_sessions = @total_users = 0

@file.write("{\"usersStats\":{")
end

def add_parsed(user)
browsers = user.prepare

@total_sessions += user.sessions_count
@total_users += 1

formatted = {
"sessionsCount": user.sessions_count,
"totalTime": "#{user.total_time} min.",
"longestSession": "#{user.longest_session} min.",
"browsers": browsers,
"usedIE": user.used_ie,
"alwaysUsedChrome": user.used_only_chrome,
"dates": user.dates
}

@file.write("\"#{user.name}\":#{Oj.dump(formatted, mode: :compat)},")
end

def add_analyse
analyze = {
"totalUsers": @total_users,
"uniqueBrowsersCount": @unique_browsers.size,
"totalSessions": @total_sessions,
"allBrowsers": @unique_browsers.sort.join(',').upcase!
}.to_json.tr!('{', '') << "}\n"

@file.write(analyze)
end
end
end
37 changes: 37 additions & 0 deletions lib/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

class User
attr_accessor :name, :sessions_count, :total_time, :longest_session,
:browsers, :dates, :used_ie, :used_only_chrome

def initialize
nullify

@dates = []
@browsers = []
end

def prepare
@dates.sort!.reverse!

browsers = @browsers.sort!.join(', ').upcase!

@used_only_chrome = true if browsers.end_with?('CHROME')
@used_ie = true if !@used_only_chrome && browsers.include?('INTERNET')

browsers
end

def reset
nullify

@dates.clear
@browsers.clear
end

private
def nullify
@longest_session = @total_time = @sessions_count = 0
@used_only_chrome = @used_ie = false
end
end
29 changes: 29 additions & 0 deletions optimizations/rubyprof.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require_relative '../lib/parser'
require_relative '../spec/stdout_to_file'
require 'ruby-prof'

GC.disable

save_stdout_to_file('rubyprof_wall.txt') do
RubyProf.measure_mode = RubyProf::WALL_TIME

result = RubyProf.profile do
work('data_65kb.txt')
end

printer = RubyProf::FlatPrinter.new(result)
printer.print($stdout)
end

GC.enable

save_stdout_to_file('rubyprof_alloc.txt') do
RubyProf.measure_mode = RubyProf::ALLOCATIONS

result = RubyProf.profile do
work('data_65kb.txt')
end

printer = RubyProf::FlatPrinter.new(result)
printer.print($stdout)
end
11 changes: 11 additions & 0 deletions optimizations/rubyprof_patched.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require_relative '../lib/parser'
require 'ruby-prof'

RubyProf.measure_mode = RubyProf::MEMORY

result = RubyProf.profile do
work('data_65kb.txt')
end

printer = RubyProf::CallTreePrinter.new(result)
printer.print(path: "#{$optimizations_dir}", profile: 'profile')
Loading