iconCache.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. // This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
  2. //
  3. // This program is free software; you can redistribute it and/or
  4. // modify it under the terms of the GNU General Public License
  5. // as published by the Free Software Foundation; either version 2
  6. // of the License, or (at your option) any later version.
  7. //
  8. // This program is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with this program; if not, write to the Free Software
  15. // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. import GLib from 'gi://GLib';
  17. import Gio from 'gi://Gio';
  18. import * as PromiseUtils from './promiseUtils.js';
  19. import * as Util from './util.js';
  20. // The icon cache caches icon objects in case they're reused shortly aftwerwards.
  21. // This is necessary for some indicators like skype which rapidly switch between serveral icons.
  22. // Without caching, the garbage collection would never be able to handle the amount of new icon data.
  23. // If the lifetime of an icon is over, the cache will destroy the icon. (!)
  24. // The presence of active icons will extend the lifetime.
  25. const GC_INTERVAL = 100; // seconds
  26. const LIFETIME_TIMESPAN = 120; // seconds
  27. // how to use: see IconCache.add, IconCache.get
  28. export class IconCache {
  29. constructor() {
  30. this._cache = new Map();
  31. this._activeIcons = Object.create(null);
  32. this._lifetime = new Map(); // we don't want to attach lifetime to the object
  33. }
  34. add(id, icon) {
  35. if (!(icon instanceof Gio.Icon)) {
  36. Util.Logger.critical('IconCache: Only Gio.Icons are supported');
  37. return null;
  38. }
  39. if (!id) {
  40. Util.Logger.critical('IconCache: Invalid ID provided');
  41. return null;
  42. }
  43. const oldIcon = this._cache.get(id);
  44. if (!oldIcon || !oldIcon.equals(icon)) {
  45. Util.Logger.debug(`IconCache: adding ${id}: ${icon}`);
  46. this._cache.set(id, icon);
  47. } else {
  48. icon = oldIcon;
  49. }
  50. this._renewLifetime(id);
  51. this._checkGC();
  52. return icon;
  53. }
  54. updateActive(iconType, gicon, isActive) {
  55. if (!gicon)
  56. return;
  57. const previousActive = this._activeIcons[iconType];
  58. if (isActive && [...this._cache.values()].some(icon => icon === gicon))
  59. this._activeIcons[iconType] = gicon;
  60. else if (previousActive === gicon)
  61. delete this._activeIcons[iconType];
  62. else
  63. return;
  64. if (previousActive) {
  65. this._cache.forEach((icon, id) => {
  66. if (icon === previousActive)
  67. this._renewLifetime(id);
  68. });
  69. }
  70. }
  71. _remove(id) {
  72. Util.Logger.debug(`IconCache: removing ${id}`);
  73. this._cache.delete(id);
  74. this._lifetime.delete(id);
  75. }
  76. _renewLifetime(id) {
  77. this._lifetime.set(id, new Date().getTime() + LIFETIME_TIMESPAN * 1000);
  78. }
  79. forceDestroy(id) {
  80. const gicon = this._cache.has(id);
  81. if (gicon) {
  82. Object.keys(this._activeIcons).forEach(iconType =>
  83. this.updateActive(iconType, gicon, false));
  84. this._remove(id);
  85. this._checkGC();
  86. }
  87. }
  88. // marks all the icons as removable, if something doesn't claim them before
  89. weakClear() {
  90. this._activeIcons = Object.create(null);
  91. this._checkGC();
  92. }
  93. // removes everything from the cache
  94. clear() {
  95. this._activeIcons = Object.create(null);
  96. this._cache.forEach((_icon, id) => this._remove(id));
  97. this._checkGC();
  98. }
  99. // returns an object from the cache, or null if it can't be found.
  100. get(id) {
  101. const icon = this._cache.get(id);
  102. if (icon) {
  103. Util.Logger.debug(`IconCache: retrieving ${id}: ${icon}`);
  104. this._renewLifetime(id);
  105. return icon;
  106. }
  107. return null;
  108. }
  109. async _checkGC() {
  110. const cacheIsEmpty = this._cache.size === 0;
  111. if (!cacheIsEmpty && !this._gcTimeout) {
  112. Util.Logger.debug('IconCache: garbage collector started');
  113. let anyUnusedInCache = false;
  114. this._gcTimeout = new PromiseUtils.TimeoutSecondsPromise(GC_INTERVAL,
  115. GLib.PRIORITY_LOW);
  116. try {
  117. await this._gcTimeout;
  118. anyUnusedInCache = this._gc();
  119. } catch (e) {
  120. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  121. logError(e, 'IconCache: garbage collector');
  122. } finally {
  123. delete this._gcTimeout;
  124. }
  125. if (anyUnusedInCache)
  126. this._checkGC();
  127. } else if (cacheIsEmpty && this._gcTimeout) {
  128. Util.Logger.debug('IconCache: garbage collector stopped');
  129. this._gcTimeout.cancel();
  130. }
  131. }
  132. _gc() {
  133. const time = new Date().getTime();
  134. let anyUnused = false;
  135. this._cache.forEach((icon, id) => {
  136. if (Object.values(this._activeIcons).includes(icon)) {
  137. Util.Logger.debug(`IconCache: ${id} is in use.`);
  138. } else if (this._lifetime.get(id) < time) {
  139. this._remove(id);
  140. } else {
  141. anyUnused = true;
  142. Util.Logger.debug(`IconCache: ${id} survived this round.`);
  143. }
  144. });
  145. return anyUnused;
  146. }
  147. destroy() {
  148. this.clear();
  149. }
  150. }