Skip to content

Commit b99fade

Browse files
Евгений ШумилинЕвгений Шумилин
authored andcommitted
[HW1] CPU optimization
1 parent 3f9982d commit b99fade

File tree

9 files changed

+204
-113
lines changed

9 files changed

+204
-113
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.idea
2+
data_*.txt
3+
result.json

.ruby-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.1.2

case-study-template.md

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,57 @@
1212
Я решил исправить эту проблему, оптимизировав эту программу.
1313

1414
## Формирование метрики
15-
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика*
15+
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику:
16+
Изменение времени обработки файла средних размеров(30000 строк)
1617

1718
## Гарантия корректности работы оптимизированной программы
1819
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации.
1920

2021
## Feedback-Loop
21-
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось*
22+
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 0.3 сек
2223

23-
Вот как я построил `feedback_loop`: *как вы построили feedback_loop*
24+
Вот как я построил `feedback_loop`:
25+
- Профилирование и поиск наиболее затратных мест в программе
26+
- Оптимизация этих мест
27+
- Зеленые тесты
28+
- Итоговый результат
2429

2530
## Вникаем в детали системы, чтобы найти главные точки роста
26-
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались*
31+
Для того, чтобы найти "точки роста" для оптимизации я воспользовался RubyProf. Попробовал разные форматы, но самым удобным показался callgrind.
2732

2833
Вот какие проблемы удалось найти и решить
2934

3035
### Ваша находка №1
31-
- какой отчёт показал главную точку роста
32-
- как вы решили её оптимизировать
33-
- как изменилась метрика
34-
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
36+
- Array#select
37+
- Было решено на этапе чтения файла сразу готовить хэш с массивом сессий с ключем user_id, чтобы уйти от сложности O(n^2)
38+
- Для файла с 30k строк:
39+
Было: 66.977 s
40+
Стало: 1.076115 s
41+
- Проблема перестала быть точкой роста
3542

36-
### Ваша находка №2
37-
- какой отчёт показал главную точку роста
38-
- как вы решили её оптимизировать
39-
- как изменилась метрика
40-
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
4143

42-
### Ваша находка №X
43-
- какой отчёт показал главную точку роста
44-
- как вы решили её оптимизировать
45-
- как изменилась метрика
46-
- как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста?
44+
### Ваша находка №2
45+
- Array#each в месте где происходит подсчёт количества уникальных браузеров
46+
- Использовать map и uniq
47+
- Для файла с 30 к строк:
48+
Было: 1.076115 s
49+
Стало: 0.781335 s
50+
- Проблема перестала быть точкой роста
51+
52+
### Ваша находка №3
53+
- Следующая точка роста Object#collect_stats_from_users
54+
- Решил сократить количество вызовов метода, убрал лишние вызовы map внутри, также убрал merge с вызовом блока.
55+
- Для файла с 30 к строк:
56+
Было: 1.076115 s
57+
Стало: 0.349385 s
58+
- Проблема перестала быть точкой роста
4759

4860
## Результаты
4961
В результате проделанной оптимизации наконец удалось обработать файл с данными.
50-
Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет.
62+
Удалось улучшить метрику системы с 66 секунд для файла с 30к строк до 0.3 секунд и уложиться в заданный бюджет.
5163

5264
*Какими ещё результами можете поделиться*
5365

5466
## Защита от регрессии производительности
55-
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали*
67+
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы был написан perfomance тест для файла с 30к строк.
5668

data_large.txt.gz

-30.6 MB
Binary file not shown.

perfomance_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require 'rspec'
2+
require 'rspec-benchmark'
3+
4+
require_relative 'task-1.rb'
5+
6+
RSpec.configure do |config|
7+
config.include RSpec::Benchmark::Matchers
8+
end
9+
10+
describe 'data_30_000.txt' do
11+
it 'performs less than 100 ms' do
12+
expect do
13+
work('data30_000.txt')
14+
end.to perform_under(100).ms.warmup(2).times.sample(10).times
15+
end
16+
end

