Esploriamo il motore VoIP Mediastreamer2. Parte 12

Il materiale dell'articolo è tratto dal mio canale Zen.

Esploriamo il motore VoIP Mediastreamer2. Parte 12

Nel passato articolo, ho promesso di considerare la questione della valutazione del carico sul ticker e dei modi per combattere un'eccessiva richiesta di calcolo nello streamer multimediale. Ma ho deciso che sarebbe più logico affrontare prima le questioni di debug dei filtri personalizzati, legate al trasferimento dei dati e solo dopo considerare i problemi di ottimizzazione delle prestazioni.

Debug dei filtri personalizzati

Dopo aver esaminato nella precedente articolo il meccanismo di trasferimento dei dati nello streamer multimediale, è logico parlare delle insidie nascoste. Una delle caratteristiche del principio del 'data flow' è che l'allocazione della memoria dalla heap avviene nei filtri che si trovano all'inizio del flusso di dati, mentre il rilascio della memoria e il ritorno alla heap sono eseguiti dai filtri posti alla fine del percorso del flusso. Inoltre, la creazione di nuovi dati e la loro distruzione possono avvenire in alcuni punti intermedi. In generale, il rilascio della memoria viene eseguito non dallo stesso filtro che ha creato il blocco di dati.

Da un punto di vista del monitoraggio trasparente della memoria, sarebbe ragionevole che il filtro, ricevuto un blocco in ingresso, dopo averlo elaborato lo distruggesse immediatamente liberando memoria, e sull'uscita esibisse un nuovo blocco creato con i dati in output. In questo modo, una perdita di memoria nel filtro sarebbe facilmente tracciabile: se l'analizzatore rileva una perdita nel filtro, significa che il filtro successivo non distrugge correttamente i blocchi in ingresso e quindi l'errore è in lui. Ma dal punto di vista del mantenimento di elevate prestazioni, questo approccio alla gestione dei blocchi di dati non è produttivo; porta a un gran numero di operazioni di allocazione/rilascio della memoria per i blocchi di dati senza alcun utile output.

Per questo motivo, i filtri dello streamer multimediale, per non rallentare l'elaborazione dei dati, durante la copia dei messaggi utilizzano funzioni che creano copie leggere (ne abbiamo parlato nell'articolo precedente). Queste funzioni creano solo una nuova istanza dell'intestazione del messaggio 'attaccando' ad essa il blocco di dati dal 'vecchio' messaggio copiato. Di conseguenza, a un blocco di dati vengono collegate due intestazioni e viene effettuato un incremento del contatore dei riferimenti nel blocco di dati. Tuttavia, questo apparirà come due messaggi. Ci possono essere più messaggi con un tale blocco di dati 'condiviso'; ad esempio, il filtro MS_TEE genera subito una decina di tali copie leggere, distribuendole sulle proprie uscite. Con un corretto funzionamento di tutti i filtri nella catena, alla fine della pipeline questo contatore dei riferimenti dovrebbe raggiungere zero e verrà chiamata la funzione di rilascio della memoria: ms_free(). Se non viene chiamata, significa che quel pezzo di memoria non tornerà più nella heap, cioè 'perderà'. Il prezzo dell'uso di copie leggere è la perdita della possibilità di stabilire facilmente (come sarebbe stato nel caso di utilizzo di copie normali) in quale filtro del grafo si verifica la perdita di memoria.

Poiché la responsabilità per la ricerca delle perdite di memoria nei filtri 'nativi' ricade sugli sviluppatori dello streamer multimediale, è probabile che non sarà necessario debugarli. Ma con il vostro filtro personalizzato — siete voi gli artefici della vostra fortuna e la vostra precisione determinerà il tempo che trascorrerete nella ricerca delle perdite nel vostro codice. Per ridurre il vostro tempo di sofferenza con il debug, dobbiamo considerare le tecniche di localizzazione delle perdite durante lo sviluppo dei filtri. Inoltre, potrebbe accadere che la perdita si manifesti solo quando il filtro viene applicato in un sistema reale, dove il numero di 'sospetti' potrebbe risultare enorme e il tempo per il debug limitato.

Come si manifesta una perdita di memoria?

È logico supporre che nell'output del programma top venga mostrato un percentuale crescente di memoria occupata dalla vostra applicazione.

L'aspetto esterno sarà che a un certo punto il sistema inizierà a rispondere lentamente ai movimenti del mouse, a rieffettuare l'aggiornamento dello schermo con lentezza. Potrebbe anche crescere il registro di sistema, occupando spazio sul disco rigido. Nel frattempo, la vostra applicazione inizierà a comportarsi in modo strano, non risponderà ai comandi, non sarà in grado di aprire file, ecc.

Per rilevare il fatto dell'insorgere di una perdita, utilizzeremo un analizzatore di memoria (d'ora in poi denominato analizzatore). Questo potrebbe essere Valgrind (buono articolo di esso) o integrato nel compilatore gcc MemorySanitizer o qualcos'altro. Se l'analizzatore mostra che la perdita si verifica in uno dei filtri del grafo, significa che è ora di applicare uno dei metodi descritti di seguito.

