@atlaspack/bundler-default 2.14.5-canary.6 → 2.14.5-canary.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.findMergeCandidates = findMergeCandidates;
7
+ function _assert() {
8
+ const data = _interopRequireDefault(require("assert"));
9
+ _assert = function () {
10
+ return data;
11
+ };
12
+ return data;
13
+ }
14
+ function _nullthrows() {
15
+ const data = _interopRequireDefault(require("nullthrows"));
16
+ _nullthrows = function () {
17
+ return data;
18
+ };
19
+ return data;
20
+ }
21
+ function _graph() {
22
+ const data = require("@atlaspack/graph");
23
+ _graph = function () {
24
+ return data;
25
+ };
26
+ return data;
27
+ }
28
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
29
+ // Returns a decimal showing the proportion source bundles are common to
30
+ // both bundles versus the total number of source bundles.
31
+ function scoreBundleMerge(bundleA, bundleB) {
32
+ let sharedSourceBundles = 0;
33
+ let allSourceBundles = new Set([...bundleA.sourceBundles, ...bundleB.sourceBundles]);
34
+ for (let bundle of bundleB.sourceBundles) {
35
+ if (bundleA.sourceBundles.has(bundle)) {
36
+ sharedSourceBundles++;
37
+ }
38
+ }
39
+ return sharedSourceBundles / allSourceBundles.size;
40
+ }
41
+ function getMergeClusters(graph, candidates) {
42
+ let clusters = [];
43
+ for (let candidate of candidates) {
44
+ let cluster = [];
45
+ graph.traverse(nodeId => {
46
+ cluster.push((0, _nullthrows().default)(graph.getNode(nodeId)));
47
+ // Remove node from candidates as it has already been processed
48
+ candidates.delete(nodeId);
49
+ }, candidate);
50
+ clusters.push(cluster);
51
+ }
52
+ return clusters;
53
+ }
54
+ function findMergeCandidates(bundleGraph, bundles, threshold) {
55
+ let graph = new (_graph().ContentGraph)();
56
+ let seen = new Set();
57
+ let candidates = new Set();
58
+
59
+ // Build graph of clustered merge candidates
60
+ for (let bundleId of bundles) {
61
+ let bundle = bundleGraph.getNode(bundleId);
62
+ (0, _assert().default)(bundle && bundle !== 'root');
63
+ if (bundle.type !== 'js') {
64
+ continue;
65
+ }
66
+ for (let otherBundleId of bundles) {
67
+ if (bundleId === otherBundleId) {
68
+ continue;
69
+ }
70
+ let key = [bundleId, otherBundleId].sort().join(':');
71
+ if (seen.has(key)) {
72
+ continue;
73
+ }
74
+ seen.add(key);
75
+ let otherBundle = bundleGraph.getNode(otherBundleId);
76
+ (0, _assert().default)(otherBundle && otherBundle !== 'root');
77
+ let score = scoreBundleMerge(bundle, otherBundle);
78
+ if (score >= threshold) {
79
+ let bundleNode = graph.addNodeByContentKeyIfNeeded(bundleId.toString(), bundleId);
80
+ let otherBundleNode = graph.addNodeByContentKeyIfNeeded(otherBundleId.toString(), otherBundleId);
81
+
82
+ // Add edge in both directions
83
+ graph.addEdge(bundleNode, otherBundleNode);
84
+ graph.addEdge(otherBundleNode, bundleNode);
85
+ candidates.add(bundleNode);
86
+ candidates.add(otherBundleNode);
87
+ }
88
+ }
89
+ }
90
+ return getMergeClusters(graph, candidates);
91
+ }
@@ -53,14 +53,16 @@ const HTTP_OPTIONS = {
53
53
  manualSharedBundles: [],
54
54
  minBundleSize: 30000,
55
55
  maxParallelRequests: 6,
56
- disableSharedBundles: false
56
+ disableSharedBundles: false,
57
+ sharedBundleMergeThreshold: 1
57
58
  },
