MythTV master
mythuithemecache.cpp
Go to the documentation of this file.
1// Qt
2#include <QDir>
3#include <QDateTime>
4
5// MythTV
13
14#include "mythuithemecache.h"
15
16// Std
17#include <unistd.h>
18#include <sys/stat.h>
19
20// Temp for DEFAULT_UI_THEME, ImageCacheMode etc
21#include "mythuihelper.h"
22
23#define LOC QString("UICache: ")
24
26 : m_imageThreadPool(new MThreadPool("MythUIHelper"))
27{
28 m_maxCacheSize.fetchAndStoreRelease(GetMythDB()->GetNumSetting("UIImageCacheSize", 30) * 1024LL * 1024);
29 LOG(VB_GUI, LOG_INFO, LOC + QString("MythUI Image Cache size set to %1 bytes")
30 .arg(m_maxCacheSize.fetchAndAddRelease(0)));
31}
32
34{
37
38 QMutableMapIterator<QString, MythImage *> i(m_imageCache);
39 while (i.hasNext())
40 {
41 i.next();
42 i.value()->SetIsInCache(false);
43 i.value()->DecrRef();
44 i.remove();
45 }
46 m_cacheTrack.clear();
47
48 delete m_imageThreadPool;
49}
50
52{
53 m_cacheScreenSize = Size;
54}
55
57{
58 m_themecachedir.clear();
59}
60
62{
63 QMutexLocker locker(&m_cacheLock);
64
65 QMutableMapIterator<QString, MythImage *> i(m_imageCache);
66
67 while (i.hasNext())
68 {
69 i.next();
70 i.value()->SetIsInCache(false);
71 i.value()->DecrRef();
72 i.remove();
73 }
74
75 m_cacheTrack.clear();
76 m_cacheSize.fetchAndStoreOrdered(0);
77
81}
82
84{
86
87 QString themecachedir = m_themecachedir;
88
89 m_themecachedir += '/';
90
91 QDir dir(GetThemeBaseCacheDir());
92 dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
93 QFileInfoList list = dir.entryInfoList();
94
95 QMap<QDateTime, QString> dirtimes;
96
97 for (const auto & fi : std::as_const(list))
98 {
99 if (fi.isDir() && !fi.isSymLink())
100 {
101 if (fi.absoluteFilePath() == themecachedir)
102 continue;
103 dirtimes[fi.lastModified()] = fi.absoluteFilePath();
104 }
105 }
106
107 // Cache two themes/resolutions to allow sampling other themes without
108 // incurring a penalty. Especially for those writing new themes or testing
109 // changes of an existing theme. The space used is neglible when compared
110 // against the average video
111 while (static_cast<size_t>(dirtimes.size()) >= 2)
112 {
113 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC + QString("Removing cache dir: %1")
114 .arg(dirtimes.begin().value()));
115
116 RemoveCacheDir(dirtimes.begin().value());
117 dirtimes.erase(dirtimes.begin());
118 }
119
120 for (const auto & dirtime : std::as_const(dirtimes))
121 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC + QString("Keeping cache dir: %1").arg(dirtime));
122}
123
124void MythUIThemeCache::RemoveCacheDir(const QString& Dir)
125{
126 QString cachedirname = GetThemeBaseCacheDir();
127
128 if (!Dir.startsWith(cachedirname))
129 return;
130
131 LOG(VB_GENERAL, LOG_ERR, LOC + QString("Removing stale cache dir: %1").arg(Dir));
132
133 QDir dir(Dir);
134 if (!dir.exists())
135 return;
136
137 dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
138 QFileInfoList list = dir.entryInfoList();
139 for (const auto & fi : std::as_const(list))
140 {
141 if (fi.isFile() && !fi.isSymLink())
142 {
143 QFile file(fi.absoluteFilePath());
144 file.remove();
145 }
146 else if (fi.isDir() && !fi.isSymLink())
147 {
148 RemoveCacheDir(fi.absoluteFilePath());
149 }
150 }
151
152 dir.rmdir(Dir);
153}
154
161void MythUIThemeCache::PruneCacheDir(const QString& dirname)
162{
163 int days = GetMythDB()->GetNumSetting("UIDiskCacheDays", 7);
164 if (days == -1)
165 {
166 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Pruning cache directory: %1 is disabled")
167 .arg(dirname));
168 return;
169 }
170
171 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Pruning cache directory: %1").arg(dirname));
172 QDateTime cutoff = MythDate::current().addDays(-days);
173 qint64 cutoffsecs = cutoff.toSecsSinceEpoch();
174
175 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC + QString("Removing files not accessed since %1")
176 .arg(cutoff.toLocalTime().toString(Qt::ISODate)));
177
178 // Trying to save every cycle possible within this loop. The
179 // stat() call seems significantly faster than the fi.fileRead()
180 // method. The documentation for QFileInfo says that the
181 // fi.absoluteFilePath() method has to query the file system, so
182 // use fi.filePath() method here and then add the directory if
183 // needed. Using dir.entryList() and adding the dirname each time
184 // is also slower just using dir.entryInfoList().
185 QDir dir(dirname);
186 dir.setFilter(QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot);
187 dir.setSorting(QDir::NoSort);
188 QFileInfoList entries = dir.entryInfoList();
189 int kept = 0;
190 int deleted = 0;
191 int errors = 0;
192 for (const QFileInfo & fi : std::as_const(entries))
193 {
194 struct stat buf {};
195 QString fullname = fi.filePath();
196 if (not fullname.startsWith('/'))
197 fullname = dirname + "/" + fullname;
198 int rc = stat(fullname.toLocal8Bit(), &buf);
199 if (rc >= 0)
200 {
201 if (buf.st_atime < cutoffsecs)
202 {
203 deleted += 1;
204 LOG(VB_GUI | VB_FILE, LOG_DEBUG, LOC + QString("%1 Delete %2")
205 .arg(fi.lastRead().toLocalTime().toString(Qt::ISODate), fi.fileName()));
206 unlink(qPrintable(fullname));
207 }
208 else
209 {
210 kept += 1;
211 LOG(VB_GUI | VB_FILE, LOG_DEBUG, LOC + QString("%1 Keep %2")
212 .arg(fi.lastRead().toLocalTime().toString(Qt::ISODate), fi.fileName()));
213 }
214 }
215 else
216 {
217 errors += 1;
218 }
219 }
220
221 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Kept %1 files, deleted %2 files, stat error on %3 files")
222 .arg(kept).arg(deleted).arg(errors));
223}
224
226{
227 static QString s_oldcachedir;
228 QString tmpcachedir = GetThemeBaseCacheDir() + "/" +
229 GetMythDB()->GetSetting("Theme", DEFAULT_UI_THEME) + "." +
230 QString::number(m_cacheScreenSize.width()) + "." +
231 QString::number(m_cacheScreenSize.height());
232
233 if (tmpcachedir != s_oldcachedir)
234 {
235 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC + QString("Creating cache dir: %1").arg(tmpcachedir));
236 QDir dir;
237 dir.mkdir(tmpcachedir);
238 s_oldcachedir = tmpcachedir;
239 }
240 return tmpcachedir;
241}
242
250QString MythUIThemeCache::GetCacheDirByUrl(const QString& URL)
251{
252 if (URL.startsWith("myth:") || URL.startsWith("-"))
253 return GetThumbnailDir();
254 return GetThemeCacheDir();
255}
256
257MythImage* MythUIThemeCache::LoadCacheImage(QString File, const QString& Label,
258 MythPainter *Painter,
259 ImageCacheMode cacheMode)
260{
261 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC +
262 QString("LoadCacheImage(%1,%2)").arg(File, Label));
263
264 if (File.isEmpty() || Label.isEmpty())
265 return nullptr;
266
267 if (!(kCacheForceStat & cacheMode))
268 {
269 // Some screens include certain images dozens or even hundreds of
270 // times. Even if the image is in the cache, there is still a
271 // stat system call on the original file to see if it has changed.
272 // This code relaxes the original-file check so that the check
273 // isn't repeated if it was already done within kImageCacheTimeout
274 // seconds.
275
276 // This only applies to the MEMORY cache
277 constexpr std::chrono::seconds kImageCacheTimeout { 60s };
278 SystemTime now = SystemClock::now();
279
280 QMutexLocker locker(&m_cacheLock);
281
282 if (m_imageCache.contains(Label) &&
283 m_cacheTrack[Label] + kImageCacheTimeout > now)
284 {
285 m_imageCache[Label]->IncrRef();
286 return m_imageCache[Label];
287 }
288 }
289
290 MythImage *ret = nullptr;
291
292 // Check Memory Cache
293 ret = GetImageFromCache(Label);
294
295 // If the image is in the memory or we are not ignoring the disk cache
296 // then proceed to check whether the source file is newer than our cached
297 // copy
298 if (ret || !(cacheMode & kCacheIgnoreDisk))
299 {
300 // Create url to image in disk cache
301 QString cachefilepath;
302 cachefilepath = GetCacheDirByUrl(Label) + '/' + Label;
303 QFileInfo cacheFileInfo(cachefilepath);
304
305 // If the file isn't in the disk cache, then we don't want to bother
306 // checking the last modified times of the original
307 if (!cacheFileInfo.exists())
308 return nullptr;
309
310 // Now compare the time on the source versus our cached copy
311 QDateTime srcLastModified;
312
313 // For internet images this involves querying the headers of the remote
314 // image. This is slow even without redownloading the whole image
315 if ((File.startsWith("http://")) ||
316 (File.startsWith("https://")) ||
317 (File.startsWith("ftp://")))
318 {
319 // If the image is in the memory cache then skip the last modified
320 // check, since memory cached images are loaded in the foreground
321 // this can cause an intolerable delay. The images won't stay in
322 // the cache forever and so eventually they will be checked.
323 if (ret)
324 srcLastModified = cacheFileInfo.lastModified();
325 else
326 srcLastModified = GetMythDownloadManager()->GetLastModified(File);
327 }
328 else if (File.startsWith("myth://"))
329 {
330 srcLastModified = RemoteFile::LastModified(File);
331 }
332 else
333 {
334 if (!GetMythUI()->FindThemeFile(File))
335 return nullptr;
336
337 QFileInfo original(File);
338
339 if (original.exists())
340 srcLastModified = original.lastModified();
341 }
342
343 // Now compare the timestamps, if the cached image is newer than the
344 // source image we can use it, otherwise we want to remove it from the
345 // cache
346 if (cacheFileInfo.lastModified() >= srcLastModified)
347 {
348 // If we haven't already loaded the image from the memory cache
349 // and we're not ignoring the disk cache, then it's time to load
350 // it from there instead
351 if (!ret && (cacheMode == kCacheNormal))
352 {
353
354 if (Painter)
355 {
356 ret = Painter->GetFormatImage();
357
358 // Load file from disk cache to memory cache
359 if (ret->Load(cachefilepath))
360 {
361 // Add to ram cache, and skip saving to disk since that is
362 // where we found this in the first place.
363 CacheImage(Label, ret, true);
364 }
365 else
366 {
367 LOG(VB_GUI | VB_FILE, LOG_WARNING, LOC +
368 QString("LoadCacheImage: Could not load: %1")
369 .arg(cachefilepath));
370
371 ret->SetIsInCache(false);
372 ret->DecrRef();
373 ret = nullptr;
374 }
375 }
376 }
377 }
378 else
379 {
380 ret = nullptr;
381 // If file has changed on disk, then remove it from the memory
382 // and disk cache
384 }
385 }
386
387 return ret;
388}
389
391{
392 QMutexLocker locker(&m_cacheLock);
393
394 if (m_imageCache.contains(URL))
395 {
396 m_cacheTrack[URL] = SystemClock::now();
397 m_imageCache[URL]->IncrRef();
398 return m_imageCache[URL];
399 }
400
401 /*
402 if (QFileInfo(URL).exists())
403 {
404 MythImage *im = GetMythPainter()->GetFormatImage();
405 im->Load(URL,false);
406 return im;
407 }
408 */
409
410 return nullptr;
411}
412
413MythImage *MythUIThemeCache::CacheImage(const QString& URL, MythImage* Image, bool NoDisk)
414{
415 if (!Image)
416 return nullptr;
417
418 if (!NoDisk)
419 {
420 QString dstfile = GetCacheDirByUrl(URL) + '/' + URL;
421 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC + QString("Saved to Cache (%1)").arg(dstfile));
422 // Save to disk cache
423 Image->save(dstfile, "PNG");
424 }
425
426 // delete the oldest cached images until we fall below threshold.
427 QMutexLocker locker(&m_cacheLock);
428
429 while ((m_cacheSize.fetchAndAddOrdered(0) + Image->sizeInBytes()) >=
430 m_maxCacheSize.fetchAndAddOrdered(0) && !m_imageCache.empty())
431 {
432 QMap<QString, MythImage *>::iterator it = m_imageCache.begin();
433 auto oldestTime = SystemClock::now();
434 QString oldestKey = it.key();
435
436 int count = 0;
437
438 for (; it != m_imageCache.end(); ++it)
439 {
440 if (m_cacheTrack[it.key()] < oldestTime)
441 {
442 if ((2 == it.value()->IncrRef()) && (it.value() != Image))
443 {
444 oldestTime = m_cacheTrack[it.key()];
445 oldestKey = it.key();
446 count++;
447 }
448 it.value()->DecrRef();
449 }
450 }
451
452 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC +QString("%1 images are eligible for expiry").arg(count));
453 if (count > 0)
454 {
455 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC + QString("Cache too big (%1), removing :%2:")
456 .arg(m_cacheSize.fetchAndAddOrdered(0) + Image->sizeInBytes())
457 .arg(oldestKey));
458
459 m_imageCache[oldestKey]->SetIsInCache(false);
460 m_imageCache[oldestKey]->DecrRef();
461 m_imageCache.remove(oldestKey);
462 m_cacheTrack.remove(oldestKey);
463 }
464 else
465 {
466 break;
467 }
468 }
469
470 QMap<QString, MythImage *>::iterator it = m_imageCache.find(URL);
471
472 if (it == m_imageCache.end())
473 {
474 Image->IncrRef();
475 m_imageCache[URL] = Image;
476 m_cacheTrack[URL] = SystemClock::now();
477
478 Image->SetIsInCache(true);
479 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC +
480 QString("NOT IN RAM CACHE, Adding, and adding to size :%1: :%2:").arg(URL)
481 .arg(Image->sizeInBytes()));
482 }
483
484 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC + QString("MythUIHelper::CacheImage : Cache Count = :%1: size :%2:")
485 .arg(m_imageCache.count()).arg(m_cacheSize.fetchAndAddRelaxed(0)));
486
487 return m_imageCache[URL];
488}
489
491{
492 QMutexLocker locker(&m_cacheLock);
493 QMap<QString, MythImage *>::iterator it = m_imageCache.find(URL);
494
495 if (it != m_imageCache.end())
496 {
497 m_imageCache[URL]->SetIsInCache(false);
498 m_imageCache[URL]->DecrRef();
499 m_imageCache.remove(URL);
500 m_cacheTrack.remove(URL);
501 }
502
503 QString dstfile = GetCacheDirByUrl(URL) + '/' + URL;
504 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC + QString("RemoveFromCacheByURL removed :%1: from cache").arg(dstfile));
505 QFile::remove(dstfile);
506}
507
509{
510 QList<QString>::iterator it;
511
512 QString partialKey = File;
513 partialKey.replace('/', '-');
514
515 m_cacheLock.lock();
516 QList<QString> m_imageCacheKeys = m_imageCache.keys();
517 m_cacheLock.unlock();
518
519 for (it = m_imageCacheKeys.begin(); it != m_imageCacheKeys.end(); ++it)
520 {
521 if ((*it).contains(partialKey))
523 }
524
525 // Loop through files to cache any that were not caught by
526 // RemoveFromCacheByURL
527 QDir dir(GetThemeCacheDir());
528 QFileInfoList list = dir.entryInfoList();
529
530 for (const auto & fileInfo : std::as_const(list))
531 {
532 if (fileInfo.fileName().contains(partialKey))
533 {
534 LOG(VB_GUI | VB_FILE, LOG_INFO, LOC +
535 QString("RemoveFromCacheByFile removed: %1: from cache")
536 .arg(fileInfo.fileName()));
537
538 if (!dir.remove(fileInfo.fileName()))
539 {
540 LOG(VB_GENERAL, LOG_ERR, LOC +
541 QString("Failed to delete %1 from the theme cache")
542 .arg(fileInfo.fileName()));
543 }
544 }
545 }
546}
547
548bool MythUIThemeCache::IsImageInCache(const QString& URL)
549{
550 QMutexLocker locker(&m_cacheLock);
551 if (m_imageCache.contains(URL))
552 return true;
553 if (QFileInfo::exists(URL))
554 return true;
555 return false;
556}
557
559{
560 if (Image)
561 m_cacheSize.fetchAndAddOrdered(Image->sizeInBytes());
562}
563
565{
566 if (Image)
567 m_cacheSize.fetchAndAddOrdered(-Image->sizeInBytes());
568}
569
571{
572 return m_imageThreadPool;
573}
574
QDateTime GetLastModified(const QString &url)
Gets the Last Modified timestamp for a URI.
bool Load(MythImageReader *reader)
Definition: mythimage.cpp:300
void SetIsInCache(bool bCached)
Definition: mythimage.cpp:70
int DecrRef(void) override
Decrements reference count and deletes on 0.
Definition: mythimage.cpp:52
int IncrRef(void) override
Increments reference count.
Definition: mythimage.cpp:44
MythImage * GetFormatImage()
Returns a blank reference counted image in the format required for the Draw functions for this painte...
MThreadPool * GetImageThreadPool()
MThreadPool * m_imageThreadPool
void SetScreenSize(QSize Size)
void RemoveFromCacheByURL(const QString &URL)
QMap< QString, SystemTime > m_cacheTrack
MythImage * CacheImage(const QString &URL, MythImage *Image, bool NoDisk=false)
MythImage * LoadCacheImage(QString File, const QString &Label, MythPainter *Painter, ImageCacheMode cacheMode=kCacheNormal)
QMap< QString, MythImage * > m_imageCache
void RemoveCacheDir(const QString &Dir)
bool IsImageInCache(const QString &URL)
void RemoveFromCacheByFile(const QString &File)
QRecursiveMutex m_cacheLock
QAtomicInteger< qint64 > m_cacheSize
void ExcludeFromCacheSize(MythImage *Image)
MythImage * GetImageFromCache(const QString &URL)
void IncludeInCacheSize(MythImage *Image)
static void PruneCacheDir(const QString &Dir)
Remove all files in the cache that haven't been accessed in a user configurable number of days.
QAtomicInteger< qint64 > m_maxCacheSize
QString GetCacheDirByUrl(const QString &URL)
Look at the url being read and decide whether the cached version should go into the theme cache or th...
QDateTime LastModified(void) const
std::chrono::time_point< SystemClock > SystemTime
Definition: mythchrono.h:67
MythDB * GetMythDB(void)
Definition: mythdb.cpp:51
QString GetRemoteCacheDir(void)
Returns the directory for all files cached from the backend.
Definition: mythdirs.cpp:280
QString GetThumbnailDir(void)
Returns the directory where all non-theme thumbnail files should be cached.
Definition: mythdirs.cpp:288
QString GetThemeBaseCacheDir(void)
Returns the base directory where all theme related files should be cached.
Definition: mythdirs.cpp:296
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MythUIHelper * GetMythUI()
#define LOC
ImageCacheMode
@ kCacheNormal
@ kCacheForceStat
@ kCacheIgnoreDisk
static constexpr const char * DEFAULT_UI_THEME
@ ISODate
Default UTC.
Definition: mythdate.h:17
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
duration< CHRONO_TYPE, ratio< 86400 > > days
Definition: mythchrono.h:25
bool exists(str path)
Definition: xbmcvfs.py:51