IdleFileCachePlugin.js 6.7 KB

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