diff --git a/src/plugins/qmldesigner/designercore/imagecache/asynchronousexplicitimagecache.cpp b/src/plugins/qmldesigner/designercore/imagecache/asynchronousexplicitimagecache.cpp new file mode 100644 index 00000000000..e73f84a66fa --- /dev/null +++ b/src/plugins/qmldesigner/designercore/imagecache/asynchronousexplicitimagecache.cpp @@ -0,0 +1,173 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "asynchronousexplicitimagecache.h" + +#include "imagecachestorage.h" + +#include + +namespace QmlDesigner { + +AsynchronousExplicitImageCache::AsynchronousExplicitImageCache(ImageCacheStorageInterface &storage) + : m_storage(storage) +{ + m_backgroundThread = std::thread{[this] { + while (isRunning()) { + if (auto [hasEntry, entry] = getEntry(); hasEntry) { + request(entry.name, + entry.extraId, + entry.requestType, + std::move(entry.captureCallback), + std::move(entry.abortCallback), + m_storage); + } + + waitForEntries(); + } + }}; +} + +AsynchronousExplicitImageCache::~AsynchronousExplicitImageCache() +{ + clean(); + wait(); +} + +void AsynchronousExplicitImageCache::request(Utils::SmallStringView name, + Utils::SmallStringView extraId, + AsynchronousExplicitImageCache::RequestType requestType, + ImageCache::CaptureImageCallback captureCallback, + ImageCache::AbortCallback abortCallback, + ImageCacheStorageInterface &storage) +{ + const auto id = extraId.empty() ? Utils::PathString{name} + : Utils::PathString::join({name, "+", extraId}); + + const auto entry = requestType == RequestType::Image + ? storage.fetchImage(id, Sqlite::TimeStamp{}) + : storage.fetchSmallImage(id, Sqlite::TimeStamp{}); + + if (entry.hasEntry && !entry.image.isNull()) + captureCallback(entry.image); + else + abortCallback(ImageCache::AbortReason::Failed); +} + +void AsynchronousExplicitImageCache::wait() +{ + stopThread(); + m_condition.notify_all(); + if (m_backgroundThread.joinable()) + m_backgroundThread.join(); +} + +void AsynchronousExplicitImageCache::requestImage(Utils::PathString name, + ImageCache::CaptureImageCallback captureCallback, + ImageCache::AbortCallback abortCallback, + Utils::SmallString extraId) +{ + addEntry(std::move(name), + std::move(extraId), + std::move(captureCallback), + std::move(abortCallback), + RequestType::Image); + m_condition.notify_all(); +} + +void AsynchronousExplicitImageCache::requestSmallImage(Utils::PathString name, + ImageCache::CaptureImageCallback captureCallback, + ImageCache::AbortCallback abortCallback, + Utils::SmallString extraId) +{ + addEntry(std::move(name), + std::move(extraId), + std::move(captureCallback), + std::move(abortCallback), + RequestType::SmallImage); + m_condition.notify_all(); +} + +void AsynchronousExplicitImageCache::clean() +{ + clearEntries(); +} + +std::tuple AsynchronousExplicitImageCache::getEntry() +{ + std::unique_lock lock{m_mutex}; + + if (m_requestEntries.empty()) + return {false, RequestEntry{}}; + + RequestEntry entry = m_requestEntries.front(); + m_requestEntries.pop_front(); + + return {true, entry}; +} + +void AsynchronousExplicitImageCache::addEntry(Utils::PathString &&name, + Utils::SmallString &&extraId, + ImageCache::CaptureImageCallback &&captureCallback, + ImageCache::AbortCallback &&abortCallback, + RequestType requestType) +{ + std::unique_lock lock{m_mutex}; + + m_requestEntries.emplace_back(std::move(name), + std::move(extraId), + std::move(captureCallback), + std::move(abortCallback), + requestType); +} + +void AsynchronousExplicitImageCache::clearEntries() +{ + std::unique_lock lock{m_mutex}; + for (RequestEntry &entry : m_requestEntries) + entry.abortCallback(ImageCache::AbortReason::Abort); + m_requestEntries.clear(); +} + +void AsynchronousExplicitImageCache::waitForEntries() +{ + std::unique_lock lock{m_mutex}; + if (m_requestEntries.empty()) + m_condition.wait(lock, [&] { return m_requestEntries.size() || m_finishing; }); +} + +void AsynchronousExplicitImageCache::stopThread() +{ + std::unique_lock lock{m_mutex}; + m_finishing = true; +} + +bool AsynchronousExplicitImageCache::isRunning() +{ + std::unique_lock lock{m_mutex}; + return !m_finishing || m_requestEntries.size(); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/include/asynchronousexplicitimagecache.h b/src/plugins/qmldesigner/designercore/include/asynchronousexplicitimagecache.h new file mode 100644 index 00000000000..9058406b78c --- /dev/null +++ b/src/plugins/qmldesigner/designercore/include/asynchronousexplicitimagecache.h @@ -0,0 +1,111 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "asynchronousimagecacheinterface.h" + +#include +#include +#include +#include +#include + +namespace QmlDesigner { + +class ImageCacheStorageInterface; + +class AsynchronousExplicitImageCache +{ +public: + ~AsynchronousExplicitImageCache(); + + AsynchronousExplicitImageCache(ImageCacheStorageInterface &storage); + + void requestImage(Utils::PathString name, + ImageCache::CaptureImageCallback captureCallback, + ImageCache::AbortCallback abortCallback, + Utils::SmallString extraId = {}); + void requestSmallImage(Utils::PathString name, + ImageCache::CaptureImageCallback captureCallback, + ImageCache::AbortCallback abortCallback, + Utils::SmallString extraId = {}); + + void clean(); + +private: + enum class RequestType { Image, SmallImage, Icon }; + struct RequestEntry + { + RequestEntry() = default; + RequestEntry(Utils::PathString name, + Utils::SmallString extraId, + ImageCache::CaptureImageCallback &&captureCallback, + ImageCache::AbortCallback &&abortCallback, + RequestType requestType) + : name{std::move(name)} + , extraId{std::move(extraId)} + , captureCallback{std::move(captureCallback)} + , abortCallback{std::move(abortCallback)} + , requestType{requestType} + {} + + Utils::PathString name; + Utils::SmallString extraId; + ImageCache::CaptureImageCallback captureCallback; + ImageCache::AbortCallback abortCallback; + RequestType requestType = RequestType::Image; + }; + + std::tuple getEntry(); + void addEntry(Utils::PathString &&name, + Utils::SmallString &&extraId, + ImageCache::CaptureImageCallback &&captureCallback, + ImageCache::AbortCallback &&abortCallback, + RequestType requestType); + void clearEntries(); + void waitForEntries(); + void stopThread(); + bool isRunning(); + static void request(Utils::SmallStringView name, + Utils::SmallStringView extraId, + AsynchronousExplicitImageCache::RequestType requestType, + ImageCache::CaptureImageCallback captureCallback, + ImageCache::AbortCallback abortCallback, + ImageCacheStorageInterface &storage); + +private: + void wait(); + +private: + std::deque m_requestEntries; + mutable std::mutex m_mutex; + std::condition_variable m_condition; + std::thread m_backgroundThread; + ImageCacheStorageInterface &m_storage; + bool m_finishing{false}; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/include/asynchronousimagecache.h b/src/plugins/qmldesigner/designercore/include/asynchronousimagecache.h index 66dd5601413..91574ee6090 100644 --- a/src/plugins/qmldesigner/designercore/include/asynchronousimagecache.h +++ b/src/plugins/qmldesigner/designercore/include/asynchronousimagecache.h @@ -43,7 +43,6 @@ class ImageCacheCollectorInterface; class AsynchronousImageCache final : public AsynchronousImageCacheInterface { public: - ~AsynchronousImageCache(); AsynchronousImageCache(ImageCacheStorageInterface &storage, @@ -62,7 +61,6 @@ public: ImageCache::AuxiliaryData auxiliaryData = {}) override; void clean(); - // void waitForFinished(); private: enum class RequestType { Image, SmallImage, Icon }; diff --git a/src/plugins/qmldesigner/qmldesignercore.cmake b/src/plugins/qmldesigner/qmldesignercore.cmake index 32c914c9b99..d31f1c4ba4a 100644 --- a/src/plugins/qmldesigner/qmldesignercore.cmake +++ b/src/plugins/qmldesigner/qmldesignercore.cmake @@ -107,6 +107,7 @@ function(extend_with_qmldesigner_core target_name) filemanager/removeuiobjectmembervisitor.cpp filemanager/removeuiobjectmembervisitor.h + imagecache/asynchronousexplicitimagecache.cpp imagecache/asynchronousimagecache.cpp imagecache/imagecachecollector.cpp imagecache/imagecachecollector.h @@ -129,6 +130,7 @@ function(extend_with_qmldesigner_core target_name) include/abstractview.h include/anchorline.h include/annotation.h + include/asynchronousexplicitimagecache.h include/asynchronousimagecache.h include/basetexteditmodifier.h include/bindingproperty.h diff --git a/tests/unit/unittest/CMakeLists.txt b/tests/unit/unittest/CMakeLists.txt index 36aa86e6312..c6cc9615c7d 100644 --- a/tests/unit/unittest/CMakeLists.txt +++ b/tests/unit/unittest/CMakeLists.txt @@ -114,6 +114,7 @@ add_qtc_test(unittest GTEST imagecachecollectormock.h mockimagecachegenerator.h mockimagecachestorage.h + asynchronousexplicitimagecache-test.cpp ) if (NOT TARGET unittest) @@ -281,6 +282,7 @@ extend_qtc_test(unittest exceptions/notimplementedexception.cpp exceptions/removebasestateexception.cpp exceptions/rewritingexception.cpp + imagecache/asynchronousexplicitimagecache.cpp imagecache/asynchronousimagecache.cpp imagecache/imagecachecollectorinterface.h imagecache/imagecachegenerator.cpp @@ -292,6 +294,7 @@ extend_qtc_test(unittest imagecache/timestampproviderinterface.h include/abstractproperty.h include/abstractview.h + include/asynchronousexplicitimagecache.h include/asynchronousimagecache.h include/asynchronousimagecacheinterface.h include/bindingproperty.h diff --git a/tests/unit/unittest/asynchronousexplicitimagecache-test.cpp b/tests/unit/unittest/asynchronousexplicitimagecache-test.cpp new file mode 100644 index 00000000000..e867c772aeb --- /dev/null +++ b/tests/unit/unittest/asynchronousexplicitimagecache-test.cpp @@ -0,0 +1,265 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "googletest.h" + +#include "mockimagecachegenerator.h" +#include "mockimagecachestorage.h" +#include "mocktimestampprovider.h" +#include "notification.h" + +#include + +namespace { + +class AsynchronousExplicitImageCache : public testing::Test +{ +protected: + Notification notification; + Notification waitInThread; + NiceMock> mockAbortCallback; + NiceMock> mockAbortCallback2; + NiceMock> mockCaptureCallback; + NiceMock> mockCaptureCallback2; + NiceMock mockStorage; + QmlDesigner::AsynchronousExplicitImageCache cache{mockStorage}; + QImage image1{10, 10, QImage::Format_ARGB32}; + QImage smallImage1{1, 1, QImage::Format_ARGB32}; +}; + +TEST_F(AsynchronousExplicitImageCache, RequestImageFetchesImageFromStorage) +{ + EXPECT_CALL(mockStorage, fetchImage(Eq("/path/to/Component.qml"), Eq(Sqlite::TimeStamp{}))) + .WillRepeatedly([&](Utils::SmallStringView, auto) { + notification.notify(); + return QmlDesigner::ImageCacheStorageInterface::ImageEntry{{}, false}; + }); + + cache.requestImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + notification.wait(); +} + +TEST_F(AsynchronousExplicitImageCache, RequestImageFetchesImageFromStorageWithTimeStamp) +{ + EXPECT_CALL(mockStorage, fetchImage(Eq("/path/to/Component.qml"), Eq(Sqlite::TimeStamp{}))) + .WillRepeatedly([&](Utils::SmallStringView, auto) { + notification.notify(); + return QmlDesigner::ImageCacheStorageInterface::ImageEntry{QImage{}, false}; + }); + + cache.requestImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + notification.wait(); +} + +TEST_F(AsynchronousExplicitImageCache, RequestImageCallsCaptureCallbackWithImageFromStorage) +{ + ON_CALL(mockStorage, fetchImage(Eq("/path/to/Component.qml"), _)) + .WillByDefault(Return(QmlDesigner::ImageCacheStorageInterface::ImageEntry{image1, true})); + + EXPECT_CALL(mockCaptureCallback, Call(Eq(image1))).WillRepeatedly([&](const QImage &) { + notification.notify(); + }); + + cache.requestImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + notification.wait(); +} + +TEST_F(AsynchronousExplicitImageCache, RequestImageCallsAbortCallbackWithoutEntry) +{ + ON_CALL(mockStorage, fetchImage(Eq("/path/to/Component.qml"), _)) + .WillByDefault(Return(QmlDesigner::ImageCacheStorageInterface::ImageEntry{image1, false})); + + EXPECT_CALL(mockAbortCallback, Call(Eq(QmlDesigner::ImageCache::AbortReason::Failed))) + .WillRepeatedly([&](auto) { notification.notify(); }); + + cache.requestImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + notification.wait(); +} + +TEST_F(AsynchronousExplicitImageCache, RequestImageCallsAbortCallbackWithoutImage) +{ + ON_CALL(mockStorage, fetchImage(Eq("/path/to/Component.qml"), _)) + .WillByDefault(Return(QmlDesigner::ImageCacheStorageInterface::ImageEntry{QImage{}, true})); + + EXPECT_CALL(mockAbortCallback, Call(Eq(QmlDesigner::ImageCache::AbortReason::Failed))) + .WillRepeatedly([&](auto) { notification.notify(); }); + + cache.requestImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + notification.wait(); +} + +TEST_F(AsynchronousExplicitImageCache, RequestSmallImageFetchesSmallImageFromStorage) +{ + EXPECT_CALL(mockStorage, fetchSmallImage(Eq("/path/to/Component.qml"), Eq(Sqlite::TimeStamp{}))) + .WillRepeatedly([&](Utils::SmallStringView, auto) { + notification.notify(); + return QmlDesigner::ImageCacheStorageInterface::ImageEntry{{}, false}; + }); + + cache.requestSmallImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + notification.wait(); +} + +TEST_F(AsynchronousExplicitImageCache, RequestSmallImageCallsCaptureCallbackWithImageFromStorage) +{ + ON_CALL(mockStorage, fetchSmallImage(Eq("/path/to/Component.qml"), _)) + .WillByDefault(Return(QmlDesigner::ImageCacheStorageInterface::ImageEntry{smallImage1, true})); + + EXPECT_CALL(mockCaptureCallback, Call(Eq(smallImage1))).WillRepeatedly([&](const QImage &) { + notification.notify(); + }); + + cache.requestSmallImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + notification.wait(); +} + +TEST_F(AsynchronousExplicitImageCache, RequestSmallImageCallsAbortCallbackWithoutEntry) +{ + ON_CALL(mockStorage, fetchSmallImage(Eq("/path/to/Component.qml"), _)) + .WillByDefault(Return(QmlDesigner::ImageCacheStorageInterface::ImageEntry{smallImage1, false})); + + EXPECT_CALL(mockAbortCallback, Call(Eq(QmlDesigner::ImageCache::AbortReason::Failed))) + .WillRepeatedly([&](auto) { notification.notify(); }); + + cache.requestSmallImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + notification.wait(); +} + +TEST_F(AsynchronousExplicitImageCache, RequestSmallImageCallsAbortCallbackWithoutSmallImage) +{ + ON_CALL(mockStorage, fetchSmallImage(Eq("/path/to/Component.qml"), _)) + .WillByDefault(Return(QmlDesigner::ImageCacheStorageInterface::ImageEntry{QImage{}, true})); + + EXPECT_CALL(mockAbortCallback, Call(Eq(QmlDesigner::ImageCache::AbortReason::Failed))) + .WillRepeatedly([&](auto) { notification.notify(); }); + + cache.requestSmallImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + notification.wait(); +} + +TEST_F(AsynchronousExplicitImageCache, CleanRemovesEntries) +{ + ON_CALL(mockStorage, fetchSmallImage(_, _)).WillByDefault([&](Utils::SmallStringView, auto) { + return QmlDesigner::ImageCacheStorageInterface::ImageEntry{smallImage1, true}; + }); + ON_CALL(mockCaptureCallback2, Call(_)).WillByDefault([&](auto) { waitInThread.wait(); }); + cache.requestSmallImage("/path/to/Component1.qml", + mockCaptureCallback2.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + + EXPECT_CALL(mockCaptureCallback, Call(_)).Times(0); + + cache.requestSmallImage("/path/to/Component3.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + cache.clean(); + waitInThread.notify(); +} + +TEST_F(AsynchronousExplicitImageCache, CleanCallsAbort) +{ + ON_CALL(mockStorage, fetchSmallImage(Eq("/path/to/Component1.qml"), _)) + .WillByDefault([&](Utils::SmallStringView, auto) { + waitInThread.wait(); + return QmlDesigner::ImageCacheStorageInterface::ImageEntry{smallImage1, true}; + }); + cache.requestSmallImage("/path/to/Component1.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback2.AsStdFunction()); + cache.requestSmallImage("/path/to/Component2.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + + EXPECT_CALL(mockAbortCallback, Call(Eq(QmlDesigner::ImageCache::AbortReason::Abort))); + + cache.clean(); + waitInThread.notify(); +} + +TEST_F(AsynchronousExplicitImageCache, AfterCleanNewJobsWorks) +{ + cache.clean(); + + ON_CALL(mockStorage, fetchSmallImage(Eq("/path/to/Component.qml"), Eq(Sqlite::TimeStamp{}))) + .WillByDefault([&](Utils::SmallStringView, auto) { + return QmlDesigner::ImageCacheStorageInterface::ImageEntry{smallImage1, true}; + }); + ON_CALL(mockCaptureCallback, Call(_)).WillByDefault([&](auto) { notification.notify(); }); + + cache.requestSmallImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction()); + notification.wait(); +} + +TEST_F(AsynchronousExplicitImageCache, RequestImageWithExtraIdFetchesImageFromStorage) +{ + EXPECT_CALL(mockStorage, fetchImage(Eq("/path/to/Component.qml+extraId1"), _)) + .WillRepeatedly([&](Utils::SmallStringView, auto) { + notification.notify(); + return QmlDesigner::ImageCacheStorageInterface::ImageEntry{{}, false}; + }); + + cache.requestImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction(), + "extraId1"); + notification.wait(); +} + +TEST_F(AsynchronousExplicitImageCache, RequestSmallImageWithExtraIdFetchesImageFromStorage) +{ + EXPECT_CALL(mockStorage, fetchSmallImage(Eq("/path/to/Component.qml+extraId1"), _)) + .WillRepeatedly([&](Utils::SmallStringView, auto) { + notification.notify(); + return QmlDesigner::ImageCacheStorageInterface::ImageEntry{{}, false}; + }); + + cache.requestSmallImage("/path/to/Component.qml", + mockCaptureCallback.AsStdFunction(), + mockAbortCallback.AsStdFunction(), + "extraId1"); + notification.wait(); +} + +} // namespace