58
59
  '2': {
59
60
  minBundles: 1,
60
61
  manualSharedBundles: [],
61
62
  minBundleSize: 20000,
62
63
  maxParallelRequests: 25,
63
- disableSharedBundles: false
64
+ disableSharedBundles: false,
65
+ sharedBundleMergeThreshold: 1
64
66
  }
65
67
  };
66
68
  const CONFIG_SCHEMA = {
@@ -115,6 +117,9 @@ const CONFIG_SCHEMA = {
115
117
  },
116
118
  loadConditionalBundlesInParallel: {
117
119
  type: 'boolean'
120
+ },
121
+ sharedBundleMergeThreshold: {
122
+ type: 'number'
118
123
  }
119
124
  },
120
125
  additionalProperties: false
@@ -172,6 +177,7 @@ async function loadBundlerConfig(config, options, logger) {
172
177
  return {
173
178
  minBundles: modeConfig.minBundles ?? defaults.minBundles,
174
179
  minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
180
+ sharedBundleMergeThreshold: modeConfig.sharedBundleMergeThreshold ?? defaults.sharedBundleMergeThreshold,
175
181
  maxParallelRequests: modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
176
182
  projectRoot: options.projectRoot,
177
183
  disableSharedBundles: modeConfig.disableSharedBundles ?? defaults.disableSharedBundles,
package/lib/idealGraph.js CHANGED
@@ -47,6 +47,7 @@ function _nullthrows() {
47
47
  };
48
48
  return data;
49
49
  }
50
+ var _bundleMerge = require("./bundleMerge");
50
51
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
51
52
  /* BundleRoot - An asset that is the main entry of a Bundle. */
52
53
  const dependencyPriorityEdges = {
@@ -848,6 +849,12 @@ function createIdealGraph(assetGraph, config, entries, logger) {
848
849
  }
849
850
  }
850
851
 
852
+ // Step merge shared bundles that meet the overlap threshold
853
+ // This step is skipped by default as the threshold defaults to 1
854
+ if (config.sharedBundleMergeThreshold < 1) {
855
+ mergeOverlapBundles();
856
+ }
857
+
851
858
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
852
859
  // their source bundles, and remove the bundle.
853
860
  // We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained
@@ -857,9 +864,9 @@ function createIdealGraph(assetGraph, config, entries, logger) {
857
864
  removeBundle(bundleGraph, bundleNodeId, assetReference);
858
865
  }
859
866
  }
860
- let modifiedSourceBundles = new Set();
861
867
 
862
868
  // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
869
+ let modifiedSourceBundles = new Set();
863
870
  if (config.disableSharedBundles === false) {
864
871
  for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
865
872
  // Find shared bundles in this bundle group.
@@ -944,6 +951,61 @@ function createIdealGraph(assetGraph, config, entries, logger) {
944
951
  }
945
952
  }
946
953
  }
