Esplorare il motore VoIP di Mediastreamer2. Parte 12

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

Esplorare il motore VoIP di Mediastreamer2. Parte 12

Nel passato Articolo, ho promesso di considerare la questione della stima del carico del ticker e dei modi per gestire un carico di elaborazione eccessivo nello streamer multimediale. Ma ho deciso che sarebbe stato più logico affrontare i problemi di debug dei filtri artigianali relativi allo spostamento dei dati e solo successivamente considerare i problemi di ottimizzazione delle prestazioni.

Debug dei filtri artigianali

Dopo che nell'articolo precedente abbiamo esaminato il meccanismo di spostamento dei dati in un media streamer, sarebbe logico parlare dei pericoli in esso nascosti. Una delle caratteristiche del principio del "flusso di dati" è che l'allocazione della memoria dall'heap avviene nei filtri situati alle origini del flusso di dati, e i filtri situati alla fine del percorso del flusso rilasciano già la memoria con ritorno al mucchio. Inoltre, la creazione di nuovi dati e la loro distruzione possono avvenire in qualche punto intermedio. In generale, il rilascio della memoria viene effettuato da un filtro diverso da quello che ha creato il blocco dati.

Dal punto di vista del monitoraggio trasparente della memoria, sarebbe ragionevole che il filtro, quando riceve un blocco di input, lo distrugga immediatamente dopo l'elaborazione, liberando memoria e inserisca in output un blocco appena creato con i dati di output. In questo caso, la perdita di memoria nel filtro potrebbe essere facilmente rintracciata: se l'analizzatore rileva una perdita nel filtro, significa che il filtro che lo segue non distrugge correttamente i blocchi in arrivo e contiene un errore. Ma dal punto di vista del mantenimento di prestazioni elevate, questo approccio al lavoro con i blocchi di dati non è produttivo: porta a un gran numero di operazioni per allocare / liberare memoria per i blocchi di dati senza alcun esaurimento utile.

