В нашем проекте возникла серьёзная проблема.
Необходимо было обработать файл с данными, чуть больше ста мегабайт.
У нас уже была программа на ruby
, которая умела делать нужную обработку.
Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время.
Я решил исправить эту проблему, оптимизировав эту программу.
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время обработки файла.
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации.
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный feedback-loop
, который позволил мне получать обратную связь по эффективности сделанных изменений за 28-28 секунд.
Вот как я построил feedback_loop
: Итерации: замерял метрики, составлял отчет, находил потенциальную точку роста, менял код, снова смотрел метрики и отчет.
Для того, чтобы найти "точки роста" для оптимизации я воспользовался гемами Ruby Prof, Stackprof
Вот какие проблемы удалось найти и решить
- в блоке создания объектов User - sessions.select - очень долгий
- группировка сессий по user_id и итерация по users - выбор аттрибутов для объекта User остался тем же, а сессии берутся из массива сгруппированных сессий (по user_id)
- обработка ускорилась значительно - раньше 40 тыс. строк обрабатывалось за 37 сек., стало - за 0,65
- отчёт профилировщика изменился значительно - исправленная проблема не перестала быть главной точкой роста, но с 90% времени в ней показывается около 50%
- в блоке file_lines.each {} - split и обращение по индексу показался дорогими; проверка условий (здесь скорее рефакторинг)
- заменить split и :[] на start_with? и добавить ветления if elsif
- ускорилась обработка на ~ 2-3%
- отчёт профилировщика почти не изменился - исправленная проблема не перестала быть главной точкой роста
- в методах parse_user, parse_session лишний split и объявление переменной parsed_result
- убрать данные конструкции и передавать в parse_session уже массив (заранее 'splitted'), а не строку
- ускорилась обработка на 5%
- отчёт профилировщика почти не изменился - исправленная проблема не перестала быть главной точкой роста
- метод вычисление параметра 'usedIE' вызовом метода 'collect_stats_from_users' - upcase
- убрать upcase и использовать case-insensitive RegEx
- ускорилась обработка на пару процентов
- отчёт профилировщика почти не изменился - исправленная проблема не перестала быть главной точкой роста
- метод вычисление параметра 'alwaysUsedChrome' вызовом метода 'collect_stats_from_users' - дорогой метод 'all?'
- заменить метод all? на any? и ревертнуть метод (добавить отрицание)
- ускорилась обработка на пару процентов
- отчёт профилировщика почти не изменился - исправленная проблема не перестала быть главной точкой роста
- вычисление uniqueBrowsers - используется all? - дорого
- заменить метод all? на sessions.map(<браузеры>).uniq
- ускорилась обработка на пару процентов
- отчёт профилировщика почти не изменился - исправленная проблема не перестала быть главной точкой роста
- парсинг дат для статистики - никак не преобразует их - не нужен
- убрать парсинг
- ускорилась обработка на ~ 45%
- отчёт профилировщика изменился - точка роста переместилась на методы parse_user, parse_session. Файл data_large.txt обработан за 43 секунды.
- слишком много вызовов метода collect_stats_from_users и многоразовый map и upcase
- соединить все вызовы collect_stats_from_users в один и присвоить результаты map и upcase переменным. Затем использовать переменнные
- ускорилась обработка на пару процентов
- отчёт профилировщика почти не изменился
- вычисление report['allBrowsers'] происходит путем сортировки, потом уникальности
- надо сначала отобрать уникальные браузеры, потом сортировать, их будет меньше -> сортировка быстрее
- ускорилась обработка на 20%
- получилось, что не это главная точка роста, просто нашел и ускорил этот фрагмент. Точка роста не изменилась.
- строковые конкатенации и интерполяции
- надо заменить их оператором '<<'
- ускорилась обработка на пару процентов
- получилось, что не это главная точка роста, просто нашел и ускорил этот фрагмент. Точка роста не изменилась. Здесь я, к сожалению, внес оптимизацию по наитию, но она оказалось весьма усместна.
- использование json
- решил поменять на Oj (упомяналась в лекциях, как оптимизированная библиотека)
- ускорилась обработка на пару процентов
- это была не главная точка роста, просто нашел и ускорил этот фрагмент. Точка роста не изменилась. Оптимизация по наитию опять)
В результате проделанной оптимизации наконец удалось обработать файл с данными. Удалось улучшить метрику системы с неопределенного времени обработки до 28-29 секунд и уложиться в заданный бюджет.
Другие наблюдения:
- в начале выполнения задания асимптотика была квадратичной (степень ~ 2, на вход подавал в два раза больше, время увеличивалось в 4 раза);
- при отключении GC время обработки увеличивалось примерно на четверть (супер странно);
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы я написал тест, замеряющий время обработки небольшого файла. Правда я так и не понял, как нормально написать тесты на линейность. Сделал несколько попыток. Тест то проходит, то нет. Извините, что сложил все в один коммит. Вспомнил про это в конце работы.