reducePlan.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const path = require("path");
  7. /**
  8. * @template T
  9. * @typedef {Object} TreeNode
  10. * @property {string} filePath
  11. * @property {TreeNode} parent
  12. * @property {TreeNode[]} children
  13. * @property {number} entries
  14. * @property {boolean} active
  15. * @property {T[] | T | undefined} value
  16. */
  17. /**
  18. * @template T
  19. * @param {Map<string, T[] | T} plan
  20. * @param {number} limit
  21. * @returns {Map<string, Map<T, string>>} the new plan
  22. */
  23. module.exports = (plan, limit) => {
  24. const treeMap = new Map();
  25. // Convert to tree
  26. for (const [filePath, value] of plan) {
  27. treeMap.set(filePath, {
  28. filePath,
  29. parent: undefined,
  30. children: undefined,
  31. entries: 1,
  32. active: true,
  33. value
  34. });
  35. }
  36. let currentCount = treeMap.size;
  37. // Create parents and calculate sum of entries
  38. for (const node of treeMap.values()) {
  39. const parentPath = path.dirname(node.filePath);
  40. if (parentPath !== node.filePath) {
  41. let parent = treeMap.get(parentPath);
  42. if (parent === undefined) {
  43. parent = {
  44. filePath: parentPath,
  45. parent: undefined,
  46. children: [node],
  47. entries: node.entries,
  48. active: false,
  49. value: undefined
  50. };
  51. treeMap.set(parentPath, parent);
  52. node.parent = parent;
  53. } else {
  54. node.parent = parent;
  55. if (parent.children === undefined) {
  56. parent.children = [node];
  57. } else {
  58. parent.children.push(node);
  59. }
  60. do {
  61. parent.entries += node.entries;
  62. parent = parent.parent;
  63. } while (parent);
  64. }
  65. }
  66. }
  67. // Reduce until limit reached
  68. while (currentCount > limit) {
  69. // Select node that helps reaching the limit most effectively without overmerging
  70. const overLimit = currentCount - limit;
  71. let bestNode = undefined;
  72. let bestCost = Infinity;
  73. for (const node of treeMap.values()) {
  74. if (node.entries <= 1 || !node.children || !node.parent) continue;
  75. if (node.children.length === 0) continue;
  76. if (node.children.length === 1 && !node.value) continue;
  77. // Try to select the node with has just a bit more entries than we need to reduce
  78. // When just a bit more is over 30% over the limit,
  79. // also consider just a bit less entries then we need to reduce
  80. const cost =
  81. node.entries - 1 >= overLimit
  82. ? node.entries - 1 - overLimit
  83. : overLimit - node.entries + 1 + limit * 0.3;
  84. if (cost < bestCost) {
  85. bestNode = node;
  86. bestCost = cost;
  87. }
  88. }
  89. if (!bestNode) break;
  90. // Merge all children
  91. const reduction = bestNode.entries - 1;
  92. bestNode.active = true;
  93. bestNode.entries = 1;
  94. currentCount -= reduction;
  95. let parent = bestNode.parent;
  96. while (parent) {
  97. parent.entries -= reduction;
  98. parent = parent.parent;
  99. }
  100. const queue = new Set(bestNode.children);
  101. for (const node of queue) {
  102. node.active = false;
  103. node.entries = 0;
  104. if (node.children) {
  105. for (const child of node.children) queue.add(child);
  106. }
  107. }
  108. }
  109. // Write down new plan
  110. const newPlan = new Map();
  111. for (const rootNode of treeMap.values()) {
  112. if (!rootNode.active) continue;
  113. const map = new Map();
  114. const queue = new Set([rootNode]);
  115. for (const node of queue) {
  116. if (node.active && node !== rootNode) continue;
  117. if (node.value) {
  118. if (Array.isArray(node.value)) {
  119. for (const item of node.value) {
  120. map.set(item, node.filePath);
  121. }
  122. } else {
  123. map.set(node.value, node.filePath);
  124. }
  125. }
  126. if (node.children) {
  127. for (const child of node.children) {
  128. queue.add(child);
  129. }
  130. }
  131. }
  132. newPlan.set(rootNode.filePath, map);
  133. }
  134. return newPlan;
  135. };