Exploring the VoIP engine Mediastreamer2. Part 12

The article material is taken from my Zen channel.

Exploring the VoIP engine Mediastreamer2. Part 12

Previously article, I promised to consider the issue of load assessment on the ticker and ways to combat excessive computational load in the media streamer. However, I decided it would be more logical to address issues related to debugging craft filters, associated with data movement, and then discuss performance optimization.

Debugging Craft Filters

After discussing the mechanism of data movement in the media streamer in the previous article, it makes sense to talk about the hidden dangers within it. One of the features of the 'data flow' principle is that memory allocation from the heap occurs in the filters at the source of the data stream, while memory deallocation with a return to the heap is performed by the filters located at the end of the stream path. Additionally, the creation and destruction of new data can happen at intermediate points. In general, it is not the filter that created the data block that deallocates memory.

From the perspective of transparent memory monitoring, it would be wise for the filter, upon receiving an input block, to destroy it immediately after processing, thereby freeing memory and outputting a newly created block with the output data. In this case, a memory leak in the filter would be easily traceable — if the analyzer detects a leak in the filter, it means the next filter does not properly destroy the incoming blocks and has an error. However, from the viewpoint of maintaining high performance, this approach to handling data blocks is unproductive — it leads to a high number of allocation/deallocation operations for data blocks without any useful output.

For this reason, media stream filters, in order not to slow down data processing, use functions that create lightweight copies when copying messages (we discussed these in the previous article). These functions only create a new instance of the message header, "attaching" to it a block of data from the copied "old" message. As a result, two headers are linked to one block of data, and the reference counter in the data block is incremented. However, it will appear as two messages. There can be more messages with such a "shared" data block; for example, the MS_TEE filter generates a dozen of these lightweight copies at once, distributing them among its outputs. If all filters in the chain work correctly, by the end of the pipeline, this reference counter should reach zero, triggering the memory release function: ms_free(). If the call does not occur, it means that this piece of memory will not return to the heap, i.e., it will "leak". The price of using lightweight copies is the loss of the ability to easily determine (as would be the case with regular copies) which filter is leaking memory.

Since the responsibility for finding memory leaks in "native" filters lies with the media streamer developers, you probably won't have to debug them. However, with your custom filter — you are the master of your own fate, and how carefully you approach this will determine the time you spend searching for leaks in your code. To reduce your debugging woes, we should look at the techniques for localizing leaks when developing filters. Moreover, it may happen that the leak only manifests itself when the filter is applied in a real system, where the number of "suspects" can be enormous, and the time for debugging is limited.

How does a memory leak manifest?

It is logical to assume that in the program's output top there will be an increasing percentage of memory occupied by your application.

The external manifestation will consist of the system starting to respond sluggishly to mouse movements, and the screen will redraw slowly. The system log may also grow, consuming space on the hard drive. Meanwhile, your application will begin to behave strangely, failing to respond to commands or not being able to open files, etc.

To detect the fact of a memory leak, we will use a memory analyzer (hereinafter referred to as the analyzer). This can be Valgrind (a good article tool) or built into the compiler gcc Control Flow Integrity or something else. If the analyzer shows that a leak occurs in one of the filters of the graph, it means it's time to apply one of the methods described below.

The Three Pines Method

As mentioned earlier, when a memory leak occurs, the analyzer will point to the filter that requested memory allocation from the heap. However, it will not indicate which filter "forgot" to return it, which is the actual culprit. Thus, the analyzer can only confirm our suspicions but cannot identify the root of the problem.

To determine the location of the "bad" filter in the graph, one can reduce the graph to the minimal number of nodes at which the analyzer still detects the leak and localize the problematic filter within the remaining three pines.

However, it may happen that reducing the number of filters in the graph disrupts the normal interaction of filters with other elements of your system, causing the leak to no longer manifest. In this case, you will have to work with the full-sized graph and use the approach outlined below.

The Sliding Isolator Method

For simplicity, we will use a graph that consists of a single chain of filters. It is illustrated in the figure.

Exploring the VoIP engine Mediastreamer2. Part 12

A regular graph, where alongside the ready-made filters of the media streamer, four custom filters F1…F4 of different types have been applied, which you created a while ago and have no doubts about their correctness. However, let's assume that a few of them have a memory leak. By running our program under the analyzer's supervision, we find out from its report that a certain filter requested a certain amount of memory and did not return it to the heap N times. It will be easy to guess that there will be a reference to the internal functions of the filter type MS_VOID_SOURCE. Its task is to take memory from the heap. Other filters should return it there. This means we have detected a leak.

To identify at which point in the pipeline inactivity led to the memory leak, it is proposed to introduce an additional filter that simply passes messages from input to output while creating a non-light, in normal 'heavy' copy of the incoming message, then completely deleting the message that was input. We will call such a filter an isolator. We assume that since the filter is simple, a leak in it is excluded. And one more positive property — if we add it anywhere in our graph, it will not affect the operation of the scheme. We will depict the isolator filter as a circle with a double outline.

We turn on the isolator immediately after the voidsource filter.
Exploring the VoIP engine Mediastreamer2. Part 12

Again we run the program with the analyzer, and see that this time the analyzer puts the blame on the isolator. After all, it is now creating data blocks that are then lost by an unknown negligent filter (or filters). The next step is to move the isolator one filter to the right along the chain and run the analysis again. Thus, by moving the isolator to the right step by step, we will reach a situation where the next report from the analyzer indicates a decrease in the number of 'leaked' memory blocks. This means that at this step the isolator found itself in the chain immediately after the problematic filter. If there was only one 'bad' filter, the leak will disappear altogether. In this way, we localized the problematic filter (or one of several). Once we 'fix' the filter, we can continue to move the isolator to the right in the chain until we completely eliminate the memory leaks.

Implementation of the isolator filter.

The implementation of the isolator looks just like a regular filter. Header file:

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

The filter itself:

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

Method for overriding memory management functions

For more in-depth studies, the media streamer provides the ability to override memory access functions with your own, which in addition to their primary task will log "Who, where, and why." Three functions are overridden. This is done as follows:

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

This capability comes in handy when the analyzer slows down filter operations to the point where it disrupts the functioning of the system into which our scheme is integrated. In such a situation, it becomes necessary to forego the analyzer and use memory function overriding.

We have examined the action algorithm for a simple graph without branches. However, this approach can also be applied to other cases, of course with increased complexity, but the idea will remain the same.

In the next article, we will discuss load assessment on the ticker and ways to combat excessive computational load in the media streamer.

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster