IdleFileCachePlugin.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. const ProgressPlugin = require("../ProgressPlugin");
  8. /** @typedef {import("../Compiler")} Compiler */
  9. /** @typedef {import("./PackFileCacheStrategy")} PackFileCacheStrategy */
  10. const BUILD_DEPENDENCIES_KEY = Symbol("build dependencies key");
  11. const PLUGIN_NAME = "IdleFileCachePlugin";
  12. class IdleFileCachePlugin {
  13. /**
  14. * Creates an instance of IdleFileCachePlugin.
  15. * @param {PackFileCacheStrategy} strategy cache strategy
  16. * @param {number} idleTimeout timeout
  17. * @param {number} idleTimeoutForInitialStore initial timeout
  18. * @param {number} idleTimeoutAfterLargeChanges timeout after changes
  19. */
  20. constructor(
  21. strategy,
  22. idleTimeout,
  23. idleTimeoutForInitialStore,
  24. idleTimeoutAfterLargeChanges
  25. ) {
  26. this.strategy = strategy;
  27. this.idleTimeout = idleTimeout;
  28. this.idleTimeoutForInitialStore = idleTimeoutForInitialStore;
  29. this.idleTimeoutAfterLargeChanges = idleTimeoutAfterLargeChanges;
  30. }
  31. /**
  32. * Applies the plugin by registering its hooks on the compiler.
  33. * @param {Compiler} compiler the compiler instance
  34. * @returns {void}
  35. */
  36. apply(compiler) {
  37. const strategy = this.strategy;
  38. const idleTimeout = this.idleTimeout;
  39. const idleTimeoutForInitialStore = Math.min(
  40. idleTimeout,
  41. this.idleTimeoutForInitialStore
  42. );
  43. const idleTimeoutAfterLargeChanges = this.idleTimeoutAfterLargeChanges;
  44. const resolvedPromise = Promise.resolve();
  45. let timeSpendInBuild = 0;
  46. let timeSpendInStore = 0;
  47. let avgTimeSpendInStore = 0;
  48. /** @type {Map<string | typeof BUILD_DEPENDENCIES_KEY, () => Promise<void | void[]>>} */
  49. const pendingIdleTasks = new Map();
  50. compiler.cache.hooks.store.tap(
  51. { name: PLUGIN_NAME, stage: Cache.STAGE_DISK },
  52. (identifier, etag, data) => {
  53. pendingIdleTasks.set(identifier, () =>
  54. strategy.store(identifier, etag, data)
  55. );
  56. }
  57. );
  58. compiler.cache.hooks.get.tapPromise(
  59. { name: PLUGIN_NAME, stage: Cache.STAGE_DISK },
  60. (identifier, etag, gotHandlers) => {
  61. const restore = () =>
  62. strategy.restore(identifier, etag).then((cacheEntry) => {
  63. if (cacheEntry === undefined) {
  64. gotHandlers.push((result, callback) => {
  65. if (result !== undefined) {
  66. pendingIdleTasks.set(identifier, () =>
  67. strategy.store(identifier, etag, result)
  68. );
  69. }
  70. callback();
  71. });
  72. } else {
  73. return cacheEntry;
  74. }
  75. });
  76. const pendingTask = pendingIdleTasks.get(identifier);
  77. if (pendingTask !== undefined) {
  78. pendingIdleTasks.delete(identifier);
  79. return pendingTask().then(restore);
  80. }
  81. return restore();
  82. }
  83. );
  84. compiler.cache.hooks.storeBuildDependencies.tap(
  85. { name: PLUGIN_NAME, stage: Cache.STAGE_DISK },
  86. (dependencies) => {
  87. pendingIdleTasks.set(BUILD_DEPENDENCIES_KEY, () =>
  88. Promise.resolve().then(() =>
  89. strategy.storeBuildDependencies(dependencies)
  90. )
  91. );
  92. }
  93. );
  94. compiler.cache.hooks.shutdown.tapPromise(
  95. { name: PLUGIN_NAME, stage: Cache.STAGE_DISK },
  96. () => {
  97. if (idleTimer) {
  98. clearTimeout(idleTimer);
  99. idleTimer = undefined;
  100. }
  101. isIdle = false;
  102. const reportProgress = ProgressPlugin.getReporter(compiler);
  103. const jobs = [...pendingIdleTasks.values()];
  104. if (reportProgress) reportProgress(0, "process pending cache items");
  105. const promises = jobs.map((fn) => fn());
  106. pendingIdleTasks.clear();
  107. promises.push(currentIdlePromise);
  108. const promise = Promise.all(promises);
  109. currentIdlePromise = promise.then(() => strategy.afterAllStored());
  110. if (reportProgress) {
  111. currentIdlePromise = currentIdlePromise.then(() => {
  112. reportProgress(1, "stored");
  113. });
  114. }
  115. return currentIdlePromise.then(() => {
  116. // Reset strategy
  117. if (strategy.clear) strategy.clear();
  118. });
  119. }
  120. );
  121. /** @type {Promise<void | void[]>} */
  122. let currentIdlePromise = resolvedPromise;
  123. let isIdle = false;
  124. let isInitialStore = true;
  125. const processIdleTasks = () => {
  126. if (isIdle) {
  127. const startTime = Date.now();
  128. if (pendingIdleTasks.size > 0) {
  129. const promises = [currentIdlePromise];
  130. const maxTime = startTime + 100;
  131. let maxCount = 100;
  132. for (const [filename, factory] of pendingIdleTasks) {
  133. pendingIdleTasks.delete(filename);
  134. promises.push(factory());
  135. if (maxCount-- <= 0 || Date.now() > maxTime) break;
  136. }
  137. currentIdlePromise = Promise.all(
  138. /** @type {Promise<void>[]} */
  139. (promises)
  140. );
  141. currentIdlePromise.then(() => {
  142. timeSpendInStore += Date.now() - startTime;
  143. // Allow to exit the process between
  144. idleTimer = setTimeout(processIdleTasks, 0);
  145. idleTimer.unref();
  146. });
  147. return;
  148. }
  149. currentIdlePromise = currentIdlePromise
  150. .then(async () => {
  151. await strategy.afterAllStored();
  152. timeSpendInStore += Date.now() - startTime;
  153. avgTimeSpendInStore =
  154. Math.max(avgTimeSpendInStore, timeSpendInStore) * 0.9 +
  155. timeSpendInStore * 0.1;
  156. timeSpendInStore = 0;
  157. timeSpendInBuild = 0;
  158. })
  159. .catch((err) => {
  160. const logger = compiler.getInfrastructureLogger(PLUGIN_NAME);
  161. logger.warn(`Background tasks during idle failed: ${err.message}`);
  162. logger.debug(err.stack);
  163. });
  164. isInitialStore = false;
  165. }
  166. };
  167. /** @type {ReturnType<typeof setTimeout> | undefined} */
  168. let idleTimer;
  169. compiler.cache.hooks.beginIdle.tap(
  170. { name: PLUGIN_NAME, stage: Cache.STAGE_DISK },
  171. () => {
  172. const isLargeChange = timeSpendInBuild > avgTimeSpendInStore * 2;
  173. if (isInitialStore && idleTimeoutForInitialStore < idleTimeout) {
  174. compiler
  175. .getInfrastructureLogger(PLUGIN_NAME)
  176. .log(
  177. `Initial cache was generated and cache will be persisted in ${
  178. idleTimeoutForInitialStore / 1000
  179. }s.`
  180. );
  181. } else if (
  182. isLargeChange &&
  183. idleTimeoutAfterLargeChanges < idleTimeout
  184. ) {
  185. compiler
  186. .getInfrastructureLogger(PLUGIN_NAME)
  187. .log(
  188. `Spend ${Math.round(timeSpendInBuild) / 1000}s in build and ${
  189. Math.round(avgTimeSpendInStore) / 1000
  190. }s in average in cache store. This is considered as large change and cache will be persisted in ${
  191. idleTimeoutAfterLargeChanges / 1000
  192. }s.`
  193. );
  194. }
  195. idleTimer = setTimeout(
  196. () => {
  197. idleTimer = undefined;
  198. isIdle = true;
  199. resolvedPromise.then(processIdleTasks);
  200. },
  201. Math.min(
  202. isInitialStore ? idleTimeoutForInitialStore : Infinity,
  203. isLargeChange ? idleTimeoutAfterLargeChanges : Infinity,
  204. idleTimeout
  205. )
  206. );
  207. idleTimer.unref();
  208. }
  209. );
  210. compiler.cache.hooks.endIdle.tap(
  211. { name: PLUGIN_NAME, stage: Cache.STAGE_DISK },
  212. () => {
  213. if (idleTimer) {
  214. clearTimeout(idleTimer);
  215. idleTimer = undefined;
  216. }
  217. isIdle = false;
  218. }
  219. );
  220. compiler.hooks.done.tap(PLUGIN_NAME, (stats) => {
  221. // 10% build overhead is ignored, as it's not cacheable
  222. timeSpendInBuild *= 0.9;
  223. timeSpendInBuild +=
  224. /** @type {number} */ (stats.endTime) -
  225. /** @type {number} */ (stats.startTime);
  226. });
  227. }
  228. }
  229. module.exports = IdleFileCachePlugin;