В нашем проекте возникла серьёзная проблема.
Необходимо было обработать файл с данными, чуть больше ста мегабайт.
У нас уже была программа на ruby, которая умела делать нужную обработку.
Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время.
Я решил исправить эту проблему, оптимизировав эту программу.
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: количество обработанных строк в мс
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации.
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный feedback-loop, который позволил мне получать обратную связь по эффективности сделанных изменений за время, которое у вас получилось
Вот как я построил feedback_loop:
профилирование для выявления главной точки роста
изменение кода проблемного участка
тестирование
проверка скорости выполнения
коммит
Для того, чтобы найти "точки роста" для оптимизации я воспользовался ruby-prof
Вот какие проблемы удалось найти и решить
- Отчет в ruby-prof показал главную точку роста в методе work - метод select в коде, считающем статистику по пользователям, выполнялся большее количество времени выполнения программы
- Я сгруппировал сессии по id пользователя, чтобы избавиться от прохождения по всему массиву сессий для каждого пользователя
- Скорость обработки 100 записей уменьшилась с 20 до 15 мс
- Array#select исчез из списка методов, которые выполняются много времени
- Отчет в ruby-prof показал что главная точка роста была методом Array#all? в коде для подсчета количества уникальных браузеров
- Переписал код чтобы получить все браузеры за один цикл и потом использовал метод uniq чтобы вернуть только уникальные браузеры
- Скорость обработки 100 записей уменьшилась с 15 до 10 мс
- Array#all? исчез из списка методов, которые выполняются много времени
- Отчет в ruby-prof показал что главная точка роста - Date.parse в коде для получения даты сессий
- Убрал парсинг даты и форматирование дат, потому что в этом нет необходимости
- Скорость обработки 100 записей уменьшилась с 10 до 6 мс
- Парсинг даты перестал быть главной точкой роста
- Главная точка роста - Array#each в коде для формирования отчета
- Объединил несколько проходов по массиву с пользователями в один
- Метрики изменились очень незначительно
- Array#each опустился на второе место в списке методов в ruby-prof
- Главная точка роста - Array#map
- Убрал лишние map
- Метрики изменились очень незначительно
- Array#map опустился на третье место в списке методов в ruby-prof
- Главная точка роста снова Array#each в методе work
- Оптимизировал код в блоках each - убрал пересоздание массивов
- Скорость обработки 1000 записей уменьшилась с 130 до 50 мс
- Array#each опустился на второе место в списке методов в ruby-prof
- Главная точка роста String#split в методе work
- Убрал лишние вызовы split
- Скорость обработки 1000 записей уменьшилась с 50 до 45 мс
- String#split опустился на третье место в списке методов в ruby-prof, количество его вызовов уменьшилось с 1300 до 659
- Главная точка роста снова Array#each в методе collect_stats_from_users
- Изменил код в блоке и метод
- Метрики изменились очень незначительно
- Array#each опустился на второе место в списке методов в ruby-prof
- Главная точка роста - метод to_json
- заменил
- Скорость обработки 1000 записей уменьшилась с 45 до 35 мс
- to_json исчез из списка методов, которые выполняются много времени
В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы до 35 мс для файла с 1000 записей и уложиться в заданный бюджет.
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написал тесты для измерения производительности