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