Skip to content

Latest commit

 

History

History
96 lines (74 loc) · 7.43 KB

File metadata and controls

96 lines (74 loc) · 7.43 KB

Case-study оптимизации

Актуальная проблема

В нашем проекте возникла серьёзная проблема.

Необходимо было обработать файл с данными, чуть больше ста мегабайт.

У нас уже была программа на ruby, которая умела делать нужную обработку.

Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время.

Я решил исправить эту проблему, оптимизировав эту программу.

Формирование метрики

Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время выполнения в секундах

Гарантия корректности работы оптимизированной программы

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

Feedback-Loop

Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный feedback-loop, который позволил мне получать обратную связь по эффективности сделанных изменений за 29 секунд

Вот как я построил feedback_loop:

  1. Использовал профилировщик RubyProf Flat с выключенным GC (по началу на объеме в 50к строк)
  2. Находил главную точку роста и начинал править ровно с этого места
  3. Перезапускал профилировщик повторно и смотрел на полученный результат + непосредственно сам скрипт с включенным GC
  4. Увеличивал объем данных и повторно начинал с пункта 1)

Вникаем в детали системы, чтобы найти главные точки роста

Для того, чтобы найти "точки роста" для оптимизации я воспользовался RubyProf Flat

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

Ваша находка №1

user_sessions = sessions.select { |session| session['user_id'] == user['id'] }
  • вместо полного перебора через select использовал
sessions_grouped = sessions.group_by {|session| session['user_id']}
user_sessions = sessions_grouped[user['id']]
  • На объеме данных в 50к строк скрипт работал +- 2 секунды
  • Array#select упал с 80% до 0%

Ваша находка №2

file_lines.each do |line|
    cols = line.split(',')
    users = users + [parse_user(line)] if cols[0] == 'user'
    sessions = sessions + [parse_session(line)] if cols[0] == 'session'
  end
  • На полном объёме данных выжирает всю оперативу, процесс убивается системой (видно в syslog). Переписал всю логику на использование массива объектов классов User и Session вместо хранения в виде массива строк
  • Программа перестала убиваться системой

Ваша находка №3

  • RubyProf Flat показывает большой % использования Array#each
  • Видно, что логика для построения статистики по каждому пользователю написана криво с использованием огромного кол-ва переборов. Нет смысла каждый раз итерироваться по всему массиву при построении отдельной статистики (т.е нет смысла начинать итерацию с нуля при построении sessionsCount, totalTime, longestSession, browsers, usedIE, alwaysUsedChrome и dates - можно строить все нужные статистики на текущей итерации по каждому пользователю, т.е можно перебрать массив users лишь один раз, а не 7)
  • После рефакторинга Array#each упал до 13.36%

Ваша находка №4

  • RubyProf Flat показывает большой % использования Array#map
  • Некоторые метрики по пользователям (totalTime + longestSession, browsers + usedIE + alwaysUsedChrome) используют одинаковые куски кода с map. Можно использовать мемоизацию. Также заметно бросается в глаза двойной вызов map - в некоторых местах достаточно его вызывать лишь один раз
  • После рефакторинга Array#map упал до 10.18% соответственно

Ваша находка №5

  • RubyProf Flat показывает большой % использования Date#parse
  • Обратив внимание на структуру данных, видно, что конструкцию Date#parse можно опустить вообще - использование `.sort.reverse' более чем достаточно
  • После рефакторинга Date#parse упал до 0% соответственно

Результаты

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

Защита от регрессии производительности

Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написал скрипт с использованием benchmark/ips для замера производительности. Итоговый результат:

ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]
Warming up --------------------------------------
slow string concatenation
                         1.000 i/100ms
Calculating -------------------------------------
slow string concatenation
                          0.034 (± 0.0%) i/s -      1.000 in  29.189983s
                   with 95.0% confidence
Run options: --seed 39795

# Running:

.

Finished in 0.008757s, 114.2005 runs/s, 114.2005 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips