LINUX.ORG.RU

(Решено) Утечка памяти при чтении картинки из поулченных данных по TCP в Qt

 , , ,


0

2

Добрый день. Столкнулся с проблемой утечки памяти при формировании QImage из полученных через TCP данных. Причем утечку вызывает именно создание экземпляра QImage из fromData, если закомментировать эту строку, то утечки не будет.

Вся схема потока данных такая: сторона отправки читает данные (Метаданные + QImage в виде QByteArray) из файла и отправляет на прием, а сторона приема читает метаданные и картинку и отображает на экране.

Мне кажется, что проблема связанна именно с TCP, т.к. для поиска места где именно течет память я написал приложение видеоплеер. Этот видеоплеер читает данные из файла (код скопировал со стороны отправки), и отображает на экране (код скопировал со стороны приема). То есть видеоплеер объединяет принцип действия отправки и приема, но отправляет данные напрямую, без TCP. И в этом видеоплеере утечка памяти не наблюдается.

Нужно отметить, что сам файл откуда читаются данные немного битый, из-за чего при вызове QImage image = QImage::fromData(imgData, "JPEG"); в лог выводится сообщение: Corrupt JPEG data: 5 extraneous bytes before marker 0xda

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

В .h файле на стороне приема у меня объявлен QTcpSocket: QTcpSocket* m_tcpSocket;

При запуске приложения устанавливаю соединение:

m_tcpSocket->connectToHost(ip, port, flag);
auto connStatus = m_tcpSocket->waitForConnected(1000);
if (connStatus){
   connect(m_tcpSocket, &QTcpSocket::readyRead, this, &StreamChannel::onDataReady);
}
return connStatus;

И затем при получении данных будем попадать в слот:

void StreamChannel::onDataReady()
{
    uint32_t packetStart = 0;
    while(packetStart != SmkpProtocol::PACKET_START && m_tcpSocket->bytesAvailable()) {
         m_tcpSocket->read(reinterpret_cast<char*>(&packetStart), sizeof(uint32_t));
    }
    if(packetStart != SmkpProtocol::PACKET_START) {
        return;
    }
    Metadata md;
    m_tcpSocket->read(reinterpret_cast<char*>(&md), sizeof(SmkpProtocol::Metadata));
    QByteArray imgData(md.videodataSize, '\0');
    m_tcpSocket->read(imgData.data(), imgData.size());
    QImage image = QImage::fromData(imgData,  "JPEG");
    //...дальнейшая передача image на экран
}

В моем коде PACKET_START - это константа преамбула типа uint32_t для сортировки пакетов, а Metadata - это структура состоящая из набора uint32_t.

Утечку памяти вызывает строка QImage image = QImage::fromData(imgData, "JPEG");, если убрать эту строку (и соответственно дальнейшее использование image), то утечки памяти не будет. Тут проблема в том, что даже если никак не использовать image, просто объявить и всё, то память всё равно будет течь

Версия Qt 5.12.2, пишу по Astra Linux

Update: Господа, приношу извинения. Проблема была вообще не связана ни с QImage ни с TCP. В основном классе я создаю несколько экземпляров StreamChannel, и до этого они работали в одном потоке. После того, как я раскидал их в разные потоки QThread утечка памяти ушла. Если честно, я не совсем разобрался как именно это помогло, ведь в каждом экземпляре StreamChannel должен быть свой QTcpSocket* m_tcpSocket, которые вообще никак между собой не связаны. Но, тем не менее, проблема решена



Последнее исправление: john_johnson (всего исправлений: 3)

Если убрать эту строчку, то в программе не будет переменной image, с которой очевидно ведётся какая-то работа дальше, и программа не скомпилируется. Может быть, дело не в этой строчке, а в том, что дальше делается с этой image?

unC0Rr ★★★★★
()
Ответ на: комментарий от unC0Rr

Извиняюсь за неточность, вы правы. Да, если закомментировать объявление image, то и дальнейшее использование необходимо убрать. Я пробовал разные варианты, и даже в случае, если сформировать QImage image через fromData и никак потом не использовать, то всё равно память будет течь

john_johnson
() автор топика
Ответ на: комментарий от john_johnson

Я нахожу это странным. Проведите эксперимент: оставьте весь код как есть, но поставьте return после объявления image. Запустите, проверьте на утечку. Затем повторите то же самое, но с return до объявления image.

