Изучаем VoIP-движок Mediastreamer2. Часть 12

Материал статьи взят с моего дзен-канала.

Изучаем VoIP-движок Mediastreamer2. Часть 12

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

Отладка крафтовых фильтров

После того, как мы в предыдущей статье рассмотрели механизм перемещения данных в медиастримере будет логично поговорить о скрывающихся в нем опасности. Одна из особенностей принципа "data flow" состоит в том, что выделение памяти из кучи происходит в фильтрах, которые находятся у истоков потока данных, а освобождение памяти с возвращением в кучу делают уже фильтры, расположенные в конце пути потока. Кроме этого, создание новых данных и их уничтожение может происходить где-то в промежуточных точках. В общем случае, освобождение памяти выполняет не тот фильтр, что создал блок данных.

С точки зрения прозрачного мониторинга за памятью было бы разумно, чтобы фильтр, получая входной блок, после обработки тут же уничтожал его с освобождением памяти, а на выход выставлял бы вновь созданный блок с выходными данными. В этом случае утечка памяти в фильтре легко бы трассировалась — если анализатор обнаружил утечку в фильтре, то значит следующий за ним фильтр не уничтожает входящие блоки надлежащим образом и ошибка в нем. Но с точки зрения поддержания высокой производительности, такой подход к работе с блоками данных, не продуктивен — он приводит к большому количество операций по выделению/освобождению памяти под блоки данных без какого либо полезного выхлопа.

По этой причине фильтры медиастримера, чтобы не замедлять обработку данных, при копировании сообщений используют функции создающие легкие копии (мы рассказывали о них в прошлой статье). Эти функции только создают новый экземпляр заголовка сообщения "пристегивая" к нему блок данных от копируемого "старого" сообщения. В результате, к одному блоку данных оказываются привязанными два заголовка и выполняется инкремент счетчика ссылок в блоке данных. Но выглядеть это будет как два сообщения. Сообщений с таким "обобществленным" блоком данных может быть и больше, так например фильтр MS_TEE порождает сразу десяток таких легких копий, распределяя их по своим выходам. При правильной работе всех фильтров в цепочке, к концу конвейера этот счетчик ссылок должен достигнуть нуля и будет вызвана функция освобождения памяти: ms_free(). Если вызова не происходит, то значит этот кусок памяти уже не вернется в кучу, т.е. он "утечет". Расплатой за использование легких копий служит утрата возможности легко установить (как это было бы в случае использования обычных копий) в каком фильтре графа утекает память.

Поскольку ответственность за поиск утечек памяти в "родных" фильтрах лежит на разработчиках медиастримера, то скорее всего вам не придется их отлаживать. Но вот с вашим крафтовым фильтром — вы сами кузнечик своего счастья и от вашей аккуратности будет зависеть время которое вы проведете в поисках утечек в вашем коде. Чтобы сократить ваше время мытарств с отладкой, мы должны рассмотреть приёмы локализации утечек при разработке фильтров. К тому же, может случиться так, утечка проявит себя только при применении фильтра в реальной системе, где количество "подозреваемых" может оказаться огромным, а время на отладку ограниченным.

Как проявляет себя утечка памяти?

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

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

Чтобы выявить факт возникновения утечки будем использовать анализатор памяти (далее анализатор). Это может быть Valgrind (хорошая статья о нем) или встроенный в компилятор gcc MemorySanitizer или что-нибудь иное. Если анализатор покажет, что утечка происходит в одном из фильтров графа, то это означает что пора применить один из способов описанных ниже.

Метод трех сосен

Как уже было сказано выше, при утечке памяти анализатор укажет на фильтр, который запросил выделение памяти из кучи. Но не укажет на фильтр который "забыл" её вернуть, который, собственно, и является виноватым. Тем самым, анализатор может только подтвердить наши опасения, но не указать на их корень.

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

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

Метод скользящего изолятора

Для простоты изложения воспользуемся графом который состоит из одной цепочки фильтров. Она изображена на рисунке.

Изучаем VoIP-движок Mediastreamer2. Часть 12