Per questo motivo i filtri media streamer, per non rallentare l'elaborazione dei dati, utilizzano funzioni che creano copie light durante la copia dei messaggi (ne abbiamo parlato in un articolo precedente). Queste funzioni creano solo una nuova copia dell'intestazione del messaggio "allegando" ad essa il blocco dati del "vecchio" messaggio copiato. Di conseguenza, due intestazioni vengono allegate a un blocco dati e il contatore di riferimento nel blocco dati viene incrementato. Ma sembreranno due messaggi. Potrebbero esserci più messaggi con un blocco dati "pubblico", ad esempio il filtro MS_TEE genera dieci copie leggere contemporaneamente, distribuendole tra le sue uscite. Se tutti i filtri nella catena funzionano correttamente, entro la fine della pipeline questo conteggio dei riferimenti dovrebbe raggiungere zero e verrà chiamata la funzione di deallocazione della memoria: ms_libero(). Se la chiamata non avviene, questo pezzo di memoria non verrà più restituito all'heap, ad es. lui "fugge". Il costo dell'utilizzo di copie leggere è la perdita della capacità di determinare facilmente (come nel caso dell'utilizzo di copie normali) in quale filtro grafico si verifica la perdita di memoria.

Poiché la responsabilità di individuare perdite di memoria nei filtri "nativi" spetta agli sviluppatori dello streamer multimediale, molto probabilmente non sarà necessario eseguirne il debug. Ma con il tuo filtro di creazione, tu stesso sei la cavalletta della tua felicità e il tempo che dedichi alla ricerca di falle nel tuo codice dipenderà dalla tua precisione. Per ridurre i tempi di debug, dobbiamo esaminare le tecniche di localizzazione delle perdite durante la progettazione dei filtri. Inoltre, può accadere che la perdita si manifesti solo quando si applica il filtro in un sistema reale, dove il numero di "sospetti" può essere enorme e il tempo per il debug è limitato.

Come si manifesta una perdita di memoria?

È logico supporre che nell'output del programma top mostrerà una percentuale crescente di memoria occupata dalla tua applicazione.

La manifestazione esterna consisterà nel fatto che ad un certo punto il sistema reagirà lentamente al movimento del mouse, ridisegnando lentamente lo schermo. È anche possibile che il registro di sistema cresca, consumando spazio sul disco rigido. In questo caso, la tua applicazione inizierà a comportarsi in modo strano, non risponderà ai comandi, non potrà aprire il file, ecc.

Per identificare il fatto di una perdita, utilizzeremo un analizzatore di memoria (di seguito denominato analizzatore). Potrebbe essere valgrind (Bene articolo a riguardo) o integrato nel compilatore gcc Disinfettante per la memoria o qualcos'altro. Se l'analizzatore mostra che la perdita si verifica in uno dei filtri del grafico, significa che è giunto il momento di applicare uno dei metodi descritti di seguito.

Metodo Tre Pini

Come accennato in precedenza, in caso di perdita di memoria, l'analizzatore punterà al filtro che ha richiesto l'allocazione di memoria dall'heap. Ma non indicherà il filtro che si è "dimenticato" di restituirlo, il che, in effetti, è la colpa. Pertanto, l’analizzatore può solo confermare le nostre paure, ma non indicarne la radice.

Per scoprire la posizione del filtro "cattivo" nel grafico, è possibile ridurre il grafico al numero minimo di nodi in cui l'analizzatore rileva ancora una perdita e individuare il filtro problematico nei restanti tre pini.

Ma può succedere che riducendo il numero di filtri nella colonna si interromperà il normale corso dell'interazione tra i filtri e gli altri elementi del sistema e la perdita non apparirà più. In questo caso, dovrai lavorare con un grafico a grandezza naturale e utilizzare l'approccio descritto di seguito.

Metodo dell'isolante scorrevole

Per semplicità di presentazione, utilizzeremo un grafico costituito da una singola catena di filtri. Lei è mostrata nella foto.

Esplorare il motore VoIP di Mediastreamer2. Parte 12

Un grafico normale, in cui, insieme ai filtri per streamer multimediali già pronti, vengono utilizzati quattro filtri artigianali F1...F4, quattro tipi diversi che hai realizzato molto tempo fa e non hai dubbi sulla loro correttezza. Supponiamo tuttavia che molti di essi abbiano una perdita di memoria. Quando eseguiamo il nostro programma di supervisione dell'analizzatore, apprendiamo dal suo rapporto che un certo filtro ha richiesto una certa quantità di memoria e non l'ha restituita all'heap N volte. È facile intuire che si tratterà di un riferimento alle funzioni di filtro interno del tipo MS_VOID_SOURCE. Il suo compito è prendere la memoria dal mucchio. Altri filtri dovrebbero restituirlo lì. Quelli. troveremo la perdita.

Per determinare in quale sezione della pipeline si è verificata un'inattività che ha portato a una perdita di memoria, si propone di introdurre un filtro aggiuntivo che sposta semplicemente i messaggi dall'input all'output, ma allo stesso tempo crea una copia non leggera dell'input messaggio in una normale copia "pesante", cancellando poi completamente il messaggio arrivato in ingresso. Chiameremo tale filtro un isolante. Riteniamo che, poiché il filtro è semplice, siano escluse eventuali perdite. E un'altra proprietà positiva: se la aggiungiamo in qualsiasi punto del nostro grafico, ciò non influirà in alcun modo sul funzionamento del circuito. Rappresenteremo il filtro isolante come un cerchio con un doppio contorno.

Abilita l'isolatore subito dopo il filtro voidsourse:
Esplorare il motore VoIP di Mediastreamer2. Parte 12

Eseguiamo nuovamente il programma con l'analizzatore e vediamo che questa volta l'analizzatore darà la colpa all'isolatore. Dopotutto, è lui che ora crea blocchi di dati, che poi vengono persi da uno o più filtri negligenti sconosciuti. Il passo successivo è spostare l'isolante lungo la catena verso destra, di un filtro, e ricominciare l'analisi. Quindi, passo dopo passo, spostando l'isolatore verso destra, otteniamo una situazione in cui il numero di blocchi di memoria "persi" nel successivo rapporto dell'analizzatore diminuisce. Ciò significa che in questa fase l'isolante è finito nella catena subito dopo il filtro problematico. Se fosse presente un solo filtro "cattivo", la perdita scomparirà del tutto. Pertanto, abbiamo localizzato il filtro problematico (o uno dei tanti). Dopo aver "riparato" il filtro, possiamo continuare a spostare l'isolatore verso destra lungo la catena fino a eliminare completamente le perdite di memoria.

Implementazione di un filtro isolatore

L'implementazione dell'isolatore sembra proprio un normale filtro. 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ù sottili, lo streamer multimediale offre la possibilità di sostituire le funzioni di accesso alla memoria con le proprie, che, oltre al lavoro principale, registreranno "Chi, dove e perché". Verranno sostituite tre funzioni. Questo viene fatto nel modo seguente:

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 funzione viene in soccorso nei casi in cui l'analizzatore rallenta così tanto i filtri da interrompere il funzionamento del sistema in cui è costruito il nostro circuito. In una situazione del genere, è necessario abbandonare l'analizzatore e utilizzare la sostituzione delle funzioni di memoria.

Abbiamo considerato un algoritmo di azioni per un grafo semplice che non contiene rami. Ma questo approccio può essere applicato ad altri casi, ovviamente con complicazioni, ma l’idea rimane la stessa.

Nel prossimo articolo esamineremo la questione della stima del carico del ticker e come gestire un carico di elaborazione eccessivo nello streamer multimediale.

Fonte: habr.com

Aggiungi un commento