prof.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
require 'ruby-prof'
2+
require_relative 'task-1.rb'
3+
4+
RubyProf.measure_mode = RubyProf::WALL_TIME
5+
6+
result = RubyProf.profile do
7+
work('data30_000.txt', disable_gc: false)
8+
end
9+
10+
printer = RubyProf::FlatPrinter.new(result)
11+
printer.print(File.open('ruby-prof-flat_final.txt', 'w+'))
12+
13+
# printer = RubyProf::GraphHtmlPrinter.new(result)
14+
# printer.print(File.open("ruby-prof-graph.html", "w+"))
15+
16+
# printer4 = RubyProf::CallTreePrinter.new(result)
17+
# printer4.print(:profile => 'callgrind')
18+

ruby-prof-flat_final.txt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
Measure Mode: wall_time
2+
Thread ID: 80
3+
Fiber ID: 60
4+
Total: 0.349385
5+
Sort by: self_time
6+
7+
%self total self wait child calls name location
8+
19.68 0.276 0.069 0.000 0.207 3 Array#each
9+
17.93 0.104 0.063 0.000 0.042 13780 Array#map
10+
13.74 0.048 0.048 0.000 0.000 30001 String#split
11+
9.10 0.046 0.032 0.000 0.015 25408 Object#parse_session /Users/evgeniyshumilin/projects/rails-optimization-task1/task-1.rb:25
12+
8.26 0.029 0.029 0.000 0.000 175408 Hash#[]
13+
6.02 0.021 0.021 0.000 0.000 175408 Array#[]
14+
3.27 0.011 0.011 0.000 0.000 50816 String#upcase
15+
3.22 0.011 0.011 0.000 0.000 9185 Array#sort
16+
2.70 0.009 0.009 0.000 0.000 19175 String#=~
17+
2.42 0.008 0.008 0.000 0.000 4597 Hash#[]=
18+
1.93 0.007 0.007 0.000 0.000 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json
19+
1.51 0.012 0.005 0.000 0.007 4592 Array#any?
20+
1.28 0.007 0.004 0.000 0.002 4592 Object#parse_user /Users/evgeniyshumilin/projects/rails-optimization-task1/task-1.rb:16
21+
1.27 0.004 0.004 0.000 0.000 30000 Array#<<
22+
1.12 0.004 0.004 0.000 0.000 4593 Array#join
23+
1.08 0.004 0.004 0.000 0.000 25408 String#to_i
24+
0.83 0.003 0.003 0.000 0.000 2 Array#uniq
25+
0.79 0.006 0.003 0.000 0.003 4592 Array#all?
26+
0.77 0.003 0.003 0.000 0.000 18368 User#sessions
27+
0.65 0.004 0.002 0.000 0.002 4592 Class#new
28+
0.50 0.002 0.002 0.000 0.000 4592 User#initialize /Users/evgeniyshumilin/projects/rails-optimization-task1/task-1.rb:10
29+
0.39 0.001 0.001 0.000 0.000 9184 User#attributes
30+
0.32 0.001 0.001 0.000 0.000 4592 Array#max
31+
0.30 0.001 0.001 0.000 0.000 4592 Array#reverse
32+
0.24 0.001 0.001 0.000 0.000 4592 Array#sum
33+
0.23 0.001 0.001 0.000 0.000 1 <Class::IO>#write
34+
0.21 0.001 0.001 0.000 0.000 4595 Array#count
35+
0.20 0.001 0.001 0.000 0.000 1 <Class::IO>#read
36+
0.04 0.349 0.000 0.000 0.349 1 Object#work /Users/evgeniyshumilin/projects/rails-optimization-task1/task-1.rb:42
37+
0.01 0.349 0.000 0.000 0.349 1 [global]# prof.rb:7
38+
0.00 0.145 0.000 0.000 0.145 1 Object#parse_data /Users/evgeniyshumilin/projects/rails-optimization-task1/task-1.rb:108
39+
0.00 0.016 0.000 0.000 0.016 1 Enumerable#group_by
40+
0.00 0.000 0.000 0.000 0.000 1 JSON::Ext::Generator::State#initialize
41+
0.00 0.125 0.000 0.000 0.125 1 Object#collect_stats_from_users /Users/evgeniyshumilin/projects/rails-optimization-task1/task-1.rb:35
42+
43+
* recursively called methods
44+
45+
Columns are:
46+
47+
%self - The percentage of time spent in this method, derived from self_time/total_time.
48+
total - The time spent in this method and its children.
49+
self - The time spent in this method.
50+
wait - The amount of time this method waited for other threads.
51+
child - The time spent in this method's children.
52+
calls - The number of times this method was called.
53+
name - The name of the method.
54+
location - The location of the method.
55+
56+
The interpretation of method names is:
57+
58+
* MyObject#test - An instance method "test" of the class "MyObject"
59+
* <Object:MyObject>#test - The <> characters indicate a method on a singleton class.
60+