Обычный граф, в котором наравне с готовыми фильтрами медиастримера применены четыре крафтовых фильтра F1…F4, четырех разных типов, которые вы сделали давно и в их корректности не сомневаетесь. Тем не менее предположим, что в несколько из них имеется утечка памяти. Запуская нашу программу по надзором анализатора, из его отчета мы узнаем, что некий фильтр запросил некоторое количество памяти и не вернул его в кучу N-ое количество раз. Легко можно догадаться, будет ссылка на внутренние функции фильтра типа MS_VOID_SOURCE. У него задача такая — забирать память из кучи. Возвращать её туда должны другие фильтры. Т.е. мы обнаружим факт утечки.

Чтобы определить на каком участке конвейера произошло бездействие приведшее к утечке памяти, предлагается ввести дополнительный фильтр, который просто перекладывает сообщения со входа на выход, но при этом создает не легкую, в нормальную "тяжелую" копию входного сообщения, затем полностью удаляя сообщение, поступивший на вход. Будем называть такой фильтр изолятором. Полагаем, что поскольку фильтр простой, то утечка в нем исключена. И еще одно положительное свойство — если мы добавим его в любое место нашего графа, то это никак не скажется на работе схемы. Будем изображать фильтр-изолятор в виде круга с двойным контуром.

Включаем изолятор сразу после фильтра voidsourse:
Изучаем VoIP-движок Mediastreamer2. Часть 12

Снова запускаем программу с анализатором, и видим, что в это раз, анализатор возложит вину на изолятор. Ведь это он теперь создает блоки данных, которые потом теряются неизвестным нерадивым фильтром (или фильтрами). Следующим шагом сдвигаем изолятор по цепочке вправо, на один фильтр и снова запускаем анализ. Так, шаг за шагом двигая изолятор вправо, мы получим ситуацию, когда в в очередном отчете анализатора количество "утекших" блоков памяти уменьшится. Это значит, что на этом шаге изолятор оказался в цепочке сразу после проблемного фильтра. Если "плохой" фильтр был один, то утечка и вовсе пропадет. Таким образом мы локализовали проблемный фильтр (или один из нескольких). "Починив" фильтр, мы можем продолжить двигать изолятор вправо по цепочке до полной победы над утечками памяти.

Реализация фильтра-изолятора

Реализация изолятора выглядит также как обычный фильтр. Заголовочный файл:

/* Файл iso_filter.h  Описание изолирующего фильтра. */

#ifndef iso_filter_h
#define iso_filter_h

/* Задаем идентификатор фильтра. */
#include <mediastreamer2/msfilter.h>

#define MY_ISO_FILTER_ID 1024

extern MSFilterDesc iso_filter_desc;

#endif

Сам фильтр:

/* Файл iso_filter.c  Описание изолирующего фильтра. */

#include "iso_filter.h"

    static void
iso_init (MSFilter * f)
{
}
    static void
iso_uninit (MSFilter * f)
{
}

    static void
iso_process (MSFilter * f)
{
    mblk_t *im;

    while ((im = ms_queue_get (f->inputs[0])) != NULL)
    {
        ms_queue_put (f->outputs[0], copymsg (im));
        freemsg (im);
    }
}

static MSFilterMethod iso_methods[] = {
    {0, NULL}
};

MSFilterDesc iso_filter_desc = {
    MY_ISO_FILTER_ID,
    "iso_filter",
    "A filter that reads from input and copy to its output.",
    MS_FILTER_OTHER,
    NULL,
    1,
    1,
    iso_init,
    NULL,
    iso_process,
    NULL,
    iso_uninit,
    iso_methods
};

MS_FILTER_DESC_EXPORT (iso_desc)

Метод подмены функций управления памятью

Для более тонких исследований, в медиастримере предусмотрена возможность подмены функций доступа к памяти на ваши собственные, которые помимо основной работы будут фиксировать "Кто, куда и зачем". Подменяются три функции. Это делается следующим образом:

OrtpMemoryFunctions reserv;
OrtpMemoryFunctions my;

reserv.malloc_fun = ortp_malloc;
reserv.realloc_fun = ortp_realloc;
reserv.free_fun = ortp_free;

my.malloc_fun = &my_malloc;
my.realloc_fun = &my_realloc;
my.free_fun = &my_free;

ortp_set_memory_functions(&my);

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

Мы рассмотрели алгоритм действий для простого графа, не содержащего разветвлений. Но этот подход можно применить и для других случаев, конечно с усложнением, но идея останется той же.

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

Источник: habr.com

Добавить комментарий