unC0Rr ★★★★★
()
    uint32_t packetStart = 0;
    while(packetStart != SmkpProtocol::PACKET_START && m_tcpSocket->bytesAvailable()) {
         m_tcpSocket->read(reinterpret_cast<char*>(&packetStart), sizeof(uint32_t));



а тут точно всё норм с `uint32_t packetStart` и `m_tcpSocket->read(reinterpret_cast<char*>(&packetStart)` ? Оно в каком endianness передаётся там?
https://godbolt.org/z/jnPqsEzKa — надеюсь понятен пример — можно в асме посмотреть что за значение подставляется (оно там с обратным порядком байтов для разных архитектур)

safocl ★★
()
Ответ на: комментарий от safocl

Да, с преамбулой должно быть всё норм. На стороне отправки пакеты формируются так:

//в .h файле:
using PacketStartType = std::remove_cv<decltype(PACKET_START)>::type;
const PacketStartType m_packetStart = PACKET_START;

//в .cpp:
QByteArray arr;
arr.resize(sizeof(PACKET_START) + sizeof(Metadata) + m_imageData.size());
auto dataPtr = arr.data();
auto offset = 0;
memcpy(dataPtr, reinterpret_cast<const char*>(&m_packetStart), sizeof(PacketStartType));
offset += sizeof(PacketStartType);
 memcpy(dataPtr + offset, reinterpret_cast<const char*>(&m_metaData), sizeof(Metadata));
offset += sizeof(Metadata);
memcpy(dataPtr + offset, m_imageData.data(), m_imageData.size());
return arr;

И я через отладку перепроверил, по условию if(packetStart != SmkpProtocol::PACKET_START) никогда не проходим и в return не попадаем, то есть преамбула всегда валидная.

По поводу того, в каком endianness передается не подскажу, к сожалению не знаю точно. Могу лишь сказать, что порядок байт в моем коде нигде не меняется и всё должно быть по дефолту

john_johnson
() автор топика
Ответ на: комментарий от john_johnson

В таком случае, возможно, никакой утечки нет, а есть некорректная интерпретация происходящего? Сколько раз создаётся QImage за время работы программы, какие симптомы утечки?

unC0Rr ★★★★★
()
Ответ на: комментарий от unC0Rr

Да, возможно имеет место некорректная интерпретация.

Пакеты данных приходят с частотой 25 штук в секунду, соответственно создается 25 QImage в секунду.

Симптом утечки - постоянное плавное увеличение занимаемой памяти приложения при получении пакетов. Отслеживаю через Системный монитор (диспетчер задач) в Astra Linux. Изначально приложение занимает 38 тысяч КиБ, через минуту уже около 60 тысяч КиБ. Увеличивается на около 20 тыс КиБ в минуту. Тестировал и на более долгие промежутки времени. Объем приложения в опреативе только растет, и никогда не уменьшается

john_johnson
() автор топика
Ответ на: комментарий от BRE

По идее, после формирования QImage я их передаю в GUI на QLabel для отображения. Но для отладки чтобы найти где происходит утечка памяти я убрал этот вывод на GUI. То есть сейчас у меня происходит только объявление QImage image без дальнейшего использования. Но в таком случае утечка памяти всё равно происходит.

Пробовал объявлять без использования и сразу удалять, но утечка остается. Удалял через присваивание к пустому QImage как image = QImage(), но так тоже течет. Хотя, насколько я помню, Qt должна сама удалять эту картинку, после того как выйдем из области видимости, ведь в другие классы и методы картинка не передается, и ссылок на нее не создается

john_johnson
() автор топика
Ответ на: комментарий от john_johnson

А на какой параметр в системном мониторе ты смотришь для определения утечки?

Ты можешь запустить htop и по нему посмотреть утечку и точное название столбца на который ты смотришь?

BRE ★★
()

попробуй ради эксперимента сделать QImage динамическим, через new. и явно удаляй через delete.

я подозреваю, что культя при присвоении объекта удваивает указатели (см. QImage implicit data sharing). может, стоит убрать оператор присвоения и вызывать сразу конструктор.

вообще, ещё вот тут человек очень подробно собрал и описал косяки QImage и, в том числе, утечки памяти при несовпадении форматов:

https://runebook.dev/en/articles/qt/qimage/operator-eq-1

может, наведёт на какие-то мысли.

Iron_Bug ★★★★★
()
Ответ на: комментарий от john_johnson

У Вас есть возможность собрать приложение с -fsanitize=address (чтобы воспользоваться LeakSanitizer) или потерпеть и запустить под valgrind --leak-check=full --track-origins=yes? Они могут подсказать ошибку.

В KDE также используют heaptrack, возможно, с ним будет не так медленно и тяжело.

anonymous
()
m_tcpSocket->read(reinterpret_cast<char*>(&md), sizeof(SmkpProtocol::Metadata));

Reads at most maxSize bytes from the device into data, and returns the number of bytes read. If an error occurs, such as when attempting to read from a device opened in WriteOnly mode, this function returns -1.

0 is returned when no more data is available for reading. However, reading past the end of the stream is considered an error, so this function returns -1 in those cases (that is, reading on a closed socket or after a process has died).

alnkapa
()