task-1.rb

Lines changed: 40 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
# Deoptimized version of homework task
1+
# frozen_string_literal: true
22

33
require 'json'
44
require 'pry'
55
require 'date'
6-
require 'minitest/autorun'
76

87
class User
98
attr_reader :attributes, :sessions
@@ -14,46 +13,36 @@ def initialize(attributes:, sessions:)
1413
end
1514
end
1615

17-
def parse_user(user)
18-
fields = user.split(',')
19-
parsed_result = {
16+
def parse_user(fields)
17+
{
2018
'id' => fields[1],
2119
'first_name' => fields[2],
2220
'last_name' => fields[3],
23-
'age' => fields[4],
21+
'age' => fields[4]
2422
}
2523
end
2624

27-
def parse_session(session)
28-
fields = session.split(',')
29-
parsed_result = {
25+
def parse_session(fields)
26+
{
3027
'user_id' => fields[1],
3128
'session_id' => fields[2],
3229
'browser' => fields[3],
3330
'time' => fields[4],
34-
'date' => fields[5],
31+
'date' => fields[5]
3532
}
3633
end
3734

3835
def collect_stats_from_users(report, users_objects, &block)
3936
users_objects.each do |user|
40-
user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}"
41-
report['usersStats'][user_key] ||= {}
42-
report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user))
37+
user_key = "#{user.attributes['first_name']} #{user.attributes['last_name']}"
38+
report['usersStats'][user_key] = block.call(user)
4339
end
4440
end
4541

46-
def work
47-
file_lines = File.read('data.txt').split("\n")
42+
def work(filename = 'data_large.txt', disable_gc: false)
43+
GC.disable if disable_gc
4844

49-
users = []
50-
sessions = []
51-
52-
file_lines.each do |line|
53-
cols = line.split(',')
54-
users = users + [parse_user(line)] if cols[0] == 'user'
55-
sessions = sessions + [parse_session(line)] if cols[0] == 'session'
56-
end
45+
users, sessions = parse_data(filename)
5746

5847
# Отчёт в json
5948
# - Сколько всего юзеров +
@@ -75,11 +64,7 @@ def work
7564
report[:totalUsers] = users.count
7665

7766
# Подсчёт количества уникальных браузеров
78-
uniqueBrowsers = []
79-
sessions.each do |session|
80-
browser = session['browser']
81-
uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser }
82-
end
67+
uniqueBrowsers = sessions.map { |s| s['browser'] }.uniq
8368

8469
report['uniqueBrowsersCount'] = uniqueBrowsers.count
8570

@@ -93,84 +78,46 @@ def work
9378
.uniq
9479
.join(',')
9580

81+
grouped_sessions = sessions.group_by { |session| session['user_id'] }
9682
# Статистика по пользователям
97-
users_objects = []
9883

99-
users.each do |user|
100-
attributes = user
101-
user_sessions = sessions.select { |session| session['user_id'] == user['id'] }
102-
user_object = User.new(attributes: attributes, sessions: user_sessions)
103-
users_objects = users_objects + [user_object]
84+
users_objects = users.map do |user|
85+
User.new(attributes: user, sessions: grouped_sessions[user['id']] || [])
10486
end
10587

