Вивчаємо VoIP-движок Mediastreamer2. Частина 12

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

Вивчаємо VoIP-движок Mediastreamer2. Частина 12

У минулій статтія обіцяв розглянути питання оцінки навантаження на тикер і способи боротьби з надмірним обчислювальним навантаженням в медіастрімері. Але вирішив, що буде логічніше висвітлити питання налагодження крафтових фільтрів, пов'язані з переміщенням даних і потім розглянути питання оптимізації продуктивності.

Налагодження крафтових фільтрів

Після того, як ми в попередній статті розглянули механізм переміщення даних в медіастрімері, буде логічно поговорити про небезпеку, що ховається в ньому. Одна з особливостей принципу "data flow" полягає в тому, що виділення пам'яті з купи відбувається у фільтрах, які знаходяться біля джерел потоку даних, а звільнення пам'яті з поверненням до купи роблять вже фільтри, розташовані в кінці шляху потоку. Крім цього, створення нових даних та їх знищення може відбуватися десь у проміжних точках. Загалом, звільнення пам'яті виконує той фільтр, що створив блок даних.

З точки зору прозорого моніторингу за пам'яттю було б розумно, щоб фільтр, отримуючи вхідний блок, після обробки відразу знищував його зі звільненням пам'яті, а на вихід виставляв би новостворений блок з вихідними даними. У цьому випадку витік пам'яті у фільтрі легко трасувався — якщо аналізатор виявив витік у фільтрі, то наступний за ним фільтр не знищує вхідні блоки належним чином і помилка в ньому. Але з точки зору підтримки високої продуктивності такий підхід до роботи з блоками даних не продуктивний — він призводить до великої кількості операцій з виділення/звільнення пам'яті під блоки даних без будь-якого корисного вихлопу.

З цієї причини фільтри медіастрімера, щоб не уповільнювати обробку даних, при копіюванні повідомлень використовують функції, що створюють легкі копії (ми розповідали про них у минулій статті). Ці функції тільки створюють новий екземпляр заголовка повідомлення "пристібаючи" до нього блок даних від "старого" повідомлення, що копіюється. В результаті, до одного блоку даних виявляються прив'язаними два заголовки та виконується інкремент лічильника посилань у блоці даних. Але виглядатиме це як два повідомлення. Повідомлень з таким "узагальненим" блоком даних може бути і більше, наприклад фільтр MS_TEE породжує відразу десяток таких легких копій, розподіляючи їх по своїх виходах. При правильній роботі всіх фільтрів у ланцюжку до кінця конвеєра цей лічильник посилань повинен досягти нуля і буде викликана функція звільнення пам'яті: ms_free(). Якщо виклику немає, то це шматок пам'яті не повернеться у купу, тобто. він "втече". Розплатою за використання легких копій служить втрата можливості легко встановити (як це було б у разі використання звичайних копій) у якому фільтрі графа витікає пам'ять.

Оскільки відповідальність за пошук витоків пам'яті в "рідних" фільтрах лежить на розробниках медіастрімера, то швидше за все вам не доведеться їх налагоджувати. Але ось з вашим крафтовим фільтром - ви самі коник свого щастя і від вашої акуратності залежатиме час, який ви проведете в пошуках витоків у вашому коді. Щоб скоротити час поневірянь, ми повинні розглянути прийоми локалізації витоків при розробці фільтрів. До того ж, може статися так, витік проявить себе тільки при застосуванні фільтра в реальній системі, де кількість "підозрюваних" може виявитися величезною, а час на налагодження обмеженим.

Як поводиться витік пам'яті?

Логічно припустити, що у виведенні програми топ буде відображатися наростаючий відсоток пам'яті, який займає ваш додаток.

Зовнішнє прояв полягатиме у цьому, що у якийсь момент система стане уповільнено реагувати на рух мишки, повільно перемальовувати екран. Можливо також зростатиме системний лог, з'їдаючи місце на жорсткому диску. При цьому ваш додаток почне вести себе дивно, не відповідатиме на команди, не може відкрити файл і т.д.

Щоб виявити факт виникнення витоку використовуватимемо аналізатор пам'яті (далі аналізатор). Це може бути Вальгрінд (добра стаття про нього) або вбудований у компілятор ПКУ 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

Додати коментар або відгук