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:
parent
e903cf1b3e
commit
96e6822179
|
@ -1224,26 +1224,38 @@ QQuickPixmapCache *QQuickPixmapCache::instance()
|
||||||
|
|
||||||
QQuickPixmapCache::~QQuickPixmapCache()
|
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;
|
m_destroying = true;
|
||||||
|
|
||||||
#ifndef QT_NO_DEBUG
|
|
||||||
int leakedPixmaps = 0;
|
|
||||||
#endif
|
|
||||||
// Prevent unreferencePixmap() from assuming it needs to kick
|
// Prevent unreferencePixmap() from assuming it needs to kick
|
||||||
// off the cache expiry timer, as we're shrinking the cache
|
// off the cache expiry timer, as we're shrinking the cache
|
||||||
// manually below after releasing all the pixmaps.
|
// manually below after releasing all the pixmaps.
|
||||||
m_timerId = -2;
|
m_timerId = -2;
|
||||||
|
|
||||||
// unreference all (leaked) pixmaps
|
// 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.
|
const auto cache = m_cache; // NOTE: intentional copy (QTBUG-65077); releasing items from the cache modifies m_cache.
|
||||||
for (auto *pixmap : cache) {
|
for (auto *pixmap : cache) {
|
||||||
auto currRefCount = pixmap->refCount;
|
auto currRefCount = pixmap->refCount;
|
||||||
if (currRefCount) {
|
if (currRefCount) {
|
||||||
#ifndef QT_NO_DEBUG
|
|
||||||
leakedPixmaps++;
|
leakedPixmaps++;
|
||||||
qCDebug(lcQsgLeak) << "leaked pixmap: refCount" << pixmap->refCount << pixmap->url << "frame" << pixmap->frame
|
qCDebug(lcQsgLeak) << "leaked pixmap: refCount" << pixmap->refCount << pixmap->url << "frame" << pixmap->frame
|
||||||
<< "size" << pixmap->requestSize << "region" << pixmap->requestRegion;
|
<< "size" << pixmap->requestSize << "region" << pixmap->requestRegion;
|
||||||
#endif
|
|
||||||
while (currRefCount > 0) {
|
while (currRefCount > 0) {
|
||||||
pixmap->release(this);
|
pixmap->release(this);
|
||||||
currRefCount--;
|
currRefCount--;
|
||||||
|
@ -1252,13 +1264,22 @@ QQuickPixmapCache::~QQuickPixmapCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
// free all unreferenced pixmaps
|
// free all unreferenced pixmaps
|
||||||
while (m_lastUnreferencedPixmap) {
|
while (m_lastUnreferencedPixmap)
|
||||||
shrinkCache(20);
|
shrinkCache(20);
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef QT_NO_DEBUG
|
|
||||||
qCDebug(lcQsgLeak, "Number of leaked pixmaps: %i", leakedPixmaps);
|
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
|
/*! \internal
|
||||||
|
|
|
@ -62,10 +62,12 @@ private:
|
||||||
Q_DISABLE_COPY(QQuickPixmapCache)
|
Q_DISABLE_COPY(QQuickPixmapCache)
|
||||||
|
|
||||||
void shrinkCache(int remove);
|
void shrinkCache(int remove);
|
||||||
|
int destroyCache();
|
||||||
|
qsizetype referencedCost() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QHash<QQuickPixmapKey, QQuickPixmapData *> m_cache;
|
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_unreferencedPixmaps = nullptr;
|
||||||
QQuickPixmapData *m_lastUnreferencedPixmap = nullptr;
|
QQuickPixmapData *m_lastUnreferencedPixmap = nullptr;
|
||||||
|
@ -76,6 +78,7 @@ private:
|
||||||
|
|
||||||
friend class QQuickPixmap;
|
friend class QQuickPixmap;
|
||||||
friend class QQuickPixmapData;
|
friend class QQuickPixmapData;
|
||||||
|
friend class tst_qquickpixmapcache;
|
||||||
};
|
};
|
||||||
|
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
|
|
@ -22,6 +22,7 @@ list(APPEND test_data ${test_data_glob})
|
||||||
qt_internal_add_test(tst_qquickpixmapcache
|
qt_internal_add_test(tst_qquickpixmapcache
|
||||||
SOURCES
|
SOURCES
|
||||||
tst_qquickpixmapcache.cpp
|
tst_qquickpixmapcache.cpp
|
||||||
|
deviceloadingimage.h deviceloadingimage.cpp
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Concurrent
|
Qt::Concurrent
|
||||||
Qt::CorePrivate
|
Qt::CorePrivate
|
||||||
|
@ -46,3 +47,7 @@ qt_internal_extend_target(tst_qquickpixmapcache CONDITION NOT ANDROID AND NOT IO
|
||||||
DEFINES
|
DEFINES
|
||||||
QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
|
QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
qt_add_qml_module(tst_qquickpixmapcache
|
||||||
|
URI PixmapCacheTest
|
||||||
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
|
@ -5,16 +5,47 @@
|
||||||
#include <QtQuick/private/qquickpixmapcache_p.h>
|
#include <QtQuick/private/qquickpixmapcache_p.h>
|
||||||
#include <QtQml/qqmlengine.h>
|
#include <QtQml/qqmlengine.h>
|
||||||
#include <QtQuick/qquickimageprovider.h>
|
#include <QtQuick/qquickimageprovider.h>
|
||||||
|
#include <QtQuick/qquickview.h>
|
||||||
#include <QtQml/QQmlComponent>
|
#include <QtQml/QQmlComponent>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
#include <QtGui/qpainter.h>
|
||||||
#include <QtQuickTestUtils/private/qmlutils_p.h>
|
#include <QtQuickTestUtils/private/qmlutils_p.h>
|
||||||
#include <QtQuickTestUtils/private/testhttpserver_p.h>
|
#include <QtQuickTestUtils/private/testhttpserver_p.h>
|
||||||
|
#include <QtQuickTestUtils/private/viewtestutils_p.h>
|
||||||
|
|
||||||
#if QT_CONFIG(concurrent)
|
#if QT_CONFIG(concurrent)
|
||||||
#include <qtconcurrentrun.h>
|
#include <qtconcurrentrun.h>
|
||||||
#include <qfuture.h>
|
#include <qfuture.h>
|
||||||
#endif
|
#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
|
#define PIXMAP_DATA_LEAK_TEST 0
|
||||||
|
|
||||||
class tst_qquickpixmapcache : public QQmlDataTest
|
class tst_qquickpixmapcache : public QQmlDataTest
|
||||||
|
@ -41,6 +72,7 @@ private slots:
|
||||||
#if PIXMAP_DATA_LEAK_TEST
|
#if PIXMAP_DATA_LEAK_TEST
|
||||||
void dataLeak();
|
void dataLeak();
|
||||||
#endif
|
#endif
|
||||||
|
void slowDevice();
|
||||||
private:
|
private:
|
||||||
QQmlEngine engine;
|
QQmlEngine engine;
|
||||||
TestHTTPServer server;
|
TestHTTPServer server;
|
||||||
|
@ -450,7 +482,6 @@ void tst_qquickpixmapcache::asynchronousNoCache()
|
||||||
QScopedPointer<QObject> root {component.create()}; // should not crash
|
QScopedPointer<QObject> root {component.create()}; // should not crash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#if PIXMAP_DATA_LEAK_TEST
|
#if PIXMAP_DATA_LEAK_TEST
|
||||||
// This test should not be enabled by default as it
|
// This test should not be enabled by default as it
|
||||||
// produces spurious output in the expected case.
|
// produces spurious output in the expected case.
|
||||||
|
@ -460,9 +491,9 @@ class DataLeakView : public QQuickView
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit DataLeakView() : QQuickView()
|
explicit DataLeakView(const QUrl &src) : QQuickView()
|
||||||
{
|
{
|
||||||
setSource(testFileUrl("dataLeak.qml"));
|
setSource(src);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showFor2Seconds()
|
void showFor2Seconds()
|
||||||
|
@ -483,11 +514,11 @@ void tst_qquickpixmapcache::dataLeak()
|
||||||
// Unfortunately, since the QQuickPixmapCache
|
// Unfortunately, since the QQuickPixmapCache
|
||||||
// is a singleton, and it releases the cache
|
// is a singleton, and it releases the cache
|
||||||
// entries on dtor (application exit), we must use
|
// 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 *p1 = new QQuickPixmap;
|
||||||
QQuickPixmap *p2 = new QQuickPixmap;
|
QQuickPixmap *p2 = new QQuickPixmap;
|
||||||
{
|
{
|
||||||
QScopedPointer<DataLeakView> test(new DataLeakView);
|
QScopedPointer<DataLeakView> test(new DataLeakView(testFileUrl("dataLeak.qml")));
|
||||||
test->showFor2Seconds();
|
test->showFor2Seconds();
|
||||||
dataLeakPixmap()->load(test->engine(), testFileUrl("exists.png"));
|
dataLeakPixmap()->load(test->engine(), testFileUrl("exists.png"));
|
||||||
p1->load(test->engine(), testFileUrl("exists.png"));
|
p1->load(test->engine(), testFileUrl("exists.png"));
|
||||||
|
@ -503,6 +534,39 @@ void tst_qquickpixmapcache::dataLeak()
|
||||||
#endif
|
#endif
|
||||||
#undef PIXMAP_DATA_LEAK_TEST
|
#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)
|
QTEST_MAIN(tst_qquickpixmapcache)
|
||||||
|
|
||||||
#include "tst_qquickpixmapcache.moc"
|
#include "tst_qquickpixmapcache.moc"
|
||||||
|
|
Loading…
Reference in New Issue