10688
report['usersStats'] = {}
10789

10890
# Собираем количество сессий по пользователям
10991
collect_stats_from_users(report, users_objects) do |user|
110-
{ 'sessionsCount' => user.sessions.count }
111-
end
112-
113-
# Собираем количество времени по пользователям
114-
collect_stats_from_users(report, users_objects) do |user|
115-
{ 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' }
116-
end
117-
118-
# Выбираем самую длинную сессию пользователя
119-
collect_stats_from_users(report, users_objects) do |user|
120-
{ 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' }
121-
end
122-
123-
# Браузеры пользователя через запятую
124-
collect_stats_from_users(report, users_objects) do |user|
125-
{ 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') }
126-
end
127-
128-
# Хоть раз использовал IE?
129-
collect_stats_from_users(report, users_objects) do |user|
130-
{ 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } }
131-
end
132-
133-
# Всегда использовал только Chrome?
134-
collect_stats_from_users(report, users_objects) do |user|
135-
{ 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } }
136-
end
137-
138-
# Даты сессий через запятую в обратном порядке в формате iso8601
139-
collect_stats_from_users(report, users_objects) do |user|
140-
{ 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } }
92+
browsers_upcased = user.sessions.map { |s| s['browser'].upcase }
93+
sessions_time = user.sessions.map { |s| s['time'].to_i }
94+
{
95+
'sessionsCount' => user.sessions.count,
96+
'totalTime' => "#{sessions_time.sum} min.",
97+
'longestSession' => "#{sessions_time.max} min.",
98+
'browsers' => browsers_upcased.sort.join(', '),
99+
'usedIE' => browsers_upcased.any? { |b| b =~ /INTERNET EXPLORER/ },
100+
'alwaysUsedChrome' => browsers_upcased.all? { |b| b =~ /CHROME/ },
101+
'dates' => user.sessions.map { |s| s['date'] }.sort.reverse
102+
}
141103
end
142104

143105
File.write('result.json', "#{report.to_json}\n")
144106
end
145107

146-
class TestMe < Minitest::Test
147-
def setup
148-
File.write('result.json', '')
149-
File.write('data.txt',
150-
'user,0,Leida,Cira,0
151-
session,0,0,Safari 29,87,2016-10-23
152-
session,0,1,Firefox 12,118,2017-02-27
153-
session,0,2,Internet Explorer 28,31,2017-03-28
154-
session,0,3,Internet Explorer 28,109,2016-09-15
155-
session,0,4,Safari 39,104,2017-09-27
156-
session,0,5,Internet Explorer 35,6,2016-09-01
157-
user,1,Palmer,Katrina,65
158-
session,1,0,Safari 17,12,2016-10-21
159-
session,1,1,Firefox 32,3,2016-12-20
160-
session,1,2,Chrome 6,59,2016-11-11
161-
session,1,3,Internet Explorer 10,28,2017-04-29
162-
session,1,4,Chrome 13,116,2016-12-28
163-
user,2,Gregory,Santos,86
164-
session,2,0,Chrome 35,6,2018-09-21
165-
session,2,1,Safari 49,85,2017-05-22
166-
session,2,2,Firefox 47,17,2018-02-02
167-
session,2,3,Chrome 20,84,2016-11-25
168-
')
169-
end
108+
def parse_data(filename)
109+
users = []
110+
sessions = []
170111

171-
def test_result
172-
work
173-
expected_result = '{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}' + "\n"
174-
assert_equal expected_result, File.read('result.json')
112+
File.read(filename).split("\n").each do |line|
113+
cols = line.split(',')
114+
case cols[0]
115+
when 'user'
116+
users << parse_user(cols)
117+
when 'session'
118+
sessions << parse_session(cols)
119+
end
175120
end
121+
122+
[users, sessions]
176123
end

0 commit comments

Comments
 (0)