├── README.md ├── case-study-template.md ├── data.txt ├── data_large.txt.gz └── task-1.rb /README.md: -------------------------------------------------------------------------------- 1 | # Задание №1 2 | 3 | В файле `task1.rb` находится ruby-программа, которая выполняет обработку данных из файла. 4 | 5 | При запуске `ruby task1.rb` запускается тест, который показывает как программа должна работать. 6 | 7 | С помощью этой программы нужно обработать файл с данными `data_large.txt`. 8 | 9 | 10 | **Проблема в том, что это происходит слишком долго, дождаться пока никому не удавалось.** 11 | 12 | 13 | ## Задача 14 | 15 | - Оптимизировать эту программу, выстроив процесс согласно "общему фреймворку оптимизации" из первой презентации; 16 | - Профилировать программу с помощью `stackprof` и `ruby-prof`, (можно ещё попробовать `vernier`); 17 | - Добиться того, чтобы программа корректно обработала файл `data_large.txt` за `30 секунд`; 18 | - Написать кейс-стади о вашей оптимизации по шаблону `case-study-template.md`. 19 | 20 | Case-study должен получиться рассказом с техническими подробностями о том как вы пришли к успеху в оптимизации благодаря системному подходу. Можно сказать, заготовкой технической статьи. 21 | 22 | ## Сдача задания 23 | 24 | Надо сделать `PR` в этот репозиторий и прислать его для проверки. 25 | 26 | В `PR`-е 27 | - должны быть внесены оптимизации в `task1.rb`; 28 | - должен быть файл `case-study.md` с описанием проделанной оптимизации; 29 | 30 | 31 | # Комментарии 32 | 33 | ## Риски 34 | 35 | Задание моделирует такую ситуацию: вы получили неффективную систему, в которой код и производительность оставляет желать лучшего. При этом актуальной проблемой является именно плохая производительность. 36 | Вам нужно оптимизировать эту незнакомую систему. 37 | 38 | С какими искушениями вы сталкиваететь: 39 | - вы “с ходу” замечаете какие-то непроизводительные идиомы, и у вас возникает соблазн их сразу исправить; 40 | - попытаться в уме очень вникнуть в работу программы, как будто просветить её микроскопом, превратиться в компилятор 41 | 42 | Эти искушения типичны и часто возникают в моделируемой ситуации, но есть риски: 43 | 44 | - перед рефакторингом “очевидных” косяков не написать тестов и незаметно внести регрессию; 45 | - потратить время на рефакторинг, хотя время было только на оптимизацию; 46 | - исправить на глаз замеченные/предположенные проблемы, не получить заметного результата, решить что наверное просто Ruby слишком медленный для этой задачи, демотивироваться 47 | 48 | ## Советы 49 | 50 | - Найдите объём данных, на которых программа отрабатывает достаточно быстро - это позволит вам выстроить фидбек-луп; если улучшите метрику для части данных, то улучшите и для полного объёма данных; *оптимально давать программе покрутиться секунд 5; слишком мало тоже нехорошо для профилирования и бенчмаркинга* 51 | - Попробуйте прикинуть ассимтотику роста времени работы в зависимости от объёма входных данных (попробуйте объём x, 2x, 3x, ...) 52 | - Оцените, как долго программа будет обрабатывать полный обём данных 53 | - Вкладывайтесь в удобство работы и скорость получения обратной связи, сделайте себе эффективный фидбек-луп 54 | 55 | ### Советы по профилированию и измерению метрики 56 | 57 | - попробуйсте `rbspy` 58 | - попробуйте `Stackprof` с визуализацией в `Speedpscope` и `CLI` 59 | - попробуйте `ruby-prof` 60 | - попробуйте `Vernier` 61 | - задайте простую и понятную метрику для оптимизируемой системы (на каждой итерации) 62 | - отключайте профилировщики при вычислении метрики (они замедляют работу системы) 63 | - не замеряйте время профилировщиком (при замерах он вообще должен быть отключен) 64 | - aka не смешивайте профилирование и бенчмаркинг 65 | 66 | ### Совет: как посчитать кол-во строк в файле 67 | 68 | ``` 69 | wc -l data_large.rb # (3250940) total line count 70 | ``` 71 | 72 | ### Совет: как создать меньший файл из большего, оставив перевые N строк 73 | 74 | ``` 75 | head -n N data_large.txt > dataN.txt # create smaller file from larger (take N first lines) 76 | ``` 77 | 78 | ## Что нужно делать 79 | 80 | - исследовать предложенную вам на рассмотрение систему 81 | - построить фидбек-луп, который позволит вам быстро тестировать гипотезы и измерять их эффект 82 | - применить инструменты профилирования CPU, чтобы найти главные точки роста 83 | - выписывать в case-study несколько пунктов: каким профилировщиком вы нашли точку роста, как её оптимизировали, какой получили прирост метрики, как найденная проблема перестала быть главной точкой роста; 84 | 85 | 86 | ## Что не нужно делать 87 | 88 | - переписывать с нуля 89 | - забивать на выстраивание фидбек-лупа 90 | - вносить оптимизации по наитию, без профилировщика и без оценки эффективности 91 | - смешивать несколько изменений в одну итерацию 92 | 93 | ## Что можно делать 94 | 95 | - рефакторить код 96 | - рефакторить/дописывать тесты 97 | - разбивать скрипт на несколько файлов 98 | 99 | ## Основная польза задания 100 | 101 | Главная польза этого задания - попрактиковаться в применении грамотного и системного подхода к оптимизации, почуствовать этот процесс: 102 | - как взяли незнакомую систему и исследовали её 103 | - как выстроили фидбек луп 104 | - как с помощью профилировщиков сформировали гипотезу о том что именно даст вам наибольший эффект (главную точку роста) 105 | - как быстро протестировали гипотезу, получили измеримый результат и зафиксировали его 106 | - как в итоге написали небольшой журнал об успешных шагах этого процесса 107 | 108 | ## Extended Checklist 109 | 110 | Советую использовать все рассмотренные в лекции инструменты хотя бы по разу - попрактикуйтесь с ними, научитесь с ними работать. 111 | 112 | - [ ] Прикинуть зависимость времени работы програмы от размера обрабатываемого файла 113 | - [ ] Поюзать `stackprof` со `speedscope` и `cli` 114 | - [ ] Профилировать работающий процесс `rbspy`; 115 | - [ ] Попробовать `vernier` 116 | - [ ] Постараться довести асимптотику до линейной и написать на это `assert`; 117 | 118 | ## Second Thread - Reaper 119 | - [ ] По фану можно завести второй тред, который будет убивать процесс, если прошло больше 30 секунд 120 | 121 | ### Главное 122 | 123 | Нужно потренироваться методично работать по схеме с фидбек-лупом: 124 | - построили отчёт каким-то из профилировщиков 125 | - осознали его 126 | - поняли, какая самая большая точка роста 127 | - внесли минимальные изменения, чтобы использовать только эту точку роста 128 | - вычислили метрику - оценили, как изменение повлияло на метрику 129 | - перестроили отчёт, убедились, что проблема решена 130 | - записали полученные результаты 131 | - закоммитились 132 | - перешли к следующей итерации 133 | 134 | ### PS 135 | 136 | **Не комитьте пожалуйста много файлов типа дампов профилировщиков или усечённых data.txt** 137 | 138 | **Самое главное в этом задании это case-study, и на втором месте ruby-код.** 139 | -------------------------------------------------------------------------------- /case-study-template.md: -------------------------------------------------------------------------------- 1 | # Case-study оптимизации 2 | 3 | ## Актуальная проблема 4 | В нашем проекте возникла серьёзная проблема. 5 | 6 | Необходимо было обработать файл с данными, чуть больше ста мегабайт. 7 | 8 | У нас уже была программа на `ruby`, которая умела делать нужную обработку. 9 | 10 | Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. 11 | 12 | Я решил исправить эту проблему, оптимизировав эту программу. 13 | 14 | ## Формирование метрики 15 | Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* 16 | 17 | ## Гарантия корректности работы оптимизированной программы 18 | Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. 19 | 20 | ## Feedback-Loop 21 | Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* 22 | 23 | Вот как я построил `feedback_loop`: *как вы построили feedback_loop* 24 | 25 | ## Вникаем в детали системы, чтобы найти главные точки роста 26 | Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* 27 | 28 | Вот какие проблемы удалось найти и решить 29 | 30 | ### Ваша находка №1 31 | - какой отчёт показал главную точку роста 32 | - как вы решили её оптимизировать 33 | - как изменилась метрика 34 | - как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? 35 | 36 | ### Ваша находка №2 37 | - какой отчёт показал главную точку роста 38 | - как вы решили её оптимизировать 39 | - как изменилась метрика 40 | - как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? 41 | 42 | ### Ваша находка №X 43 | - какой отчёт показал главную точку роста 44 | - как вы решили её оптимизировать 45 | - как изменилась метрика 46 | - как изменился отчёт профилировщика - исправленная проблема перестала быть главной точкой роста? 47 | 48 | ## Результаты 49 | В результате проделанной оптимизации наконец удалось обработать файл с данными. 50 | Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. 51 | 52 | *Какими ещё результами можете поделиться* 53 | 54 | ## Защита от регрессии производительности 55 | Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* 56 | 57 | -------------------------------------------------------------------------------- /data.txt: -------------------------------------------------------------------------------- 1 | user,0,Leida,Cira,0 2 | session,0,0,Safari 29,87,2016-10-23 3 | session,0,1,Firefox 12,118,2017-02-27 4 | session,0,2,Internet Explorer 28,31,2017-03-28 5 | session,0,3,Internet Explorer 28,109,2016-09-15 6 | session,0,4,Safari 39,104,2017-09-27 7 | session,0,5,Internet Explorer 35,6,2016-09-01 8 | user,1,Palmer,Katrina,65 9 | session,1,0,Safari 17,12,2016-10-21 10 | session,1,1,Firefox 32,3,2016-12-20 11 | session,1,2,Chrome 6,59,2016-11-11 12 | session,1,3,Internet Explorer 10,28,2017-04-29 13 | session,1,4,Chrome 13,116,2016-12-28 14 | user,2,Gregory,Santos,86 15 | session,2,0,Chrome 35,6,2018-09-21 16 | session,2,1,Safari 49,85,2017-05-22 17 | session,2,2,Firefox 47,17,2018-02-02 18 | session,2,3,Chrome 20,84,2016-11-25 19 | -------------------------------------------------------------------------------- /data_large.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardcode-dev/rails-optimization-task1/bca675edbd556062a83df917c58766fe136f9743/data_large.txt.gz -------------------------------------------------------------------------------- /task-1.rb: -------------------------------------------------------------------------------- 1 | # Deoptimized version of homework task 2 | 3 | require 'json' 4 | require 'pry' 5 | require 'date' 6 | require 'minitest/autorun' 7 | 8 | class User 9 | attr_reader :attributes, :sessions 10 | 11 | def initialize(attributes:, sessions:) 12 | @attributes = attributes 13 | @sessions = sessions 14 | end 15 | end 16 | 17 | def parse_user(user) 18 | fields = user.split(',') 19 | parsed_result = { 20 | 'id' => fields[1], 21 | 'first_name' => fields[2], 22 | 'last_name' => fields[3], 23 | 'age' => fields[4], 24 | } 25 | end 26 | 27 | def parse_session(session) 28 | fields = session.split(',') 29 | parsed_result = { 30 | 'user_id' => fields[1], 31 | 'session_id' => fields[2], 32 | 'browser' => fields[3], 33 | 'time' => fields[4], 34 | 'date' => fields[5], 35 | } 36 | end 37 | 38 | def collect_stats_from_users(report, users_objects, &block) 39 | 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)) 43 | end 44 | end 45 | 46 | def work 47 | file_lines = File.read('data.txt').split("\n") 48 | 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 57 | 58 | # Отчёт в json 59 | # - Сколько всего юзеров + 60 | # - Сколько всего уникальных браузеров + 61 | # - Сколько всего сессий + 62 | # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + 63 | # 64 | # - По каждому пользователю 65 | # - сколько всего сессий + 66 | # - сколько всего времени + 67 | # - самая длинная сессия + 68 | # - браузеры через запятую + 69 | # - Хоть раз использовал IE? + 70 | # - Всегда использовал только Хром? + 71 | # - даты сессий в порядке убывания через запятую + 72 | 73 | report = {} 74 | 75 | report[:totalUsers] = users.count 76 | 77 | # Подсчёт количества уникальных браузеров 78 | uniqueBrowsers = [] 79 | sessions.each do |session| 80 | browser = session['browser'] 81 | uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } 82 | end 83 | 84 | report['uniqueBrowsersCount'] = uniqueBrowsers.count 85 | 86 | report['totalSessions'] = sessions.count 87 | 88 | report['allBrowsers'] = 89 | sessions 90 | .map { |s| s['browser'] } 91 | .map { |b| b.upcase } 92 | .sort 93 | .uniq 94 | .join(',') 95 | 96 | # Статистика по пользователям 97 | users_objects = [] 98 | 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] 104 | end 105 | 106 | report['usersStats'] = {} 107 | 108 | # Собираем количество сессий по пользователям 109 | 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 } } 141 | end 142 | 143 | File.write('result.json', "#{report.to_json}\n") 144 | end 145 | 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 170 | 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') 175 | end 176 | end 177 | --------------------------------------------------------------------------------