You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+8-8Lines changed: 8 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -94,17 +94,17 @@ head -n N data_large.txt > dataN.txt # create smaller file from larger (take N f
94
94
## Checklist
95
95
Советую использовать все рассмотренные в лекции инструменты хотя бы по разу - попрактикуйтесь с ними, научитесь с ними работать.
96
96
97
-
-[] Прикинуть зависимость времени работы програмы от размера обрабатываемого файла
98
-
-[] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`;
99
-
-[] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`;
97
+
-[x] Прикинуть зависимость времени работы програмы от размера обрабатываемого файла
98
+
-[x] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`;
99
+
-[x] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`;
100
100
-[ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`;
101
101
-[ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`;
102
-
-[] Построить дамп `stackprof` и проанализировать его с помощью `CLI`
103
-
-[] Построить дамп `stackprof` в `json` и проанализировать его с помощью `speedscope.app`
104
-
-[] Профилировать работающий процесс `rbspy`;
102
+
-[x] Построить дамп `stackprof` и проанализировать его с помощью `CLI`
103
+
-[x] Построить дамп `stackprof` в `json` и проанализировать его с помощью `speedscope.app`
104
+
-[x] Профилировать работающий процесс `rbspy`;
105
105
-[ ] Добавить в программу `ProgressBar`;
106
-
-[] Постараться довести асимптотику до линейной и проверить это тестом;
107
-
-[] Написать простой тест на время работы: когда вы придёте к оптимизированному решению, замерьте, сколько оно будет работать на тестовом объёме данных; и напишите тест на то, что это время не превышается (чтобы не было ложных срабатываний, задайте время с небольшим запасом);
106
+
-[x] Постараться довести асимптотику до линейной и проверить это тестом;
107
+
-[x] Написать простой тест на время работы: когда вы придёте к оптимизированному решению, замерьте, сколько оно будет работать на тестовом объёме данных; и напишите тест на то, что это время не превышается (чтобы не было ложных срабатываний, задайте время с небольшим запасом);
108
108
109
109
### Главное
110
110
Нужно потренироваться методично работать по схеме с фидбек-лупом:
Необходимо было обработать файл с данными, чуть больше ста мегабайт.
7
+
8
+
У нас уже была программа на `ruby`, которая умела делать нужную обработку.
9
+
10
+
Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время.
11
+
12
+
Я решил исправить эту проблему, оптимизировав эту программу.
13
+
14
+
## Формирование метрики
15
+
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время выполнения программы.
16
+
17
+
Сначала сделал гипотезу о том, что асимптотика времени работы программы квадратичная: отношение количества записей к времени выполнения в секундах: 100000/115 750000/61 50000/26, 25000/6). Подтвердил эту гипотезу с помощью теста rspec-benchmark.
18
+
В таком случае для полного объема понадобится 4.7 дней.
19
+
20
+
## Гарантия корректности работы оптимизированной программы
21
+
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации.
22
+
23
+
## Feedback-Loop
24
+
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось*
25
+
26
+
Вот как я построил `feedback_loop`: профилирование - изменение кода - тестирование – бенчмаркинг – откат при отсутствии разницы от оптимизации/сохранение результатов
27
+
28
+
## Вникаем в детали системы, чтобы найти главные точки роста
29
+
Для того, чтобы найти "точки роста" для оптимизации я воспользовался rbspy
30
+
31
+
Вот какие проблемы удалось найти и решить
32
+
33
+
### Находка №1
34
+
- rbspy показал `83.55 83.55 block (2 levels) in work - task-1.rb:101`: вызов `sessions.filter {}` на каждой итерации по `users.each`;
35
+
- перед `users.each` сгруппировал `sessions_by_user = sessions.group_by { |session| session['user_id'] }`, в `each` использовал как `sessions_by_user[user['id']] || []`
36
+
- время выполнения программы для 100к входных данных сократилось с 115с до 4с
37
+
- исправленная проблема перестала быть главной точкой роста, rbspy показал, что теперь это `98.49 100.00 block in work - task-1.rb:56`
38
+
39
+
### Находка №2
40
+
- stackprof cli показал `7126 (99.4%) 11 (0.2%) Array#each`, он вызывается несколько раз, наибольшее `6504 ( 91.3%) Object#work]`. Поскольку rbspy указывал на `task-1.rb:56`, что является `end``each` блока, пробую вынести этот`each` в отдельный метод `parse_file`и подтвердить гипотезу, которая и подтверждается: `5765 (99.8%) 5525 (95.7%) Object#parse_file`. Теперь нужно разобраться, какая именно операция в этом блоке `each` требует оптимизации, `stackprof stackprof.dump --method Object#parse_file` показывает, что это заполнение массива сессий: `5261 (93.2%) / 5133 (90.9%) | 52 | sessions = sessions + [parse_session(line)] if cols[0] == 'session'`.
41
+
- вместо `sessions = sessions + [parse_session(line)] if cols[0] == 'session'` использую `sessions << parse_session(line) if cols[0] == 'session'`. аналогично для `users`
42
+
- время выполнения программы для 500к входных данных сократилось с 100с до 13с
43
+
- исправленная проблема перестала быть главной точкой роста, stackprof cli показал, что теперь это `558 (100.0%) 202 (36.2%) Object#work`
44
+
45
+
### Находка №3
46
+
-`ruby-prof` в режиме `Graph` показывает, что точкой роста является `25.55% 25.55% 8.23 8.23 0.00 0.00 154066 Array#+` в `8.23 8.23 0.00 0.00 154066/154066 Array#each`. под это описания подходит 108 строка.
- время выполнения программы для 500к входных данных сократилось с 12с до с 6c
49
+
- исправленная проблема перестала быть главной точкой роста, ruby prof показал, что теперь это `66.16% 26.52% 13.47 5.40 0.00 8.07 500000 Array#all?`
50
+
51
+
### Находка №3
52
+
-`ruby-prof` в режиме `Graph` показывает, что точкой роста является `25.55% 25.55% 8.23 8.23 0.00 0.00 154066 Array#+` в `8.23 8.23 0.00 0.00 154066/154066 Array#each`. под это описания подходит 108 строка.
- время выполнения программы для 500к входных данных сократилось с 12с до с 6c
55
+
- исправленная проблема перестала быть главной точкой роста, ruby prof показал, что теперь это `66.16% 26.52% 13.47 5.40 0.00 8.07 500000 Array#all?`
56
+
57
+
### Находка №4
58
+
-`ruby-prof` в режиме `Graph` показывает, что точкой роста является `8.03 5.25 0.00 2.78 42580848/42580848 BasicObject#!= 85` в `66.16% 26.52% 13.47 5.40 0.00 8.07 500000 Array#all?`.
59
+
- вместо `if uniqueBrowsers.all? { |b| b != browser }` используем `unless uniqueBrowsers.include?(browser)`
60
+
- время выполнения программы для 500к входных данных сократилось с 6с до с 5c
61
+
- исправленная проблема перестала быть главной точкой роста, ruby prof показал, что теперь это `66.16% 26.52% 13.47 5.40 0.00 8.07 500000 Array#all?`
62
+
63
+
### Находка №5
64
+
-`ruby-prof` в режиме `Graph` показывает, что точкой роста является `2.65 0.81 0.00 1.84 846263/846265 Array#map 120` в `94.64% 22.99% 7.22 1.75 0.00 5.47 11 Array#each`. Больше всего вызовов из `Object#collect_stats_from_users`
65
+
- объединяем все блоки вызова `collect_stats_from_users` в один
66
+
- время выполнения программы для 1кк входных данных сократилось с 12с до с 10c
67
+
- исправленная проблема перестала быть главной точкой роста, ruby prof показал, что теперь это `27.07% 16.32% 3.99 2.41 0.00 1.58 846230 <Class::Date>#parse`
68
+
69
+
### Находка №5
70
+
-`ruby-prof` в режиме `Graph` показывает, что точкой роста является `27.07% 16.32% 3.99 2.41 0.00 1.58 846230 <Class::Date>#parse`, это строка `user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 }`
71
+
- вместо `Date.parse(d)` используем `Date.strptime(d, '%Y-%m-%d')` (заранее известен формат). Даты часто повторяются, используем мемоизацию для уже распаршенных дат.
72
+
- время выполнения программы для 1кк входных данных сократилось с 10с до с 7.7c
73
+
- исправленная проблема перестала быть главной точкой роста.
74
+
75
+
## Результаты
76
+
В результате проделанной оптимизации наконец удалось обработать файл с данными.
77
+
Удалось улучшить метрику системы с 4.7 дней до 13 секунд и уложиться в заданный бюджет.
78
+
79
+
## Защита от регрессии производительности
80
+
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавил два теста: прогон на полных данных до 15 секунд, проверка на линейную асимптотику
0 commit comments