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()
|
||||
{
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 <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"
|
||||
|
|
Loading…
Reference in New Issue