Skip to content

Commit

Permalink
feat: add cache pruning
Browse files Browse the repository at this point in the history
Prune, by default, caches that are older than 2 days after accumulating
50 megabytes of cache.
  • Loading branch information
mzgoddard committed Jun 22, 2018
1 parent 87f8858 commit f946d68
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 0 deletions.
8 changes: 8 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,14 @@ class HardSourceWebpackPlugin {
'relativeHelpers',
]);

if (configHashInDirectory) {
const PruneCachesSystem = require('./lib/SystemPruneCaches');

new PruneCachesSystem(path.dirname(cacheDirPath), options.prune).apply(
compiler,
);
}

function runReadOrReset(_compiler) {
logger.unlock();

Expand Down
9 changes: 9 additions & 0 deletions lib/ChalkLoggerPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ const messages = {
short: value =>
`Reading from cache ${value.data.configHash.substring(0, 8)}...`,
},
'caches--delete-old': {
short: value =>
`Deleted ${value.data.deletedSizeMB} MB. Using ${
value.data.sizeMB
} MB of disk space.`,
},
'caches--keep': {
short: value => `Using ${value.data.sizeMB} MB of disk space.`,
},
'environment--inputs': {
short: value =>
`Tracking node dependencies with: ${value.data.inputs.join(', ')}.`,
Expand Down
142 changes: 142 additions & 0 deletions lib/SystemPruneCaches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
const { readdir: _readdir, stat: _stat } = require('fs');
const { basename, join } = require('path');

const _rimraf = require('rimraf');

const logMessages = require('./util/log-messages');
const pluginCompat = require('./util/plugin-compat');
const promisify = require('./util/promisify');

const readdir = promisify(_readdir);
const rimraf = promisify(_rimraf);
const stat = promisify(_stat);

const directorySize = async dir => {
const _stat = await stat(dir);
if (_stat.isFile()) {
return _stat.size;
}

if (_stat.isDirectory()) {
const names = await readdir(dir);
let size = 0;
for (const name of names) {
size += await directorySize(join(dir, name));
}
return size;
}

return 0;
};

class CacheInfo {
constructor(id = '') {
this.id = id;
this.lastModified = 0;
this.size = 0;
}

static async fromDirectory(dir) {
const info = new CacheInfo(basename(dir));
info.lastModified = +(await stat(join(dir, 'stamp'))).mtime;
info.size = await directorySize(dir);
return info;
}

static async fromDirectoryChildren(dir) {
const children = [];
const names = await readdir(dir);
for (const name of names) {
children.push(await CacheInfo.fromDirectory(join(dir, name)));
}
return children;
}
}

class PruneCachesSystem {
constructor(cacheRoot, options = {}) {
this.cacheRoot = cacheRoot;

this.options = Object.assign(
{
// Caches created or modified before `deleteAfter` are not considered for
// deletion. They must be at least this (default: 2 days) old.
deleteAfter: 2 * 24 * 60 * 60 * 1000,
// All caches together must be larger than `allCacheLimit` before any
// caches will be deleted. Together they must be at least this (default:
// 50 MB) in size.
allCacheLimit: 50 * 1024 * 1024,
},
options,
);
}

apply(compiler) {
const compilerHooks = pluginCompat.hooks(compiler);

const deleteOldCaches = async () => {
let infos;
try {
infos = await CacheInfo.fromDirectoryChildren(this.cacheRoot);
} catch (error) {
if (error.code === 'ENOENT') {
return;
}
throw error;
}

// Sort lastModified in descending order. More recently modified at the
// beginning of the array.
infos.sort((a, b) => b.lastModified - a.lastModified);

const totalSize = infos.reduce((carry, info) => carry + info.size, 0);
const oldInfos = infos.filter(
info => info.lastModified < Date.now() - this.options.deleteAfter,
);
const oldTotalSize = oldInfos.reduce(
(carry, info) => carry + info.size,
0,
);

if (oldInfos.length > 0 && totalSize > this.options.allCacheLimit) {
const newInfos = infos.filter(
info => info.lastModified >= Date.now() - this.options.deleteAfter,
);

for (const info of oldInfos) {
rimraf(join(this.cacheRoot, info.id));
}

const newTotalSize = newInfos.reduce(
(carry, info) => carry + info.size,
0,
);

logMessages.deleteOldCaches(compiler, {
infos,
totalSize,
newInfos,
newTotalSize,
oldInfos,
oldTotalSize,
});
} else {
logMessages.keepCaches(compiler, {
infos,
totalSize,
});
}
};

compilerHooks.watchRun.tapPromise(
'HardSource - PruneCachesSystem',
deleteOldCaches,
);
compilerHooks.run.tapPromise(
'HardSource - PruneCachesSystem',
deleteOldCaches,
);
}
}

module.exports = PruneCachesSystem;
29 changes: 29 additions & 0 deletions lib/util/log-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,35 @@ exports.configHashBuildWith = (compiler, { cacheDirPath, configHash }) => {
);
};

exports.deleteOldCaches = (compiler, { newTotalSize, oldTotalSize }) => {
const loggerCore = logCore(compiler);
const sizeMB = Math.ceil(newTotalSize / 1024 / 1024);
const deletedSizeMB = Math.ceil(oldTotalSize / 1024 / 1024);
loggerCore.log(
{
id: 'caches--delete-old',
size: newTotalSize,
sizeMB,
deletedSize: oldTotalSize,
deletedSizeMB,
},
`HardSourceWebpackPlugin is using ${sizeMB} MB of disk space after deleting ${deletedSizeMB} MB.`,
);
};

exports.keepCaches = (compiler, { totalSize }) => {
const loggerCore = logCore(compiler);
const sizeMB = Math.ceil(totalSize / 1024 / 1024);
loggerCore.log(
{
id: 'caches--keep',
size: totalSize,
sizeMB,
},
`HardSourceWebpackPlugin is using ${sizeMB} MB of disk space.`,
);
};

exports.environmentInputs = (compiler, { inputs }) => {
const loggerCore = logCore(compiler);
loggerCore.log(
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/hard-source-prune/config-hash
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
b
3 changes: 3 additions & 0 deletions tests/fixtures/hard-source-prune/fib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function(n) {
return n + (n > 0 ? n - 2 : 0);
};
3 changes: 3 additions & 0 deletions tests/fixtures/hard-source-prune/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
var fib = require('./fib');

console.log(fib(3));
27 changes: 27 additions & 0 deletions tests/fixtures/hard-source-prune/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
var fs = require('fs');

var HardSourceWebpackPlugin = require('../../..');

module.exports = {
context: __dirname,
entry: './index.js',
output: {
path: __dirname + '/tmp',
filename: 'main.js',
},
plugins: [
new HardSourceWebpackPlugin({
cacheDirectory: 'cache/[confighash]',
configHash: function(config) {
return fs.readFileSync(__dirname + '/config-hash', 'utf8');
},
environmentHash: {
root: __dirname + '/../../..',
},
prune: {
deleteAfter: -2000,
allCacheLimit: 0,
},
}),
],
};
9 changes: 9 additions & 0 deletions tests/hard-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,4 +278,13 @@ describe('hard-source features', function() {
itCompilesTwice('hard-source-exclude-plugin');
itCompilesHardModules('hard-source-exclude-plugin', ['./index.js', '!./fib.js']);

itCompilesChange('hard-source-prune', {
'config-hash': 'a',
}, {
'config-hash': 'b',
}, function(output) {
expect(fs.readdirSync(__dirname + '/fixtures/hard-source-prune/tmp/cache'))
.to.have.length(1);
});

});

0 comments on commit f946d68

Please sign in to comment.