Test QQuickPixmap::loadImageFromDevice()

SlowProvider is slow like QPdfIOHandler is; it's a QQuickImageProvider
rather than a QImageIOHandler for ease of testing, and we invoke it via
a QQuickImage subclass that calls QQuickPixmap::loadImageFromDevice(),
like QQuickPdfPageImage does.

Also get the old dataLeak() test running, as a drive-by. It still isn't
useful in CI though, because it has no built-in way of detecting leaks.

Task-number: QTBUG-114953
Change-Id: I9e2950fbaf4ea69969b3bd10a0d8e624f0e4e8c1
Reviewed-by: Axel Spoerl <axel.spoerl@qt.io>
This commit is contained in:
Shawn Rutledge 2023-08-29 23:39:08 +02:00
parent e903cf1b3e
commit 96e6822179
7 changed files with 215 additions and 15 deletions

View File

@ -1224,26 +1224,38 @@ QQuickPixmapCache *QQuickPixmapCache::instance()
QQuickPixmapCache::~QQuickPixmapCache()
{
destroyCache();
}
/*! \internal
Empty the cache completely, to prevent leaks. Returns the number of
leaked pixmaps (should always be \c 0).
This is work the destructor needs to do, but we put it into a function
only to make it testable in autotests, because the static instance()
cannot be destroyed before shutdown.
*/
int QQuickPixmapCache::destroyCache()
{
if (m_destroying)
return -1;
m_destroying = true;
#ifndef QT_NO_DEBUG
int leakedPixmaps = 0;
#endif
// Prevent unreferencePixmap() from assuming it needs to kick
// off the cache expiry timer, as we're shrinking the cache
// manually below after releasing all the pixmaps.
m_timerId = -2;
// unreference all (leaked) pixmaps
int leakedPixmaps = 0;
const auto cache = m_cache; // NOTE: intentional copy (QTBUG-65077); releasing items from the cache modifies m_cache.
for (auto *pixmap : cache) {
auto currRefCount = pixmap->refCount;
if (currRefCount) {
#ifndef QT_NO_DEBUG
leakedPixmaps++;
qCDebug(lcQsgLeak) << "leaked pixmap: refCount" << pixmap->refCount << pixmap->url << "frame" << pixmap->frame
<< "size" << pixmap->requestSize << "region" << pixmap->requestRegion;
#endif
while (currRefCount > 0) {
pixmap->release(this);
currRefCount--;
@ -1252,13 +1264,22 @@ QQuickPixmapCache::~QQuickPixmapCache()
}
// free all unreferenced pixmaps
while (m_lastUnreferencedPixmap) {
while (m_lastUnreferencedPixmap)
shrinkCache(20);
}
#ifndef QT_NO_DEBUG
qCDebug(lcQsgLeak, "Number of leaked pixmaps: %i", leakedPixmaps);
#endif
return leakedPixmaps;
}
qsizetype QQuickPixmapCache::referencedCost() const
{
qsizetype ret = 0;
QMutexLocker locker(&m_cacheMutex);
for (const auto *pixmap : std::as_const(m_cache)) {
if (pixmap->refCount)
ret += pixmap->cost();
}
return ret;
}
/*! \internal

View File

@ -62,10 +62,12 @@ private:
Q_DISABLE_COPY(QQuickPixmapCache)
void shrinkCache(int remove);
int destroyCache();
qsizetype referencedCost() const;
private:
QHash<QQuickPixmapKey, QQuickPixmapData *> m_cache;
QMutex m_cacheMutex; // avoid simultaneous iteration and modification
mutable QMutex m_cacheMutex; // avoid simultaneous iteration and modification
QQuickPixmapData *m_unreferencedPixmaps = nullptr;
QQuickPixmapData *m_lastUnreferencedPixmap = nullptr;
@ -76,6 +78,7 @@ private:
friend class QQuickPixmap;
friend class QQuickPixmapData;
friend class tst_qquickpixmapcache;
};
QT_END_NAMESPACE

View File

@ -22,6 +22,7 @@ list(APPEND test_data ${test_data_glob})
qt_internal_add_test(tst_qquickpixmapcache
SOURCES
tst_qquickpixmapcache.cpp
deviceloadingimage.h deviceloadingimage.cpp
LIBRARIES
Qt::Concurrent
Qt::CorePrivate
@ -46,3 +47,7 @@ qt_internal_extend_target(tst_qquickpixmapcache CONDITION NOT ANDROID AND NOT IO
DEFINES
QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
)
qt_add_qml_module(tst_qquickpixmapcache
URI PixmapCacheTest
)

View File

@ -0,0 +1,45 @@
import QtQuick
import PixmapCacheTest
TableView {
id: root
width: 640
height: 480
model: 100
rowSpacing: 6
property real size: 40
columnWidthProvider: function(col) { return root.size }
rowHeightProvider: function(col) { return root.size }
Timer {
interval: 200
repeat: true
running: true
onTriggered: {
root.size = Math.random() * 200
root.positionViewAtRow(Math.round(Math.random() * 100), TableView.Visible)
}
}
delegate: DeviceLoadingImage {
required property int index
width: root.size; height: root.size
asynchronous: true
source: "image://slow/" + index
sourceSize.width: width
sourceSize.height: height
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: "red"
}
Text {
color: "red"
style: Text.Outline
styleColor: "white"
text: index + "\nsize " + root.size.toFixed(1)
}
}
}

View File

@ -0,0 +1,43 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "deviceloadingimage.h"
#include <QtQuick/private/qquickimage_p_p.h>
Q_DECLARE_LOGGING_CATEGORY(lcTests)
void DeviceLoadingImage::load()
{
auto *d = static_cast<QQuickImagePrivate *>(QQuickImagePrivate::get(this));
static int thisRequestFinished = -1;
if (thisRequestFinished == -1) {
thisRequestFinished =
QQuickImageBase::staticMetaObject.indexOfSlot("requestFinished()");
}
const QQmlContext *context = qmlContext(this);
Q_ASSERT(context);
QUrl resolved = context->resolvedUrl(d->url);
device = std::make_unique<QFile>(resolved.toLocalFile());
d->pix.loadImageFromDevice(qmlEngine(this), device.get(), context->resolvedUrl(d->url),
d->sourceClipRect.toRect(), d->sourcesize * d->devicePixelRatio,
QQuickImageProviderOptions(), d->currentFrame, d->frameCount);
qCDebug(lcTests) << "loading page" << d->currentFrame << "of" << d->frameCount << "status" << d->pix.status();
switch (d->pix.status()) {
case QQuickPixmap::Ready:
pixmapChange();
break;
case QQuickPixmap::Loading:
d->pix.connectFinished(this, thisRequestFinished);
if (d->status != Loading) {
d->status = Loading;
emit statusChanged(d->status);
}
break;
default:
qCWarning(lcTests) << "unexpected status" << d->pix.status();
break;
}
}

View File

@ -0,0 +1,19 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtCore/QFile>
#include <QtQuick/private/qquickimage_p.h>
class DeviceLoadingImage : public QQuickImage
{
Q_OBJECT
QML_NAMED_ELEMENT(DeviceLoadingImage)
public:
DeviceLoadingImage(QQuickItem *parent = nullptr) : QQuickImage(parent) { }
protected:
void load() override;
std::unique_ptr<QFile> device;
};

View File

@ -5,16 +5,47 @@
#include <QtQuick/private/qquickpixmapcache_p.h>
#include <QtQml/qqmlengine.h>
#include <QtQuick/qquickimageprovider.h>
#include <QtQuick/qquickview.h>
#include <QtQml/QQmlComponent>
#include <QNetworkReply>
#include <QtGui/qpainter.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <QtQuickTestUtils/private/testhttpserver_p.h>
#include <QtQuickTestUtils/private/viewtestutils_p.h>
#if QT_CONFIG(concurrent)
#include <qtconcurrentrun.h>
#include <qfuture.h>
#endif
Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests")
class SlowProvider : public QQuickImageProvider
{
public:
SlowProvider() : QQuickImageProvider(Pixmap) {}
QPixmap requestPixmap(const QString &id, QSize *size, const QSize& requestedSize) override
{
const int row = id.toInt();
qCDebug(lcTests) << requestCount << QThread::currentThread() << "row" << row << requestedSize;
QPixmap image(requestedSize);
QPainter painter(&image);
const QColor c(128, row % 8 * 32, 64);
painter.fillRect(0, 0, requestedSize.width(), requestedSize.height(), c);
if (size)
*size = requestedSize;
++requestCount;
QThread::currentThread()->msleep(row);
return image;
}
int requestCount = 0;
};
Q_DECLARE_METATYPE(SlowProvider*);
QT_BEGIN_NAMESPACE
#define PIXMAP_DATA_LEAK_TEST 0
class tst_qquickpixmapcache : public QQmlDataTest
@ -41,6 +72,7 @@ private slots:
#if PIXMAP_DATA_LEAK_TEST
void dataLeak();
#endif
void slowDevice();
private:
QQmlEngine engine;
TestHTTPServer server;
@ -450,7 +482,6 @@ void tst_qquickpixmapcache::asynchronousNoCache()
QScopedPointer<QObject> root {component.create()}; // should not crash
}
#if PIXMAP_DATA_LEAK_TEST
// This test should not be enabled by default as it
// produces spurious output in the expected case.
@ -460,9 +491,9 @@ class DataLeakView : public QQuickView
Q_OBJECT
public:
explicit DataLeakView() : QQuickView()
explicit DataLeakView(const QUrl &src) : QQuickView()
{
setSource(testFileUrl("dataLeak.qml"));
setSource(src);
}
void showFor2Seconds()
@ -483,11 +514,11 @@ void tst_qquickpixmapcache::dataLeak()
// Unfortunately, since the QQuickPixmapCache
// is a singleton, and it releases the cache
// entries on dtor (application exit), we must use
// valgrind to determine whether it leaks or not.
// valgrind or ASAN to determine whether it leaks or not.
QQuickPixmap *p1 = new QQuickPixmap;
QQuickPixmap *p2 = new QQuickPixmap;
{
QScopedPointer<DataLeakView> test(new DataLeakView);
QScopedPointer<DataLeakView> test(new DataLeakView(testFileUrl("dataLeak.qml")));
test->showFor2Seconds();
dataLeakPixmap()->load(test->engine(), testFileUrl("exists.png"));
p1->load(test->engine(), testFileUrl("exists.png"));
@ -503,6 +534,39 @@ void tst_qquickpixmapcache::dataLeak()
#endif
#undef PIXMAP_DATA_LEAK_TEST
/*!
Test lots of async QQuickPixmap::loadImageFromDevice() jobs with random
sizes and frames, so that cached images are seldom reused. Some jobs get
cancelled, some succeed. This should not lead to any cache leaks.
Similar to the QtPdf usecase in QTBUG-114953
*/
void tst_qquickpixmapcache::slowDevice()
{
#ifdef QT_BUILD_INTERNAL
auto *provider = new SlowProvider;
engine.addImageProvider("slow", provider); // takes ownership
{
QQuickView window(&engine, nullptr);
QVERIFY(QQuickTest::showView(window, testFileUrl("tableViewWithDeviceLoadingImages.qml")));
// Timer generates 5 requests / sec; TableView shows multiple delegates (depending on size);
// so we should get 100 requests in some fraction of 20 sec. Give it 30 to be sure.
QTRY_COMPARE_GE_WITH_TIMEOUT(provider->requestCount, 100, 30000);
const int cacheCount = QQuickPixmapCache::instance()->m_cache.size();
qCDebug(lcTests) << "cached pixmaps" << cacheCount;
QCOMPARE_GT(cacheCount, 0);
} // window goes out of scope: all QQuickPixmapData instances should be eventually unreferenced
QTRY_COMPARE(QQuickPixmapCache::instance()->referencedCost(), 0);
const int leakedPixmaps = QQuickPixmapCache::instance()->destroyCache();
QCOMPARE(leakedPixmaps, 0);
#else
QSKIP("This test relies on private APIs that are only exported in developer-builds");
#endif
}
QT_END_NAMESPACE
QTEST_MAIN(tst_qquickpixmapcache)
#include "tst_qquickpixmapcache.moc"