Metodo delle tre pigne

Come già accennato in precedenza, in caso di perdita di memoria, l'analizzatore indicherà il filtro che ha richiesto l'allocazione di memoria dal heap. Tuttavia, non indicherà il filtro che "ha dimenticato" di restituirla, che è a sua volta il colpevole. Così facendo, l'analizzatore può solo confermare le nostre preoccupazioni, ma non indicare la loro origine.

Per determinare la posizione del filtro "cattivo" nel grafo, si può ridurre il grafo al numero minimo di nodi, nel quale l'analizzatore rileva ancora la perdita e, nelle restanti tre diramazioni, localizzare il filtro problematico.

Tuttavia, potrebbe accadere che riducendo il numero di filtri nel grafo si interrompa il normale flusso di interazione dei filtri con altri elementi del sistema e la perdita smetta di manifestarsi. In tal caso, sarà necessario lavorare con il grafo completo e adottare l'approccio descritto di seguito.

Metodo dell'isolatore scorrevole

Per semplicità di esposizione, useremo un grafo composto da una sola catena di filtri. Viene rappresentato nell'immagine.

Esploriamo il motore VoIP Mediastreamer2. Parte 12

Un grafo normale, in cui, insieme ai filtri già pronti del mediastreamer, sono stati applicati quattro filtri artigianali F1…F4, di quattro diversi tipi, che hai creato tempo fa e di cui non metti in discussione la correttezza. Tuttavia, supponiamo che in alcuni di essi ci sia una perdita di memoria. Eseguendo il nostro programma di monitoraggio, dall'analisi scopriremo che un certo filtro ha richiesto una certa quantità di memoria e non l'ha restituita al heap N volte. È facile intuire che ci sarà un riferimento alle funzioni interne del filtro di tipo MS_VOID_SOURCE. Il suo compito è prelevare memoria dal heap. Altri filtri dovrebbero restituirla. Cioè, scopriremo un fatto di perdita.

Per determinare dove nel pipeline si è verificata l'inattività che ha portato alla perdita di memoria, si propone di inserire un filtro aggiuntivo che semplicemente trasferisce i messaggi dall'ingresso all'uscita, ma allo stesso tempo crea una copia "non leggera", in una "pesante" copia normale del messaggio in ingresso, eliminando poi completamente il messaggio che è entrato. Chiamiamo questo filtro isolatore. Presumiamo che, essendo il filtro semplice, la perdita in esso sia esclusa. E un altro aspetto positivo: se lo aggiungiamo in qualsiasi punto del nostro grafo, non influenzerà il funzionamento dello schema. Rappresenteremo il filtro-isolatore come un cerchio con un doppio contorno.

Attiviamo l'isolatore immediatamente dopo il filtro voidsource:
Esploriamo il motore VoIP Mediastreamer2. Parte 12

Riavviamo il programma con l'analizzatore e vediamo che questa volta l'analizzatore attribuisce la colpa all'isolatore. Infatti, è lui a creare i blocchi di dati che poi vengono smarriti da un filtro (o filtri) ignoti e negligenti. Il passo successivo è spostare l'isolatore lungo la catena a destra, su un filtro, e riavviare nuovamente l'analisi. Così, spostando l'isolatore a destra passo dopo passo, otterremo una situazione in cui nel prossimo rapporto dell'analizzatore il numero di blocchi di memoria "perduti" diminuirà. Questo significa che a questo passo l'isolatore si è trovato nella catena immediatamente dopo il filtro problematico. Se il filtro "cattivo" era uno solo, la perdita scomparirà del tutto. Così, abbiamo localizzato il filtro problematico (o uno dei diversi). "Riparando" il filtro, possiamo continuare a spostare l'isolatore a destra lungo la catena fino a ottenere una vittoria completa sulle perdite di memoria.

Implementazione del filtro-isolatore

L'implementazione dell'isolatore appare come un filtro normale. File di intestazione:

/* Файл 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

Il filtro stesso:

/* Файл 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)

Metodo di sostituzione delle funzioni di gestione della memoria

Per ricerche più approfondite, nel mediastreamer è prevista la possibilità di sostituire le funzioni di accesso alla memoria con le proprie, che, oltre al lavoro principale, registreranno "Chi, dove e perché". Vengono sostituite tre funzioni. Questo viene fatto nel seguente modo:

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);

Questa possibilità è utile nei casi in cui l'analizzatore rallenti il funzionamento dei filtri tanto da compromettere il funzionamento del sistema in cui è integrato il nostro schema. In tale situazione è necessario rinunciare all'analizzatore e utilizzare la sostituzione delle funzioni di gestione della memoria.

Abbiamo esaminato l'algoritmo di azione per un grafo semplice, che non contiene ramificazioni. Ma questo approccio può essere applicato anche ad altri casi, naturalmente con complicazioni, ma l'idea rimarrà la stessa.

Nell'articolo seguente, esamineremo la questione della valutazione del carico sul ticker e i modi per affrontare un'eccessiva carico computazionale nel media streamer.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster