SystemPruneCaches.js 4.2 KB

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