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