954
+ function mergeBundles(bundleGraph, bundleToKeepId, bundleToRemoveId, assetReference) {
955
+ let bundleToKeep = (0, _nullthrows().default)(bundleGraph.getNode(bundleToKeepId), '18');
956
+ let bundleToRemove = (0, _nullthrows().default)(bundleGraph.getNode(bundleToRemoveId), '19');
957
+ (0, _assert().default)(bundleToKeep !== 'root' && bundleToRemove !== 'root');
958
+ for (let asset of bundleToRemove.assets) {
959
+ bundleToKeep.assets.add(asset);
960
+ bundleToKeep.size += asset.stats.size;
961
+ let newAssetReference = assetReference.get(asset).map(([dep, bundle]) => bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle]);
962
+ assetReference.set(asset, newAssetReference);
963
+ }
964
+ for (let sourceBundleId of bundleToRemove.sourceBundles) {
965
+ if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
966
+ continue;
967
+ }
968
+ bundleToKeep.sourceBundles.add(sourceBundleId);
969
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
970
+ }
971
+
972
+ // Merge any internalized assets
973
+ if (bundleToRemove.internalizedAssets) {
974
+ if (bundleToKeep.internalizedAssets) {
975
+ bundleToKeep.internalizedAssets.union(bundleToRemove.internalizedAssets);
976
+ } else {
977
+ bundleToKeep.internalizedAssets = bundleToRemove.internalizedAssets;
978
+ }
979
+ }
980
+ bundleGraph.removeNode(bundleToRemoveId);
981
+ }
982
+ function mergeOverlapBundles() {
983
+ // Find all shared bundles
984
+ let sharedBundles = new Set();
985
+ bundleGraph.traverse(nodeId => {
986
+ let bundle = bundleGraph.getNode(nodeId);
987
+ if (!bundle) {
988
+ throw new Error(`Unable to find bundle ${nodeId} in bundle graph`);
989
+ }
990
+ if (bundle === 'root') {
991
+ return;
992
+ }
993
+
994
+ // Only consider JS shared bundles and non-reused bundles.
995
+ // These count potentially be considered for merging in future but they're
996
+ // more complicated to merge
997
+ if (bundle.sourceBundles.size > 0 && bundle.manualSharedBundle == null && !bundle.mainEntryAsset && bundle.type === 'js') {
998
+ sharedBundles.add(nodeId);
999
+ }
1000
+ });
1001
+ let clusters = (0, _bundleMerge.findMergeCandidates)(bundleGraph, Array.from(sharedBundles), config.sharedBundleMergeThreshold);
1002
+ for (let cluster of clusters) {
1003
+ let [mergeTarget, ...rest] = cluster;
1004
+ for (let bundleIdToMerge of rest) {
1005
+ mergeBundles(bundleGraph, mergeTarget, bundleIdToMerge, assetReference);
1006
+ }
1007
+ }
1008
+ }
947
1009
  function getBigIntFromContentKey(contentKey) {
948
1010
  let b = Buffer.alloc(64);
949
1011
  b.write(contentKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaspack/bundler-default",
3
- "version": "2.14.5-canary.6+b5da6b749",
3
+ "version": "2.14.5-canary.8+46a90dccd",
4
4
  "license": "(MIT OR Apache-2.0)",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {
@@ -16,13 +16,13 @@
16
16
  "node": ">= 16.0.0"
17
17
  },
18
18
  "dependencies": {
19
- "@atlaspack/diagnostic": "2.14.1-canary.74+b5da6b749",
20
- "@atlaspack/feature-flags": "2.14.1-canary.74+b5da6b749",
21
- "@atlaspack/graph": "3.4.1-canary.74+b5da6b749",
22
- "@atlaspack/plugin": "2.14.5-canary.6+b5da6b749",
23
- "@atlaspack/rust": "3.2.1-canary.6+b5da6b749",
24
- "@atlaspack/utils": "2.14.5-canary.6+b5da6b749",
19
+ "@atlaspack/diagnostic": "2.14.1-canary.76+46a90dccd",
20
+ "@atlaspack/feature-flags": "2.14.1-canary.76+46a90dccd",
21
+ "@atlaspack/graph": "3.4.1-canary.76+46a90dccd",
22
+ "@atlaspack/plugin": "2.14.5-canary.8+46a90dccd",
23
+ "@atlaspack/rust": "3.2.1-canary.8+46a90dccd",
24
+ "@atlaspack/utils": "2.14.5-canary.8+46a90dccd",
25
25
  "nullthrows": "^1.1.1"
26
26
  },
27
- "gitHead": "b5da6b749ddb23cfc212a640df1f07850da8307f"
27
+ "gitHead": "46a90dccd019a26b222c878a92d23acc75dc67c5"
28
28
  }
@@ -0,0 +1,103 @@
1
+ // @flow strict-local
2
+
3
+ import invariant from 'assert';
4
+ import nullthrows from 'nullthrows';
5
+ import type {NodeId} from '@atlaspack/graph';
6
+ import type {Bundle, IdealBundleGraph} from './idealGraph';
7
+ import {ContentGraph} from '@atlaspack/graph';
8
+
9
+ // Returns a decimal showing the proportion source bundles are common to
10
+ // both bundles versus the total number of source bundles.
11
+ function scoreBundleMerge(bundleA: Bundle, bundleB: Bundle): number {
12
+ let sharedSourceBundles = 0;
13
+ let allSourceBundles = new Set([
14
+ ...bundleA.sourceBundles,
15
+ ...bundleB.sourceBundles,
16
+ ]);
17
+
18
+ for (let bundle of bundleB.sourceBundles) {
19
+ if (bundleA.sourceBundles.has(bundle)) {
20
+ sharedSourceBundles++;
21
+ }
22
+ }
23
+
24
+ return sharedSourceBundles / allSourceBundles.size;
25
+ }
26
+
27
+ function getMergeClusters(
28
+ graph: ContentGraph<NodeId>,
29
+ candidates: Set<NodeId>,
30
+ ): Array<Array<NodeId>> {
31
+ let clusters = [];
32
+
33
+ for (let candidate of candidates) {
34
+ let cluster: Array<NodeId> = [];
35
+
36
+ graph.traverse((nodeId) => {
37
+ cluster.push(nullthrows(graph.getNode(nodeId)));
38
+ // Remove node from candidates as it has already been processed
39
+ candidates.delete(nodeId);
40
+ }, candidate);
41
+
42
+ clusters.push(cluster);
43
+ }
44
+
45
+ return clusters;
46
+ }
47
+
48
+ export function findMergeCandidates(
49
+ bundleGraph: IdealBundleGraph,
50
+ bundles: Array<NodeId>,
51
+ threshold: number,
52
+ ): Array<Array<NodeId>> {
53
+ let graph = new ContentGraph<NodeId>();
54
+ let seen = new Set<string>();
55
+ let candidates = new Set<NodeId>();
56
+
57
+ // Build graph of clustered merge candidates
58
+ for (let bundleId of bundles) {
59
+ let bundle = bundleGraph.getNode(bundleId);
60
+ invariant(bundle && bundle !== 'root');
61
+ if (bundle.type !== 'js') {
62
+ continue;
63
+ }
64
+
65
+ for (let otherBundleId of bundles) {
66
+ if (bundleId === otherBundleId) {
67
+ continue;
68
+ }
69
+
70
+ let key = [bundleId, otherBundleId].sort().join(':');
71
+
72
+ if (seen.has(key)) {
73
+ continue;
74
+ }
75
+ seen.add(key);
76
+
77
+ let otherBundle = bundleGraph.getNode(otherBundleId);
78
+ invariant(otherBundle && otherBundle !== 'root');
79
+
80
+ let score = scoreBundleMerge(bundle, otherBundle);
81
+
82
+ if (score >= threshold) {
83
+ let bundleNode = graph.addNodeByContentKeyIfNeeded(
84
+ bundleId.toString(),
85
+ bundleId,
86
+ );
87
+ let otherBundleNode = graph.addNodeByContentKeyIfNeeded(
88
+ otherBundleId.toString(),
89
+ otherBundleId,
90
+ );
91
+
92
+ // Add edge in both directions
93
+ graph.addEdge(bundleNode, otherBundleNode);
94
+ graph.addEdge(otherBundleNode, bundleNode);
95
+
96
+ candidates.add(bundleNode);
97
+ candidates.add(otherBundleNode);
98
+ }
99
+ }
100
+ }
101
+
102
+ return getMergeClusters(graph, candidates);
103
+ }
@@ -28,6 +28,7 @@ type BaseBundlerConfig = {|
28
28
  disableSharedBundles?: boolean,
29
29
  manualSharedBundles?: ManualSharedBundles,
30
30
  loadConditionalBundlesInParallel?: boolean,
31
+ sharedBundleMergeThreshold?: number,
31
32
  |};
32
33
 
33
34
  type BundlerConfig = {|
@@ -42,6 +43,7 @@ export type ResolvedBundlerConfig = {|
42
43
  disableSharedBundles: boolean,
43
44
  manualSharedBundles: ManualSharedBundles,
44
45
  loadConditionalBundlesInParallel?: boolean,
46
+ sharedBundleMergeThreshold: number,
45
47
  |};
46
48
 
47
49
  function resolveModeConfig(
@@ -76,6 +78,7 @@ const HTTP_OPTIONS = {
76
78
  minBundleSize: 30000,
77
79
  maxParallelRequests: 6,
78
80
  disableSharedBundles: false,
81
+ sharedBundleMergeThreshold: 1,
79
82
  },
80
83
  '2': {
81
84
  minBundles: 1,
@@ -83,6 +86,7 @@ const HTTP_OPTIONS = {
83
86
  minBundleSize: 20000,
84
87
  maxParallelRequests: 25,
85
88
  disableSharedBundles: false,
89
+ sharedBundleMergeThreshold: 1,
86
90
  },
87
91
  };
88
92
 
@@ -139,6 +143,9 @@ const CONFIG_SCHEMA: SchemaEntity = {
139
143
  loadConditionalBundlesInParallel: {
140
144
  type: 'boolean',
141
145
  },
146
+ sharedBundleMergeThreshold: {
147
+ type: 'number',
148
+ },
142
149
  },
143
150
  additionalProperties: false,
144
151
  };
@@ -224,6 +231,9 @@ export async function loadBundlerConfig(
224
231
  return {
225
232
  minBundles: modeConfig.minBundles ?? defaults.minBundles,
226
233
  minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
234
+ sharedBundleMergeThreshold:
235
+ modeConfig.sharedBundleMergeThreshold ??
236
+ defaults.sharedBundleMergeThreshold,
227
237
  maxParallelRequests:
228
238
  modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
229
239
  projectRoot: options.projectRoot,
package/src/idealGraph.js CHANGED
@@ -23,6 +23,7 @@ import {DefaultMap, globToRegex} from '@atlaspack/utils';
23
23
  import invariant from 'assert';
24
24
  import nullthrows from 'nullthrows';
25
25
 
26
+ import {findMergeCandidates} from './bundleMerge';
26
27
  import type {ResolvedBundlerConfig} from './bundlerConfig';
27
28
 
28
29
  /* BundleRoot - An asset that is the main entry of a Bundle. */
@@ -67,7 +68,7 @@ export const idealBundleGraphEdges = Object.freeze({
67
68
  conditional: 2,
68
69
  });
69
70
 
70
- type IdealBundleGraph = Graph<
71
+ export type IdealBundleGraph = Graph<
71
72
  Bundle | 'root',
72
73
  $Values<typeof idealBundleGraphEdges>,
73
74
  >;
@@ -1128,6 +1129,12 @@ export function createIdealGraph(
1128
1129
  }
1129
1130
  }
1130
1131
 
1132
+ // Step merge shared bundles that meet the overlap threshold
1133
+ // This step is skipped by default as the threshold defaults to 1
1134
+ if (config.sharedBundleMergeThreshold < 1) {
1135
+ mergeOverlapBundles();
1136
+ }
1137
+
1131
1138
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
1132
1139
  // their source bundles, and remove the bundle.
1133
1140
  // We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained
@@ -1143,9 +1150,9 @@ export function createIdealGraph(
1143
1150
  }
1144
1151
  }
1145
1152
 
1153
+ // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
1146
1154
  let modifiedSourceBundles = new Set();
1147
1155
 
1148
- // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
1149
1156
  if (config.disableSharedBundles === false) {
1150
1157
  for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
1151
1158
  // Find shared bundles in this bundle group.
@@ -1249,6 +1256,96 @@ export function createIdealGraph(
1249
1256
  }
1250
1257
  }
1251
1258
 
1259
+ function mergeBundles(
1260
+ bundleGraph: IdealBundleGraph,
1261
+ bundleToKeepId: NodeId,
1262
+ bundleToRemoveId: NodeId,
1263
+ assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>,
1264
+ ) {
1265
+ let bundleToKeep = nullthrows(bundleGraph.getNode(bundleToKeepId), '18');
1266
+ let bundleToRemove = nullthrows(
1267
+ bundleGraph.getNode(bundleToRemoveId),
1268
+ '19',
1269
+ );
1270
+ invariant(bundleToKeep !== 'root' && bundleToRemove !== 'root');
1271
+ for (let asset of bundleToRemove.assets) {
1272
+ bundleToKeep.assets.add(asset);
1273
+ bundleToKeep.size += asset.stats.size;
1274
+
1275
+ let newAssetReference = assetReference
1276
+ .get(asset)
1277
+ .map(([dep, bundle]) =>
1278
+ bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle],
1279
+ );
1280
+
1281
+ assetReference.set(asset, newAssetReference);
1282
+ }
1283
+
1284
+ for (let sourceBundleId of bundleToRemove.sourceBundles) {
1285
+ if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
1286
+ continue;
1287
+ }
1288
+
1289
+ bundleToKeep.sourceBundles.add(sourceBundleId);
1290
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1291
+ }
1292
+
1293
+ // Merge any internalized assets
1294
+ if (bundleToRemove.internalizedAssets) {
1295
+ if (bundleToKeep.internalizedAssets) {
1296
+ bundleToKeep.internalizedAssets.union(
1297
+ bundleToRemove.internalizedAssets,
1298
+ );
1299
+ } else {
1300
+ bundleToKeep.internalizedAssets = bundleToRemove.internalizedAssets;
1301
+ }
1302
+ }
1303
+
1304
+ bundleGraph.removeNode(bundleToRemoveId);
1305
+ }
1306
+
1307
+ function mergeOverlapBundles() {
1308
+ // Find all shared bundles
1309
+ let sharedBundles = new Set<NodeId>();
1310
+ bundleGraph.traverse((nodeId) => {
1311
+ let bundle = bundleGraph.getNode(nodeId);
1312
+
1313
+ if (!bundle) {
1314
+ throw new Error(`Unable to find bundle ${nodeId} in bundle graph`);
1315
+ }
1316
+
1317
+ if (bundle === 'root') {
1318
+ return;
1319
+ }
1320
+
1321
+ // Only consider JS shared bundles and non-reused bundles.
1322
+ // These count potentially be considered for merging in future but they're
1323
+ // more complicated to merge
1324
+ if (
1325
+ bundle.sourceBundles.size > 0 &&
1326
+ bundle.manualSharedBundle == null &&
1327
+ !bundle.mainEntryAsset &&
1328
+ bundle.type === 'js'
1329
+ ) {
1330
+ sharedBundles.add(nodeId);
1331
+ }
1332
+ });
1333
+
1334
+ let clusters = findMergeCandidates(
1335
+ bundleGraph,
1336
+ Array.from(sharedBundles),
1337
+ config.sharedBundleMergeThreshold,
1338
+ );
1339
+
1340
+ for (let cluster of clusters) {
1341
+ let [mergeTarget, ...rest] = cluster;
1342
+
1343
+ for (let bundleIdToMerge of rest) {
1344
+ mergeBundles(bundleGraph, mergeTarget, bundleIdToMerge, assetReference);
1345
+ }
1346
+ }
1347
+ }
1348
+
1252
1349
  function getBigIntFromContentKey(contentKey) {
1253
1350
  let b = Buffer.alloc(64);
1254
1351
  b.write(contentKey);