IdleFileCachePlugin.js 7.2 KB

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