Skip to content

Commit 0b2de85

Browse files
committedOct 31, 2017
Check file modified time when retrieving svg images from cache
If file has been modified since the cache, regenerate a new cache image. We don't want to check the file modified time too often though, (e.g., we don't want to check for every point render in a 100k point file), so use a hardcoded 30 second minimum time between consecutive file modified checks. This means that file modifications occuring more often than every 30 seconds won't be picked up till 30 seconds has elapsed since the last modification. But at the same time it means that if the render takes < 30 seconds we'll only check each svg at most once (and if a render takes > 30 seconds, adding a few more milliseconds won't hurt!). Fixes #13565
1 parent b07f675 commit 0b2de85

File tree

7 files changed

+211
-6
lines changed

7 files changed

+211
-6
lines changed
 

‎src/core/symbology/qgssvgcache.cpp

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,26 @@
4040

4141
QgsSvgCacheEntry::QgsSvgCacheEntry( const QString &p, double s, double ow, double wsf, const QColor &fi, const QColor &ou, double far )
4242
: path( p )
43+
, fileModified( QFileInfo( p ).lastModified() )
4344
, size( s )
4445
, strokeWidth( ow )
4546
, widthScaleFactor( wsf )
4647
, fixedAspectRatio( far )
4748
, fill( fi )
4849
, stroke( ou )
4950
{
51+
fileModifiedLastCheckTimer.start();
5052
}
5153

5254
bool QgsSvgCacheEntry::operator==( const QgsSvgCacheEntry &other ) const
5355
{
54-
return other.path == path && qgsDoubleNear( other.size, size ) && qgsDoubleNear( other.strokeWidth, strokeWidth ) && qgsDoubleNear( other.widthScaleFactor, widthScaleFactor )
55-
&& other.fill == fill && other.stroke == stroke;
56+
bool equal = other.path == path && qgsDoubleNear( other.size, size ) && qgsDoubleNear( other.strokeWidth, strokeWidth ) && qgsDoubleNear( other.widthScaleFactor, widthScaleFactor )
57+
&& other.fixedAspectRatio == fixedAspectRatio && other.fill == fill && other.stroke == stroke;
58+
59+
if ( equal && ( mFileModifiedCheckTimeout <= 0 || fileModifiedLastCheckTimer.hasExpired( mFileModifiedCheckTimeout ) ) )
60+
equal = other.fileModified == fileModified;
61+
62+
return equal;
5663
}
5764

5865
int QgsSvgCacheEntry::dataSize() const
@@ -186,6 +193,7 @@ QgsSvgCacheEntry *QgsSvgCache::insertSvg( const QString &path, double size, cons
186193
double widthScaleFactor, double fixedAspectRatio )
187194
{
188195
QgsSvgCacheEntry *entry = new QgsSvgCacheEntry( path, size, strokeWidth, widthScaleFactor, fill, stroke, fixedAspectRatio );
196+
entry->mFileModifiedCheckTimeout = mFileModifiedCheckTimeout;
189197

190198
replaceParamsAndCacheSvg( entry );
191199

@@ -552,7 +560,7 @@ QgsSvgCacheEntry *QgsSvgCache::cacheEntry( const QString &path, double size, con
552560
//search entries in mEntryLookup
553561
QgsSvgCacheEntry *currentEntry = nullptr;
554562
QList<QgsSvgCacheEntry *> entries = mEntryLookup.values( path );
555-
563+
QDateTime modified;
556564
QList<QgsSvgCacheEntry *>::iterator entryIt = entries.begin();
557565
for ( ; entryIt != entries.end(); ++entryIt )
558566
{
@@ -561,6 +569,14 @@ QgsSvgCacheEntry *QgsSvgCache::cacheEntry( const QString &path, double size, con
561569
qgsDoubleNear( cacheEntry->strokeWidth, strokeWidth ) && qgsDoubleNear( cacheEntry->widthScaleFactor, widthScaleFactor ) &&
562570
qgsDoubleNear( cacheEntry->fixedAspectRatio, fixedAspectRatio ) )
563571
{
572+
if ( mFileModifiedCheckTimeout <= 0 || cacheEntry->fileModifiedLastCheckTimer.hasExpired( mFileModifiedCheckTimeout ) )
573+
{
574+
if ( !modified.isValid() )
575+
modified = QFileInfo( path ).lastModified();
576+
577+
if ( cacheEntry->fileModified != modified )
578+
continue;
579+
}
564580
currentEntry = cacheEntry;
565581
break;
566582
}

‎src/core/symbology/qgssvgcache.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
#include <QUrl>
2828
#include <QObject>
2929
#include <QSizeF>
30+
#include <QDateTime>
31+
#include <QElapsedTimer>
3032

3133
#include "qgis_core.h"
3234

@@ -46,7 +48,7 @@ class CORE_EXPORT QgsSvgCacheEntry
4648
{
4749
public:
4850

49-
QgsSvgCacheEntry() = default;
51+
QgsSvgCacheEntry() = delete;
5052

5153
/**
5254
* Constructor.
@@ -68,6 +70,13 @@ class CORE_EXPORT QgsSvgCacheEntry
6870

6971
//! Absolute path to SVG file
7072
QString path;
73+
74+
//! Timestamp when file was last modified
75+
QDateTime fileModified;
76+
//! Time since last check of file modified date
77+
QElapsedTimer fileModifiedLastCheckTimer;
78+
int mFileModifiedCheckTimeout = 30000;
79+
7180
double size = 0.0; //size in pixels (cast to int for QImage)
7281
double strokeWidth = 0;
7382
double widthScaleFactor = 1.0;
@@ -248,6 +257,9 @@ class CORE_EXPORT QgsSvgCache : public QObject
248257
//Removes entry from the ordered list (but does not delete the entry itself)
249258
void takeEntryFromList( QgsSvgCacheEntry *entry );
250259

260+
//! Minimum time (in ms) between consecutive svg file modified time checks
261+
int mFileModifiedCheckTimeout = 30000;
262+
251263
//! Entry pointers accessible by file name
252264
QMultiHash< QString, QgsSvgCacheEntry * > mEntryLookup;
253265
//! Estimated total size of all images, pictures and svgContent
@@ -297,6 +309,7 @@ class CORE_EXPORT QgsSvgCache : public QObject
297309
//! Mutex to prevent concurrent access to the class from multiple threads at once (may corrupt the entries otherwise).
298310
QMutex mMutex;
299311

312+
friend class TestQgsSvgCache;
300313
};
301314

302315
#endif // QGSSVGCACHE_H

‎tests/src/core/testqgssvgcache.cpp

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
#include <QPicture>
2424
#include <QPainter>
2525
#include <QtConcurrent>
26+
#include <QElapsedTimer>
2627
#include "qgssvgcache.h"
28+
#include "qgsmultirenderchecker.h"
29+
#include "qgsapplication.h"
2730

2831
/**
2932
* \ingroup UnitTests
@@ -33,8 +36,11 @@ class TestQgsSvgCache : public QObject
3336
{
3437
Q_OBJECT
3538

36-
public:
37-
TestQgsSvgCache() = default;
39+
private:
40+
41+
QString mReport;
42+
43+
bool imageCheck( const QString &testName, QImage &image, int mismatchCount );
3844

3945
private slots:
4046
void initTestCase();// will be called before the first testfunction is executed.
@@ -44,6 +50,7 @@ class TestQgsSvgCache : public QObject
4450
void fillCache();
4551
void threadSafePicture();
4652
void threadSafeImage();
53+
void changeImage(); //check that cache is updated if svg source file changes
4754

4855
};
4956

@@ -52,10 +59,22 @@ void TestQgsSvgCache::initTestCase()
5259
{
5360
QgsApplication::init();
5461
QgsApplication::initQgis();
62+
mReport += "<h1>QgsSvgCache Tests</h1>\n";
5563
}
5664

5765
void TestQgsSvgCache::cleanupTestCase()
5866
{
67+
QgsApplication::exitQgis();
68+
69+
QString myReportFile = QDir::tempPath() + "/qgistest.html";
70+
QFile myFile( myReportFile );
71+
if ( myFile.open( QIODevice::WriteOnly | QIODevice::Append ) )
72+
{
73+
QTextStream myQTextStream( &myFile );
74+
myQTextStream << mReport;
75+
myFile.close();
76+
//QDesktopServices::openUrl( "file:///" + myReportFile );
77+
}
5978
}
6079

6180
void TestQgsSvgCache::fillCache()
@@ -155,5 +174,86 @@ void TestQgsSvgCache::threadSafeImage()
155174
QtConcurrent::blockingMap( list, RenderImageWrapper( cache, svgPath ) );
156175
}
157176

177+
void TestQgsSvgCache::changeImage()
178+
{
179+
bool inCache;
180+
QgsSvgCache cache;
181+
// no minimum time between checks
182+
cache.mFileModifiedCheckTimeout = 0;
183+
184+
//copy an image to the temp folder
185+
QString tempImagePath = QDir::tempPath() + "/svg_cache.svg";
186+
187+
QString originalImage = TEST_DATA_DIR + QStringLiteral( "/test_symbol_svg.svg" );
188+
if ( QFileInfo::exists( tempImagePath ) )
189+
QFile::remove( tempImagePath );
190+
QFile::copy( originalImage, tempImagePath );
191+
192+
//render it through the cache
193+
QImage img = cache.svgAsImage( tempImagePath, 200, QColor( 0, 0, 0 ), QColor( 0, 0, 0 ), 1.0,
194+
1.0, inCache );
195+
QVERIFY( imageCheck( "svgcache_changed_before", img, 30 ) );
196+
197+
// wait a second so that modified time is different
198+
QElapsedTimer t;
199+
t.start();
200+
while ( !t.hasExpired( 1000 ) )
201+
{}
202+
203+
//replace the image in the temp folder
204+
QString newImage = TEST_DATA_DIR + QStringLiteral( "/test_symbol_svg2.svg" );
205+
QFile::remove( tempImagePath );
206+
QFile::copy( newImage, tempImagePath );
207+
208+
//re-render it
209+
img = cache.svgAsImage( tempImagePath, 200, QColor( 0, 0, 0 ), QColor( 0, 0, 0 ), 1.0,
210+
1.0, inCache );
211+
QVERIFY( imageCheck( "svgcache_changed_after", img, 30 ) );
212+
213+
// repeat, with minimum time between checks
214+
QgsSvgCache cache2;
215+
QFile::remove( tempImagePath );
216+
QFile::copy( originalImage, tempImagePath );
217+
img = cache2.svgAsImage( tempImagePath, 200, QColor( 0, 0, 0 ), QColor( 0, 0, 0 ), 1.0,
218+
1.0, inCache );
219+
QVERIFY( imageCheck( "svgcache_changed_before", img, 30 ) );
220+
221+
// wait a second so that modified time is different
222+
t.restart();
223+
while ( !t.hasExpired( 1000 ) )
224+
{}
225+
226+
//replace the image in the temp folder
227+
QFile::remove( tempImagePath );
228+
QFile::copy( newImage, tempImagePath );
229+
230+
//re-render it - not enough time has elapsed between checks, so file modification time will NOT be rechecked and
231+
// existing cached image should be used
232+
img = cache2.svgAsImage( tempImagePath, 200, QColor( 0, 0, 0 ), QColor( 0, 0, 0 ), 1.0,
233+
1.0, inCache );
234+
QVERIFY( imageCheck( "svgcache_changed_before", img, 30 ) );
235+
}
236+
237+
bool TestQgsSvgCache::imageCheck( const QString &testName, QImage &image, int mismatchCount )
238+
{
239+
//draw background
240+
QImage imageWithBackground( image.width(), image.height(), QImage::Format_RGB32 );
241+
QgsRenderChecker::drawBackground( &imageWithBackground );
242+
QPainter painter( &imageWithBackground );
243+
painter.drawImage( 0, 0, image );
244+
painter.end();
245+
246+
mReport += "<h2>" + testName + "</h2>\n";
247+
QString tempDir = QDir::tempPath() + '/';
248+
QString fileName = tempDir + testName + ".png";
249+
imageWithBackground.save( fileName, "PNG" );
250+
QgsRenderChecker checker;
251+
checker.setControlName( "expected_" + testName );
252+
checker.setRenderedImage( fileName );
253+
checker.setColorTolerance( 2 );
254+
bool resultFlag = checker.compareImages( testName, mismatchCount );
255+
mReport += checker.report();
256+
return resultFlag;
257+
}
158258
QGSTEST_MAIN( TestQgsSvgCache )
159259
#include "testqgssvgcache.moc"

‎tests/testdata/test_symbol_svg2.svg

Lines changed: 76 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)
Please sign in to comment.