Skip to content

Commit c685a61

Browse files
authored
feat: add cache pruning (#386)
- Add `prune` configuration information to README Prune, by default, caches that are older than 2 days after accumulating 50 megabytes of cache.
1 parent 6f27e91 commit c685a61

File tree

11 files changed

+279
-4
lines changed

11 files changed

+279
-4
lines changed

.travis.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ matrix:
1010
- node_js: node
1111
env: NPM_SCRIPT=commitlint-travis
1212
- node_js: node
13-
env: NPM_INSTALL_EXTRA="webpack@4 file-loader@1 html-webpack-plugin@3.2.0"
13+
env: NPM_SCRIPT=test NPM_INSTALL_EXTRA="webpack@4 file-loader@1 html-webpack-plugin@3.2.0"
1414
- node_js: 8
15-
env: NPM_INSTALL_EXTRA="webpack@3 file-loader@0.11 html-webpack-plugin@2.22.0"
15+
env: NPM_SCRIPT=test NPM_INSTALL_EXTRA="webpack@3 file-loader@0.11 html-webpack-plugin@2.22.0"
1616
- node_js: 8
17-
env: NPM_INSTALL_EXTRA="webpack@4 file-loader@1 html-webpack-plugin@3.2.0"
17+
env: NPM_SCRIPT=test NPM_INSTALL_EXTRA="webpack@4 file-loader@1 html-webpack-plugin@3.2.0"
1818

1919
before_script:
2020
- npm install ${NPM_INSTALL_EXTRA}
21-
script: npm run ${NPM_SCRIPT:test}
21+
script: npm run ${NPM_SCRIPT}
2222
cache:
2323
directories:
2424
- node_modules

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ new HardSourceWebpackPlugin({
4444
// 'debug', 'log', 'info', 'warn', or 'error'.
4545
level: 'debug',
4646
},
47+
// Clean up large, old caches automatically.
48+
cachePrune: {
49+
// Caches younger than `maxAge` are not considered for deletion. They must
50+
// be at least this (default: 2 days) old in milliseconds.
51+
maxAge: 2 * 24 * 60 * 60 * 1000,
52+
// All caches together must be larger than `sizeThreshold` before any
53+
// caches will be deleted. Together they must be at least this
54+
// (default: 50 MB) big in bytes.
55+
sizeThreshold: 50 * 1024 * 1024
56+
},
4757
}),
4858
```
4959

@@ -146,6 +156,18 @@ The level of log messages to report down to. Defaults to 'debug' when mode is 'n
146156

147157
For example 'debug' reports all messages while 'warn' reports warn and error level messages.
148158

159+
### `cachePrune`
160+
161+
`hard-source` caches are by default created when the webpack configuration changes. Each cache holds a copy of all the data to create a build so they can become quite large. Once a cache is considered "old enough" that it is unlikely to be reused `hard-source` will delete it to free up space automatically.
162+
163+
#### `maxAge`
164+
165+
Caches older than `maxAge` in milliseconds are considered for automatic deletion.
166+
167+
#### `sizeThreshold`
168+
169+
For caches to be deleted, all of them together must total more than this threshold.
170+
149171
## Troubleshooting
150172

151173
### Configuration changes are not being detected

index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,15 @@ class HardSourceWebpackPlugin {
241241
'relativeHelpers',
242242
]);
243243

244+
if (configHashInDirectory) {
245+
const PruneCachesSystem = require('./lib/SystemPruneCaches');
246+
247+
new PruneCachesSystem(
248+
path.dirname(cacheDirPath),
249+
options.cachePrune,
250+
).apply(compiler);
251+
}
252+
244253
function runReadOrReset(_compiler) {
245254
logger.unlock();
246255

lib/ChalkLoggerPlugin.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ const messages = {
2727
short: value =>
2828
`Reading from cache ${value.data.configHash.substring(0, 8)}...`,
2929
},
30+
'caches--delete-old': {
31+
short: value =>
32+
`Deleted ${value.data.deletedSizeMB} MB. Using ${
33+
value.data.sizeMB
34+
} MB of disk space.`,
35+
},
36+
'caches--keep': {
37+
short: value => `Using ${value.data.sizeMB} MB of disk space.`,
38+
},
3039
'environment--inputs': {
3140
short: value =>
3241
`Tracking node dependencies with: ${value.data.inputs.join(', ')}.`,

lib/SystemPruneCaches.js

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
const { readdir: _readdir, stat: _stat } = require('fs');
2+
const { basename, join } = require('path');
3+
4+
const _rimraf = require('rimraf');
5+
6+
const logMessages = require('./util/log-messages');
7+
const pluginCompat = require('./util/plugin-compat');
8+
const promisify = require('./util/promisify');
9+
10+
const readdir = promisify(_readdir);
11+
const rimraf = promisify(_rimraf);
12+
const stat = promisify(_stat);
13+
14+
const directorySize = async dir => {
15+
const _stat = await stat(dir);
16+
if (_stat.isFile()) {
17+
return _stat.size;
18+
}
19+
20+
if (_stat.isDirectory()) {
21+
const names = await readdir(dir);
22+
let size = 0;
23+
for (const name of names) {
24+
size += await directorySize(join(dir, name));
25+
}
26+
return size;
27+
}
28+
29+
return 0;
30+
};
31+
32+
class CacheInfo {
33+
constructor(id = '') {
34+
this.id = id;
35+
this.lastModified = 0;
36+
this.size = 0;
37+
}
38+
39+
static async fromDirectory(dir) {
40+
const info = new CacheInfo(basename(dir));
41+
info.lastModified = new Date(
42+
(await stat(join(dir, 'stamp'))).mtime,
43+
).getTime();
44+
info.size = await directorySize(dir);
45+
return info;
46+
}
47+
48+
static async fromDirectoryChildren(dir) {
49+
const children = [];
50+
const names = await readdir(dir);
51+
for (const name of names) {
52+
children.push(await CacheInfo.fromDirectory(join(dir, name)));
53+
}
54+
return children;
55+
}
56+
}
57+
58+
// Compilers for webpack with multiple parallel configurations might try to
59+
// delete caches at the same time. Mutex lock the process of pruning to keep
60+
// from multiple pruning runs from colliding with each other.
61+
let deleteLock = null;
62+
63+
class PruneCachesSystem {
64+
constructor(cacheRoot, options = {}) {
65+
this.cacheRoot = cacheRoot;
66+
67+
this.options = Object.assign(
68+
{
69+
// Caches younger than `maxAge` are not considered for deletion. They
70+
// must be at least this (default: 2 days) old in milliseconds.
71+
maxAge: 2 * 24 * 60 * 60 * 1000,
72+
// All caches together must be larger than `sizeThreshold` before any
73+
// caches will be deleted. Together they must be at least this
74+
// (default: 50 MB) big in bytes.
75+
sizeThreshold: 50 * 1024 * 1024,
76+
},
77+
options,
78+
);
79+
}
80+
81+
apply(compiler) {
82+
const compilerHooks = pluginCompat.hooks(compiler);
83+
84+
const deleteOldCaches = async () => {
85+
while (deleteLock !== null) {
86+
await deleteLock;
87+
}
88+
89+
let resolveLock;
90+
91+
let infos;
92+
try {
93+
deleteLock = new Promise(resolve => {
94+
resolveLock = resolve;
95+
});
96+
97+
infos = await CacheInfo.fromDirectoryChildren(this.cacheRoot);
98+
99+
// Sort lastModified in descending order. More recently modified at the
100+
// beginning of the array.
101+
infos.sort((a, b) => b.lastModified - a.lastModified);
102+
103+
const totalSize = infos.reduce((carry, info) => carry + info.size, 0);
104+
const oldInfos = infos.filter(
105+
info => info.lastModified < Date.now() - this.options.maxAge,
106+
);
107+
const oldTotalSize = oldInfos.reduce(
108+
(carry, info) => carry + info.size,
109+
0,
110+
);
111+
112+
if (oldInfos.length > 0 && totalSize > this.options.sizeThreshold) {
113+
const newInfos = infos.filter(
114+
info => info.lastModified >= Date.now() - this.options.maxAge,
115+
);
116+
117+
for (const info of oldInfos) {
118+
rimraf(join(this.cacheRoot, info.id));
119+
}
120+
121+
const newTotalSize = newInfos.reduce(
122+
(carry, info) => carry + info.size,
123+
0,
124+
);
125+
126+
logMessages.deleteOldCaches(compiler, {
127+
infos,
128+
totalSize,
129+
newInfos,
130+
newTotalSize,
131+
oldInfos,
132+
oldTotalSize,
133+
});
134+
} else {
135+
logMessages.keepCaches(compiler, {
136+
infos,
137+
totalSize,
138+
});
139+
}
140+
} catch (error) {
141+
if (error.code !== 'ENOENT') {
142+
throw error;
143+
}
144+
} finally {
145+
if (typeof resolveLock === 'function') {
146+
deleteLock = null;
147+
resolveLock();
148+
}
149+
}
150+
};
151+
152+
compilerHooks.watchRun.tapPromise(
153+
'HardSource - PruneCachesSystem',
154+
deleteOldCaches,
155+
);
156+
compilerHooks.run.tapPromise(
157+
'HardSource - PruneCachesSystem',
158+
deleteOldCaches,
159+
);
160+
}
161+
}
162+
163+
module.exports = PruneCachesSystem;

lib/util/log-messages.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,35 @@ exports.configHashBuildWith = (compiler, { cacheDirPath, configHash }) => {
9191
);
9292
};
9393

94+
exports.deleteOldCaches = (compiler, { newTotalSize, oldTotalSize }) => {
95+
const loggerCore = logCore(compiler);
96+
const sizeMB = Math.ceil(newTotalSize / 1024 / 1024);
97+
const deletedSizeMB = Math.ceil(oldTotalSize / 1024 / 1024);
98+
loggerCore.log(
99+
{
100+
id: 'caches--delete-old',
101+
size: newTotalSize,
102+
sizeMB,
103+
deletedSize: oldTotalSize,
104+
deletedSizeMB,
105+
},
106+
`HardSourceWebpackPlugin is using ${sizeMB} MB of disk space after deleting ${deletedSizeMB} MB.`,
107+
);
108+
};
109+
110+
exports.keepCaches = (compiler, { totalSize }) => {
111+
const loggerCore = logCore(compiler);
112+
const sizeMB = Math.ceil(totalSize / 1024 / 1024);
113+
loggerCore.log(
114+
{
115+
id: 'caches--keep',
116+
size: totalSize,
117+
sizeMB,
118+
},
119+
`HardSourceWebpackPlugin is using ${sizeMB} MB of disk space.`,
120+
);
121+
};
122+
94123
exports.environmentInputs = (compiler, { inputs }) => {
95124
const loggerCore = logCore(compiler);
96125
loggerCore.log(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
b
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = function(n) {
2+
return n + (n > 0 ? n - 2 : 0);
3+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
var fib = require('./fib');
2+
3+
console.log(fib(3));
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
var fs = require('fs');
2+
3+
var HardSourceWebpackPlugin = require('../../..');
4+
5+
module.exports = {
6+
context: __dirname,
7+
entry: './index.js',
8+
output: {
9+
path: __dirname + '/tmp',
10+
filename: 'main.js',
11+
},
12+
plugins: [
13+
new HardSourceWebpackPlugin({
14+
cacheDirectory: 'cache/[confighash]',
15+
configHash: function(config) {
16+
return fs.readFileSync(__dirname + '/config-hash', 'utf8');
17+
},
18+
environmentHash: {
19+
root: __dirname + '/../../..',
20+
},
21+
cachePrune: {
22+
maxAge: -2000,
23+
sizeThreshold: 0,
24+
},
25+
}),
26+
],
27+
};

0 commit comments

Comments
 (0)