reducePlan.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  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} target target
  11. * @property {TreeNode<T>} parent parent
  12. * @property {TreeNode<T>[]} children children
  13. * @property {number} entries number of entries
  14. * @property {boolean} active true when active, otherwise false
  15. * @property {T[] | T | undefined} value value
  16. */
  17. /**
  18. * @template T
  19. * @param {Map<string, T[] | T>} plan plan
  20. * @param {number} limit 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 [target, value] of plan) {
  27. treeMap.set(target, {
  28. target,
  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.target);
  40. if (parentPath !== node.target) {
  41. let parent = treeMap.get(parentPath);
  42. if (parent === undefined) {
  43. parent = {
  44. target: 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. When no reduction is needed at all, skip
  68. // building the candidate set entirely to avoid paying for the setup on the
  69. // common fast path.
  70. if (currentCount > limit) {
  71. // Pre-filter candidate nodes so the inner selection loop skips structural
  72. // non-candidates entirely. `children` length and parent presence are
  73. // fixed after tree construction; only `entries` can change (it can only
  74. // decrease), so a node that fails the `entries` check in a later round
  75. // is simply skipped via `continue`. When we merge a subtree we drop the
  76. // descendants from the candidate set to keep it shrinking over
  77. // iterations.
  78. /** @type {Set<TreeNode<T>>} */
  79. const candidates = new Set();
  80. for (const node of treeMap.values()) {
  81. if (!node.parent || !node.children) continue;
  82. if (node.children.length === 0) continue;
  83. if (node.children.length === 1 && !node.value) continue;
  84. candidates.add(node);
  85. }
  86. const costBias = limit * 0.3;
  87. while (currentCount > limit) {
  88. // Select node that helps reaching the limit most effectively without overmerging
  89. const overLimit = currentCount - limit;
  90. let bestNode;
  91. let bestCost = Infinity;
  92. for (const node of candidates) {
  93. if (node.entries <= 1) continue;
  94. // Try to select the node with has just a bit more entries than we need to reduce
  95. // When just a bit more is over 30% over the limit,
  96. // also consider just a bit less entries then we need to reduce
  97. const diff = node.entries - 1 - overLimit;
  98. const cost = diff >= 0 ? diff : -diff + costBias;
  99. if (cost < bestCost) {
  100. bestNode = node;
  101. bestCost = cost;
  102. // A cost of 0 means the merge reduces exactly to the limit;
  103. // no further candidate can improve on that, so stop scanning.
  104. if (cost === 0) break;
  105. }
  106. }
  107. if (!bestNode) break;
  108. // Merge all children
  109. const reduction = bestNode.entries - 1;
  110. bestNode.active = true;
  111. bestNode.entries = 1;
  112. candidates.delete(bestNode);
  113. currentCount -= reduction;
  114. let { parent } = bestNode;
  115. while (parent) {
  116. parent.entries -= reduction;
  117. parent = parent.parent;
  118. }
  119. const queue = new Set(bestNode.children);
  120. for (const node of queue) {
  121. node.active = false;
  122. node.entries = 0;
  123. candidates.delete(node);
  124. if (node.children) {
  125. for (const child of node.children) queue.add(child);
  126. }
  127. }
  128. }
  129. }
  130. // Write down new plan
  131. const newPlan = new Map();
  132. for (const rootNode of treeMap.values()) {
  133. if (!rootNode.active) continue;
  134. const map = new Map();
  135. const queue = new Set([rootNode]);
  136. for (const node of queue) {
  137. if (node.active && node !== rootNode) continue;
  138. if (node.value) {
  139. if (Array.isArray(node.value)) {
  140. for (const item of node.value) {
  141. map.set(item, node.target);
  142. }
  143. } else {
  144. map.set(node.value, node.target);
  145. }
  146. }
  147. if (node.children) {
  148. for (const child of node.children) {
  149. queue.add(child);
  150. }
  151. }
  152. }
  153. newPlan.set(rootNode.target, map);
  154. }
  155. return newPlan;
  156. };