MemoryWithGcCachePlugin.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  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. /** @typedef {import("webpack-sources").Source} Source */
  8. /** @typedef {import("../Cache").Data} Data */
  9. /** @typedef {import("../Cache").Etag} Etag */
  10. /** @typedef {import("../Compiler")} Compiler */
  11. /** @typedef {import("../Module")} Module */
  12. /**
  13. * @typedef {object} MemoryWithGcCachePluginOptions
  14. * @property {number} maxGenerations max generations
  15. */
  16. const PLUGIN_NAME = "MemoryWithGcCachePlugin";
  17. class MemoryWithGcCachePlugin {
  18. /**
  19. * @param {MemoryWithGcCachePluginOptions} options options
  20. */
  21. constructor({ maxGenerations }) {
  22. this._maxGenerations = maxGenerations;
  23. }
  24. /**
  25. * Apply the plugin
  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. let lastClearedIdentifier;
  42. // Avoid coverage problems due indirect changes
  43. /* istanbul ignore next */
  44. for (const [identifier, entry] of oldCache) {
  45. if (entry.until > generation) break;
  46. oldCache.delete(identifier);
  47. if (cache.get(identifier) === undefined) {
  48. cache.delete(identifier);
  49. clearedEntries++;
  50. lastClearedIdentifier = identifier;
  51. }
  52. }
  53. if (clearedEntries > 0 || oldCache.size > 0) {
  54. logger.log(
  55. `${cache.size - oldCache.size} active entries, ${
  56. oldCache.size
  57. } recently unused cached entries${
  58. clearedEntries > 0
  59. ? `, ${clearedEntries} old unused cache entries removed e. g. ${lastClearedIdentifier}`
  60. : ""
  61. }`
  62. );
  63. }
  64. let i = (cache.size / maxGenerations) | 0;
  65. let j = cachePosition >= cache.size ? 0 : cachePosition;
  66. cachePosition = j + i;
  67. for (const [identifier, entry] of cache) {
  68. if (j !== 0) {
  69. j--;
  70. continue;
  71. }
  72. if (entry !== undefined) {
  73. // We don't delete the cache entry, but set it to undefined instead
  74. // This reserves the location in the data table and avoids rehashing
  75. // when constantly adding and removing entries.
  76. // It will be deleted when removed from oldCache.
  77. cache.set(identifier, undefined);
  78. oldCache.delete(identifier);
  79. oldCache.set(identifier, {
  80. entry,
  81. until: generation + maxGenerations
  82. });
  83. if (i-- === 0) break;
  84. }
  85. }
  86. });
  87. compiler.cache.hooks.store.tap(
  88. { name: PLUGIN_NAME, stage: Cache.STAGE_MEMORY },
  89. (identifier, etag, data) => {
  90. cache.set(identifier, { etag, data });
  91. }
  92. );
  93. compiler.cache.hooks.get.tap(
  94. { name: PLUGIN_NAME, stage: Cache.STAGE_MEMORY },
  95. (identifier, etag, gotHandlers) => {
  96. const cacheEntry = cache.get(identifier);
  97. if (cacheEntry === null) {
  98. return null;
  99. } else if (cacheEntry !== undefined) {
  100. return cacheEntry.etag === etag ? cacheEntry.data : null;
  101. }
  102. const oldCacheEntry = oldCache.get(identifier);
  103. if (oldCacheEntry !== undefined) {
  104. const cacheEntry = oldCacheEntry.entry;
  105. if (cacheEntry === null) {
  106. oldCache.delete(identifier);
  107. cache.set(identifier, cacheEntry);
  108. return null;
  109. }
  110. if (cacheEntry.etag !== etag) return null;
  111. oldCache.delete(identifier);
  112. cache.set(identifier, cacheEntry);
  113. return cacheEntry.data;
  114. }
  115. gotHandlers.push((result, callback) => {
  116. if (result === undefined) {
  117. cache.set(identifier, null);
  118. } else {
  119. cache.set(identifier, { etag, data: result });
  120. }
  121. return callback();
  122. });
  123. }
  124. );
  125. compiler.cache.hooks.shutdown.tap(
  126. { name: PLUGIN_NAME, stage: Cache.STAGE_MEMORY },
  127. () => {
  128. cache.clear();
  129. oldCache.clear();
  130. }
  131. );
  132. }
  133. }
  134. module.exports = MemoryWithGcCachePlugin;