MemoryWithGcCachePlugin.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const Cache = require("../Cache");
  7. /** @import { Data, Etag } from "../Cache" */
  8. /** @import Compiler from "../Compiler" */
  9. /**
  10. * Defines the memory with gc cache plugin options type used by this module.
  11. * @typedef {object} MemoryWithGcCachePluginOptions
  12. * @property {number} maxGenerations max generations
  13. */
  14. const PLUGIN_NAME = "MemoryWithGcCachePlugin";
  15. class MemoryWithGcCachePlugin {
  16. /**
  17. * Creates an instance of MemoryWithGcCachePlugin.
  18. * @param {MemoryWithGcCachePluginOptions} options options
  19. */
  20. constructor({ maxGenerations }) {
  21. /** @type {number} */
  22. this._maxGenerations = maxGenerations;
  23. }
  24. /**
  25. * Applies the plugin by registering its hooks on the compiler.
  26. * @param {Compiler} compiler the compiler instance
  27. * @returns {void}
  28. */
  29. apply(compiler) {
  30. const maxGenerations = this._maxGenerations;
  31. /** @type {Map<string, { etag: Etag | null, data: Data } | undefined | null>} */
  32. const cache = new Map();
  33. /** @type {Map<string, { entry: { etag: Etag | null, data: Data } | null, until: number }>} */
  34. const oldCache = new Map();
  35. let generation = 0;
  36. let cachePosition = 0;
  37. const logger = compiler.getInfrastructureLogger(PLUGIN_NAME);
  38. compiler.hooks.afterDone.tap(PLUGIN_NAME, () => {
  39. generation++;
  40. let clearedEntries = 0;
  41. /** @type {undefined | string} */
  42. let lastClearedIdentifier;
  43. // Avoid coverage problems due indirect changes
  44. /* istanbul ignore next */
  45. for (const [identifier, entry] of oldCache) {
  46. if (entry.until > generation) break;
  47. oldCache.delete(identifier);
  48. if (cache.get(identifier) === undefined) {
  49. cache.delete(identifier);
  50. clearedEntries++;
  51. lastClearedIdentifier = identifier;
  52. }
  53. }
  54. if (clearedEntries > 0 || oldCache.size > 0) {
  55. logger.log(
  56. `${cache.size - oldCache.size} active entries, ${
  57. oldCache.size
  58. } recently unused cached entries${
  59. clearedEntries > 0
  60. ? `, ${clearedEntries} old unused cache entries removed e. g. ${lastClearedIdentifier}`
  61. : ""
  62. }`
  63. );
  64. }
  65. let i = (cache.size / maxGenerations) | 0;
  66. let j = cachePosition >= cache.size ? 0 : cachePosition;
  67. cachePosition = j + i;
  68. for (const [identifier, entry] of cache) {
  69. if (j !== 0) {
  70. j--;
  71. continue;
  72. }
  73. if (entry !== undefined) {
  74. // We don't delete the cache entry, but set it to undefined instead
  75. // This reserves the location in the data table and avoids rehashing
  76. // when constantly adding and removing entries.
  77. // It will be deleted when removed from oldCache.
  78. cache.set(identifier, undefined);
  79. oldCache.delete(identifier);
  80. oldCache.set(identifier, {
  81. entry,
  82. until: generation + maxGenerations
  83. });
  84. if (i-- === 0) break;
  85. }
  86. }
  87. });
  88. compiler.cache.hooks.store.tap(
  89. { name: PLUGIN_NAME, stage: Cache.STAGE_MEMORY },
  90. (identifier, etag, data) => {
  91. cache.set(identifier, { etag, data });
  92. }
  93. );
  94. compiler.cache.hooks.get.tap(
  95. { name: PLUGIN_NAME, stage: Cache.STAGE_MEMORY },
  96. (identifier, etag, gotHandlers) => {
  97. const cacheEntry = cache.get(identifier);
  98. // A recorded miss: the whole chain was asked for this identifier already.
  99. if (cacheEntry === null) return null;
  100. // Etags are compared by identity — a lazy one is interned per source
  101. // object, so equal content reached through a second object is a
  102. // different etag. Hashing to tell those apart would cost every hit what
  103. // laziness saves, so a mismatch falls through to the next stage instead:
  104. // the file cache compares etags by value and can still answer. Returning
  105. // `null` here would bail the hook and lose that (`Cache.get` maps it to
  106. // `undefined` regardless, so it buys the caller nothing).
  107. let known = cacheEntry !== undefined;
  108. if (cacheEntry !== undefined) {
  109. if (cacheEntry.etag === etag) return cacheEntry.data;
  110. } else {
  111. const oldCacheEntry = oldCache.get(identifier);
  112. if (oldCacheEntry !== undefined) {
  113. const entry = oldCacheEntry.entry;
  114. if (entry === null) {
  115. oldCache.delete(identifier);
  116. cache.set(identifier, entry);
  117. return null;
  118. }
  119. known = true;
  120. if (entry.etag === etag) {
  121. oldCache.delete(identifier);
  122. cache.set(identifier, entry);
  123. return entry.data;
  124. }
  125. }
  126. }
  127. gotHandlers.push((result, callback) => {
  128. if (result !== undefined) {
  129. cache.set(identifier, { etag, data: result });
  130. } else if (!known) {
  131. // Record the miss only for an identifier nothing was known about:
  132. // an entry reached with a different etag still answers its own.
  133. cache.set(identifier, null);
  134. }
  135. return callback();
  136. });
  137. }
  138. );
  139. compiler.cache.hooks.shutdown.tap(
  140. { name: PLUGIN_NAME, stage: Cache.STAGE_MEMORY },
  141. () => {
  142. cache.clear();
  143. oldCache.clear();
  144. }
  145. );
  146. }
  147. }
  148. module.exports = MemoryWithGcCachePlugin;