@atlaspack/core 2.16.2-canary.13 → 2.16.2-canary.131

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +300 -0
  2. package/index.d.ts +4 -0
  3. package/lib/AssetGraph.js +27 -7
  4. package/lib/Atlaspack.js +35 -27
  5. package/lib/AtlaspackConfig.schema.js +7 -1
  6. package/lib/BundleGraph.js +8 -5
  7. package/lib/Dependency.js +6 -2
  8. package/lib/Environment.js +5 -3
  9. package/lib/EnvironmentManager.js +137 -0
  10. package/lib/InternalConfig.js +3 -2
  11. package/lib/PackagerRunner.js +54 -16
  12. package/lib/RequestTracker.js +345 -132
  13. package/lib/SymbolPropagation.js +14 -0
  14. package/lib/Transformation.js +2 -2
  15. package/lib/UncommittedAsset.js +20 -2
  16. package/lib/applyRuntimes.js +2 -1
  17. package/lib/assetUtils.js +2 -1
  18. package/lib/atlaspack-v3/AtlaspackV3.js +16 -3
  19. package/lib/atlaspack-v3/worker/compat/environment.js +2 -2
  20. package/lib/atlaspack-v3/worker/compat/mutable-asset.js +6 -6
  21. package/lib/atlaspack-v3/worker/compat/plugin-config.js +5 -5
  22. package/lib/atlaspack-v3/worker/index.js +3 -0
  23. package/lib/atlaspack-v3/worker/worker.js +8 -0
  24. package/lib/dumpGraphToGraphViz.js +1 -1
  25. package/lib/index.js +29 -1
  26. package/lib/public/Asset.js +7 -9
  27. package/lib/public/Bundle.js +12 -13
  28. package/lib/public/BundleGraph.js +3 -2
  29. package/lib/public/BundleGroup.js +2 -3
  30. package/lib/public/Config.js +95 -8
  31. package/lib/public/Dependency.js +4 -4
  32. package/lib/public/Environment.js +2 -3
  33. package/lib/public/MutableBundleGraph.js +5 -4
  34. package/lib/public/PluginOptions.js +1 -2
  35. package/lib/public/Target.js +4 -4
  36. package/lib/requests/AssetGraphRequest.js +13 -1
  37. package/lib/requests/AssetGraphRequestRust.js +17 -2
  38. package/lib/requests/AssetRequest.js +2 -1
  39. package/lib/requests/BundleGraphRequest.js +13 -1
  40. package/lib/requests/ConfigRequest.js +27 -4
  41. package/lib/requests/DevDepRequest.js +11 -1
  42. package/lib/requests/PathRequest.js +10 -0
  43. package/lib/requests/TargetRequest.js +18 -16
  44. package/lib/requests/WriteBundleRequest.js +15 -3
  45. package/lib/requests/WriteBundlesRequest.js +22 -1
  46. package/lib/resolveOptions.js +7 -4
  47. package/lib/worker.js +18 -1
  48. package/package.json +18 -25
  49. package/src/AssetGraph.js +30 -7
  50. package/src/Atlaspack.js +40 -23
  51. package/src/BundleGraph.js +13 -8
  52. package/src/Dependency.js +13 -5
  53. package/src/Environment.js +9 -6
  54. package/src/EnvironmentManager.js +145 -0
  55. package/src/InternalConfig.js +6 -5
  56. package/src/PackagerRunner.js +72 -20
  57. package/src/RequestTracker.js +526 -157
  58. package/src/SymbolPropagation.js +13 -1
  59. package/src/UncommittedAsset.js +23 -3
  60. package/src/applyRuntimes.js +6 -1
  61. package/src/assetUtils.js +4 -3
  62. package/src/atlaspack-v3/AtlaspackV3.js +24 -3
  63. package/src/atlaspack-v3/worker/compat/plugin-config.js +9 -5
  64. package/src/atlaspack-v3/worker/index.js +2 -1
  65. package/src/atlaspack-v3/worker/worker.js +7 -0
  66. package/src/index.js +5 -1
  67. package/src/public/Asset.js +13 -6
  68. package/src/public/Bundle.js +12 -11
  69. package/src/public/BundleGraph.js +10 -2
  70. package/src/public/BundleGroup.js +2 -2
  71. package/src/public/Config.js +132 -18
  72. package/src/public/Dependency.js +4 -3
  73. package/src/public/Environment.js +2 -2
  74. package/src/public/MutableBundleGraph.js +8 -5
  75. package/src/public/PluginOptions.js +1 -1
  76. package/src/public/Target.js +4 -3
  77. package/src/requests/AssetGraphRequest.js +13 -3
  78. package/src/requests/AssetGraphRequestRust.js +14 -2
  79. package/src/requests/AssetRequest.js +2 -1
  80. package/src/requests/BundleGraphRequest.js +13 -3
  81. package/src/requests/ConfigRequest.js +33 -9
  82. package/src/requests/DevDepRequest.js +22 -9
  83. package/src/requests/PathRequest.js +4 -0
  84. package/src/requests/TargetRequest.js +19 -25
  85. package/src/requests/WriteBundleRequest.js +14 -8
  86. package/src/requests/WriteBundlesRequest.js +31 -3
  87. package/src/resolveOptions.js +4 -2
  88. package/src/types.js +10 -7
  89. package/test/Environment.test.js +43 -34
  90. package/test/EnvironmentManager.test.js +192 -0
  91. package/test/PublicEnvironment.test.js +10 -7
  92. package/test/RequestTracker.test.js +124 -29
  93. package/test/public/Config.test.js +108 -0
  94. package/test/requests/ConfigRequest.test.js +199 -7
  95. package/test/test-utils.js +4 -9
@@ -30,15 +30,15 @@ import nullthrows from 'nullthrows';
30
30
 
31
31
  import {
32
32
  ATLASPACK_VERSION,
33
- VALID,
34
- INITIAL_BUILD,
35
33
  FILE_CREATE,
36
- FILE_UPDATE,
37
34
  FILE_DELETE,
35
+ FILE_UPDATE,
38
36
  ENV_CHANGE,
37
+ ERROR,
38
+ INITIAL_BUILD,
39
39
  OPTION_CHANGE,
40
40
  STARTUP,
41
- ERROR,
41
+ VALID,
42
42
  } from './constants';
43
43
  import type {AtlaspackV3} from './atlaspack-v3/AtlaspackV3';
44
44
  import {
@@ -68,7 +68,13 @@ import type {
68
68
  InternalFileCreateInvalidation,
69
69
  InternalGlob,
70
70
  } from './types';
71
- import {BuildAbortError, assertSignalNotAborted, hashFromOption} from './utils';
71
+ import {BuildAbortError, hashFromOption} from './utils';
72
+ import {performance} from 'perf_hooks';
73
+
74
+ import {
75
+ loadEnvironmentsFromCache,
76
+ writeEnvironmentsToCache,
77
+ } from './EnvironmentManager';
72
78
 
73
79
  export const requestGraphEdgeTypes = {
74
80
  subrequest: 2,
@@ -143,7 +149,7 @@ type OptionNode = {|
143
149
  type ConfigKeyNode = {|
144
150
  id: ContentKey,
145
151
  +type: typeof CONFIG_KEY,
146
- configKey: string,
152
+ configKey: string[],
147
153
  contentHash: string,
148
154
  |};
149
155
 
@@ -215,7 +221,7 @@ export type RunAPI<TResult: RequestResult> = {|
215
221
  invalidateOnFileUpdate: (ProjectPath) => void,
216
222
  invalidateOnConfigKeyChange: (
217
223
  filePath: ProjectPath,
218
- configKey: string,
224
+ configKey: string[],
219
225
  contentHash: string,
220
226
  ) => void,
221
227
  invalidateOnStartup: () => void,
@@ -282,10 +288,12 @@ const nodeFromOption = (option: string, value: mixed): RequestGraphNode => ({
282
288
 
283
289
  const nodeFromConfigKey = (
284
290
  fileName: ProjectPath,
285
- configKey: string,
291
+ configKey: string[],
286
292
  contentHash: string,
287
293
  ): RequestGraphNode => ({
288
- id: `config_key:${fromProjectPathRelative(fileName)}:${configKey}`,
294
+ id: `config_key:${fromProjectPathRelative(fileName)}:${JSON.stringify(
295
+ configKey,
296
+ )}`,
289
297
  type: CONFIG_KEY,
290
298
  configKey,
291
299
  contentHash,
@@ -446,6 +454,11 @@ export class RequestGraph extends ContentGraph<
446
454
  this.removeCachedRequestChunkForNode(nodeId);
447
455
  }
448
456
 
457
+ /**
458
+ * Nodes that are invalidated on start-up, such as JavaScript babel configuration files which are
459
+ * imported when the build kicks-off and might doing arbitrary work such as reading from the file
460
+ * system.
461
+ */
449
462
  invalidateUnpredictableNodes() {
450
463
  for (let nodeId of this.unpredicatableNodeIds) {
451
464
  let node = nullthrows(this.getNode(nodeId));
@@ -454,6 +467,9 @@ export class RequestGraph extends ContentGraph<
454
467
  }
455
468
  }
456
469
 
470
+ /**
471
+ * Effectively uncacheable nodes.
472
+ */
457
473
  invalidateOnBuildNodes() {
458
474
  for (let nodeId of this.invalidateOnBuildNodeIds) {
459
475
  let node = nullthrows(this.getNode(nodeId));
@@ -462,11 +478,20 @@ export class RequestGraph extends ContentGraph<
462
478
  }
463
479
  }
464
480
 
465
- invalidateEnvNodes(env: EnvMap) {
481
+ /**
482
+ * Nodes invalidated by environment changes, corresponds to `env: ...` inputs.
483
+ */
484
+ invalidateEnvNodes(env: EnvMap): string[] {
485
+ const invalidatedKeys = [];
486
+
466
487
  for (let nodeId of this.envNodeIds) {
467
488
  let node = nullthrows(this.getNode(nodeId));
468
489
  invariant(node.type === ENV);
469
- if (env[keyFromEnvContentKey(node.id)] !== node.value) {
490
+
491
+ const key = keyFromEnvContentKey(node.id);
492
+ if (env[key] !== node.value) {
493
+ invalidatedKeys.push(key);
494
+
470
495
  let parentNodes = this.getNodeIdsConnectedTo(
471
496
  nodeId,
472
497
  requestGraphEdgeTypes.invalidated_by_update,
@@ -476,15 +501,23 @@ export class RequestGraph extends ContentGraph<
476
501
  }
477
502
  }
478
503
  }
504
+
505
+ return invalidatedKeys;
479
506
  }
480
507
 
481
- invalidateOptionNodes(options: AtlaspackOptions) {
508
+ /**
509
+ * Nodes invalidated by option changes.
510
+ */
511
+ invalidateOptionNodes(options: AtlaspackOptions): string[] {
512
+ const invalidatedKeys = [];
513
+
482
514
  for (let nodeId of this.optionNodeIds) {
483
515
  let node = nullthrows(this.getNode(nodeId));
484
516
  invariant(node.type === OPTION);
485
- if (
486
- hashFromOption(options[keyFromOptionContentKey(node.id)]) !== node.hash
487
- ) {
517
+ const key = keyFromOptionContentKey(node.id);
518
+
519
+ if (hashFromOption(options[key]) !== node.hash) {
520
+ invalidatedKeys.push(key);
488
521
  let parentNodes = this.getNodeIdsConnectedTo(
489
522
  nodeId,
490
523
  requestGraphEdgeTypes.invalidated_by_update,
@@ -494,12 +527,14 @@ export class RequestGraph extends ContentGraph<
494
527
  }
495
528
  }
496
529
  }
530
+
531
+ return invalidatedKeys;
497
532
  }
498
533
 
499
534
  invalidateOnConfigKeyChange(
500
535
  requestNodeId: NodeId,
501
536
  filePath: ProjectPath,
502
- configKey: string,
537
+ configKey: string[],
503
538
  contentHash: string,
504
539
  ) {
505
540
  let configKeyNodeId = this.addNode(
@@ -686,8 +721,8 @@ export class RequestGraph extends ContentGraph<
686
721
  env: string,
687
722
  value: string | void,
688
723
  ) {
689
- let envNode = nodeFromEnv(env, value);
690
- let envNodeId = this.addNode(envNode);
724
+ const envNode = nodeFromEnv(env, value);
725
+ const envNodeId = this.addNode(envNode);
691
726
 
692
727
  if (
693
728
  !this.hasEdge(
@@ -907,7 +942,10 @@ export class RequestGraph extends ContentGraph<
907
942
  * True if this is the start-up (loading phase) invalidation.
908
943
  */
909
944
  isInitialBuild: boolean = false,
910
- ): Async<boolean> {
945
+ ): Promise<{|
946
+ didInvalidate: boolean,
947
+ invalidationsByPath: Map<string, number>,
948
+ |}> {
911
949
  let didInvalidate = false;
912
950
  let count = 0;
913
951
  let predictedTime = 0;
@@ -945,7 +983,10 @@ export class RequestGraph extends ContentGraph<
945
983
  return above;
946
984
  };
947
985
 
986
+ const invalidationsByPath = new Map();
948
987
  for (let {path: _path, type} of events) {
988
+ const invalidationsBefore = this.getInvalidNodeCount();
989
+
949
990
  if (
950
991
  !enableOptimization &&
951
992
  process.env.ATLASPACK_DISABLE_CACHE_TIMEOUT !== 'true' &&
@@ -991,7 +1032,10 @@ export class RequestGraph extends ContentGraph<
991
1032
  this.invalidNodeIds.add(id);
992
1033
  }
993
1034
  }
994
- return true;
1035
+ return {
1036
+ didInvalidate: true,
1037
+ invalidationsByPath: new Map(),
1038
+ };
995
1039
  }
996
1040
 
997
1041
  // sometimes mac os reports update events as create events.
@@ -1072,11 +1116,18 @@ export class RequestGraph extends ContentGraph<
1072
1116
  }
1073
1117
 
1074
1118
  let configKeyNodes = this.configKeyNodes.get(_filePath);
1075
- if (configKeyNodes && (type === 'delete' || type === 'update')) {
1119
+
1120
+ // With granular invalidations we will always run this block,
1121
+ // so even if we get a create event (for whatever reason), we will still
1122
+ // try to limit invalidations from config key changes through hashing.
1123
+ //
1124
+ // Currently create events can invalidate a large number of nodes due to
1125
+ // "create above" invalidations.
1126
+ if (configKeyNodes) {
1076
1127
  for (let nodeId of configKeyNodes) {
1077
1128
  let isInvalid = type === 'delete';
1078
1129
 
1079
- if (type === 'update') {
1130
+ if (type !== 'delete') {
1080
1131
  let node = this.getNode(nodeId);
1081
1132
  invariant(node && node.type === CONFIG_KEY);
1082
1133
 
@@ -1104,6 +1155,13 @@ export class RequestGraph extends ContentGraph<
1104
1155
  }
1105
1156
  }
1106
1157
  }
1158
+
1159
+ const invalidationsAfter = this.getInvalidNodeCount();
1160
+ const invalidationsForEvent = invalidationsAfter - invalidationsBefore;
1161
+ invalidationsByPath.set(
1162
+ _path,
1163
+ (invalidationsByPath.get(_path) ?? 0) + invalidationsForEvent,
1164
+ );
1107
1165
  }
1108
1166
 
1109
1167
  if (getFeatureFlag('fixQuadraticCacheInvalidation')) {
@@ -1124,7 +1182,10 @@ export class RequestGraph extends ContentGraph<
1124
1182
  },
1125
1183
  });
1126
1184
 
1127
- return didInvalidate && this.invalidNodeIds.size > 0;
1185
+ return {
1186
+ didInvalidate,
1187
+ invalidationsByPath,
1188
+ };
1128
1189
  }
1129
1190
 
1130
1191
  hasCachedRequestChunk(index: number): boolean {
@@ -1138,6 +1199,13 @@ export class RequestGraph extends ContentGraph<
1138
1199
  removeCachedRequestChunkForNode(nodeId: number): void {
1139
1200
  this.cachedRequestChunks.delete(Math.floor(nodeId / this.nodesPerBlob));
1140
1201
  }
1202
+
1203
+ /**
1204
+ * Returns the number of invalidated nodes in the graph.
1205
+ */
1206
+ getInvalidNodeCount(): number {
1207
+ return this.invalidNodeIds.size;
1208
+ }
1141
1209
  }
1142
1210
 
1143
1211
  export default class RequestTracker {
@@ -1145,7 +1213,6 @@ export default class RequestTracker {
1145
1213
  farm: WorkerFarm;
1146
1214
  options: AtlaspackOptions;
1147
1215
  rustAtlaspack: ?AtlaspackV3;
1148
- signal: ?AbortSignal;
1149
1216
  stats: Map<RequestType, number> = new Map();
1150
1217
 
1151
1218
  constructor({
@@ -1165,11 +1232,6 @@ export default class RequestTracker {
1165
1232
  this.rustAtlaspack = rustAtlaspack;
1166
1233
  }
1167
1234
 
1168
- // TODO: refactor (abortcontroller should be created by RequestTracker)
1169
- setSignal(signal?: AbortSignal) {
1170
- this.signal = signal;
1171
- }
1172
-
1173
1235
  startRequest(request: RequestNode): {|
1174
1236
  requestNodeId: NodeId,
1175
1237
  deferred: Deferred<boolean>,
@@ -1261,7 +1323,13 @@ export default class RequestTracker {
1261
1323
  }
1262
1324
  }
1263
1325
 
1264
- respondToFSEvents(events: Array<Event>, threshold: number): Async<boolean> {
1326
+ respondToFSEvents(
1327
+ events: Array<Event>,
1328
+ threshold: number,
1329
+ ): Promise<{|
1330
+ didInvalidate: boolean,
1331
+ invalidationsByPath: Map<string, number>,
1332
+ |}> {
1265
1333
  return this.graph.respondToFSEvents(events, this.options, threshold);
1266
1334
  }
1267
1335
 
@@ -1344,7 +1412,6 @@ export default class RequestTracker {
1344
1412
  rustAtlaspack: this.rustAtlaspack,
1345
1413
  });
1346
1414
 
1347
- assertSignalNotAborted(this.signal);
1348
1415
  this.completeRequest(requestNodeId);
1349
1416
 
1350
1417
  deferred.resolve(true);
@@ -1472,130 +1539,143 @@ export default class RequestTracker {
1472
1539
  }
1473
1540
  }
1474
1541
 
1542
+ let cacheKey = getCacheKey(this.options);
1543
+ let requestGraphKey = getFeatureFlag('cachePerformanceImprovements')
1544
+ ? `${cacheKey}/RequestGraph`
1545
+ : `requestGraph-${cacheKey}`;
1546
+ let snapshotKey = getFeatureFlag('cachePerformanceImprovements')
1547
+ ? `${cacheKey}/snapshot`
1548
+ : `snapshot-${cacheKey}`;
1549
+
1550
+ if (this.options.shouldDisableCache) {
1551
+ return;
1552
+ }
1553
+
1554
+ let total = 0;
1475
1555
  await runCacheImprovements(
1476
1556
  async (cache) => {
1477
1557
  await cache.getNativeRef().startWriteTransaction();
1478
1558
  },
1479
1559
  () => Promise.resolve(),
1480
1560
  );
1561
+ try {
1562
+ report({
1563
+ type: 'cache',
1564
+ phase: 'start',
1565
+ total,
1566
+ size: this.graph.nodes.length,
1567
+ });
1481
1568
 
1482
- let cacheKey = getCacheKey(this.options);
1483
- let requestGraphKey = `requestGraph-${cacheKey}`;
1484
- let snapshotKey = `snapshot-${cacheKey}`;
1485
-
1486
- if (this.options.shouldDisableCache) {
1487
- return;
1488
- }
1569
+ if (getFeatureFlag('environmentDeduplication')) {
1570
+ await writeEnvironmentsToCache(options.cache);
1571
+ }
1489
1572
 
1490
- let total = 0;
1491
- report({
1492
- type: 'cache',
1493
- phase: 'start',
1494
- total,
1495
- size: this.graph.nodes.length,
1496
- });
1573
+ let serialisedGraph = this.graph.serialize();
1497
1574
 
1498
- let serialisedGraph = this.graph.serialize();
1575
+ // Delete an existing request graph cache, to prevent invalid states
1576
+ await this.options.cache.deleteLargeBlob(requestGraphKey);
1499
1577
 
1500
- // Delete an existing request graph cache, to prevent invalid states
1501
- await this.options.cache.deleteLargeBlob(requestGraphKey);
1578
+ const serialiseAndSet = async (
1579
+ key: string,
1580
+ // $FlowFixMe serialise input is any type
1581
+ contents: any,
1582
+ ): Promise<void> => {
1583
+ if (signal?.aborted) {
1584
+ throw new Error('Serialization was aborted');
1585
+ }
1502
1586
 
1503
- const serialiseAndSet = async (
1504
- key: string,
1505
- // $FlowFixMe serialise input is any type
1506
- contents: any,
1507
- ): Promise<void> => {
1508
- if (signal?.aborted) {
1509
- throw new Error('Serialization was aborted');
1510
- }
1587
+ await runCacheImprovements(
1588
+ (cache) => {
1589
+ instrument(
1590
+ `RequestTracker::writeToCache::cache.put(${key})`,
1591
+ () => {
1592
+ cache.getNativeRef().putNoConfirm(key, serialize(contents));
1593
+ },
1594
+ );
1595
+ return Promise.resolve();
1596
+ },
1597
+ async () => {
1598
+ await this.options.cache.setLargeBlob(
1599
+ key,
1600
+ serialize(contents),
1601
+ signal
1602
+ ? {
1603
+ signal: signal,
1604
+ }
1605
+ : undefined,
1606
+ );
1607
+ },
1608
+ );
1511
1609
 
1512
- await runCacheImprovements(
1513
- (cache) => {
1514
- instrument(`cache.put(${key})`, () => {
1515
- cache.getNativeRef().putNoConfirm(key, serialize(contents));
1516
- });
1517
- return Promise.resolve();
1518
- },
1519
- async () => {
1520
- await this.options.cache.setLargeBlob(
1521
- key,
1522
- serialize(contents),
1523
- signal
1524
- ? {
1525
- signal: signal,
1526
- }
1527
- : undefined,
1528
- );
1529
- },
1530
- );
1610
+ total += 1;
1531
1611
 
1532
- total += 1;
1612
+ report({
1613
+ type: 'cache',
1614
+ phase: 'write',
1615
+ total,
1616
+ size: this.graph.nodes.length,
1617
+ });
1618
+ };
1533
1619
 
1534
- report({
1535
- type: 'cache',
1536
- phase: 'write',
1537
- total,
1538
- size: this.graph.nodes.length,
1620
+ let queue = new PromiseQueue({
1621
+ maxConcurrent: 32,
1539
1622
  });
1540
- };
1541
1623
 
1542
- let queue = new PromiseQueue({
1543
- maxConcurrent: 32,
1544
- });
1624
+ // Preallocating a sparse array is faster than pushing when N is high enough
1625
+ let cacheableNodes = new Array(serialisedGraph.nodes.length);
1626
+ for (let i = 0; i < serialisedGraph.nodes.length; i += 1) {
1627
+ let node = serialisedGraph.nodes[i];
1545
1628
 
1546
- // Preallocating a sparse array is faster than pushing when N is high enough
1547
- let cacheableNodes = new Array(serialisedGraph.nodes.length);
1548
- for (let i = 0; i < serialisedGraph.nodes.length; i += 1) {
1549
- let node = serialisedGraph.nodes[i];
1550
-
1551
- let resultCacheKey = node?.resultCacheKey;
1552
- if (
1553
- node?.type === REQUEST &&
1554
- resultCacheKey != null &&
1555
- node?.result != null
1556
- ) {
1557
- queue.add(() => serialiseAndSet(resultCacheKey, node.result));
1629
+ let resultCacheKey = node?.resultCacheKey;
1630
+ if (
1631
+ node?.type === REQUEST &&
1632
+ resultCacheKey != null &&
1633
+ node?.result != null
1634
+ ) {
1635
+ queue.add(() => serialiseAndSet(resultCacheKey, node.result));
1558
1636
 
1559
- // eslint-disable-next-line no-unused-vars
1560
- let {result: _, ...newNode} = node;
1561
- cacheableNodes[i] = newNode;
1562
- } else {
1563
- cacheableNodes[i] = node;
1637
+ // eslint-disable-next-line no-unused-vars
1638
+ let {result: _, ...newNode} = node;
1639
+ cacheableNodes[i] = newNode;
1640
+ } else {
1641
+ cacheableNodes[i] = node;
1642
+ }
1564
1643
  }
1565
- }
1566
1644
 
1567
- let nodeCountsPerBlob = [];
1645
+ let nodeCountsPerBlob = [];
1568
1646
 
1569
- for (
1570
- let i = 0;
1571
- i * this.graph.nodesPerBlob < cacheableNodes.length;
1572
- i += 1
1573
- ) {
1574
- let nodesStartIndex = i * this.graph.nodesPerBlob;
1575
- let nodesEndIndex = Math.min(
1576
- (i + 1) * this.graph.nodesPerBlob,
1577
- cacheableNodes.length,
1578
- );
1647
+ for (
1648
+ let i = 0;
1649
+ i * this.graph.nodesPerBlob < cacheableNodes.length;
1650
+ i += 1
1651
+ ) {
1652
+ let nodesStartIndex = i * this.graph.nodesPerBlob;
1653
+ let nodesEndIndex = Math.min(
1654
+ (i + 1) * this.graph.nodesPerBlob,
1655
+ cacheableNodes.length,
1656
+ );
1579
1657
 
1580
- nodeCountsPerBlob.push(nodesEndIndex - nodesStartIndex);
1658
+ nodeCountsPerBlob.push(nodesEndIndex - nodesStartIndex);
1581
1659
 
1582
- if (!this.graph.hasCachedRequestChunk(i)) {
1583
- // We assume the request graph nodes are immutable and won't change
1584
- let nodesToCache = cacheableNodes.slice(nodesStartIndex, nodesEndIndex);
1660
+ if (!this.graph.hasCachedRequestChunk(i)) {
1661
+ // We assume the request graph nodes are immutable and won't change
1662
+ let nodesToCache = cacheableNodes.slice(
1663
+ nodesStartIndex,
1664
+ nodesEndIndex,
1665
+ );
1585
1666
 
1586
- queue.add(() =>
1587
- serialiseAndSet(
1588
- getRequestGraphNodeKey(i, cacheKey),
1589
- nodesToCache,
1590
- ).then(() => {
1591
- // Succeeded in writing to disk, save that we have completed this chunk
1592
- this.graph.setCachedRequestChunk(i);
1593
- }),
1594
- );
1667
+ queue.add(() =>
1668
+ serialiseAndSet(
1669
+ getRequestGraphNodeKey(i, cacheKey),
1670
+ nodesToCache,
1671
+ ).then(() => {
1672
+ // Succeeded in writing to disk, save that we have completed this chunk
1673
+ this.graph.setCachedRequestChunk(i);
1674
+ }),
1675
+ );
1676
+ }
1595
1677
  }
1596
- }
1597
1678
 
1598
- try {
1599
1679
  await queue.run();
1600
1680
 
1601
1681
  // Set the request graph after the queue is flushed to avoid writing an invalid state
@@ -1607,7 +1687,7 @@ export default class RequestTracker {
1607
1687
 
1608
1688
  await runCacheImprovements(
1609
1689
  () =>
1610
- serialiseAndSet(`request_tracker:cache_metadata:${cacheKey}`, {
1690
+ serialiseAndSet(`${cacheKey}/cache_metadata`, {
1611
1691
  version: ATLASPACK_VERSION,
1612
1692
  entries: this.options.entries,
1613
1693
  mode: this.options.mode,
@@ -1628,15 +1708,15 @@ export default class RequestTracker {
1628
1708
  } catch (err) {
1629
1709
  // If we have aborted, ignore the error and continue
1630
1710
  if (!signal?.aborted) throw err;
1711
+ } finally {
1712
+ await runCacheImprovements(
1713
+ async (cache) => {
1714
+ await cache.getNativeRef().commitWriteTransaction();
1715
+ },
1716
+ () => Promise.resolve(),
1717
+ );
1631
1718
  }
1632
1719
 
1633
- await runCacheImprovements(
1634
- async (cache) => {
1635
- await cache.getNativeRef().commitWriteTransaction();
1636
- },
1637
- () => Promise.resolve(),
1638
- );
1639
-
1640
1720
  report({type: 'cache', phase: 'end', total, size: this.graph.nodes.length});
1641
1721
  }
1642
1722
 
@@ -1668,6 +1748,18 @@ export function getWatcherOptions({
1668
1748
  }
1669
1749
 
1670
1750
  function getCacheKey(options) {
1751
+ if (getFeatureFlag('cachePerformanceImprovements')) {
1752
+ const hash = hashString(
1753
+ `${ATLASPACK_VERSION}:${JSON.stringify(options.entries)}:${
1754
+ options.mode
1755
+ }:${options.shouldBuildLazily ? 'lazy' : 'eager'}:${
1756
+ options.watchBackend ?? ''
1757
+ }`,
1758
+ );
1759
+
1760
+ return `RequestTracker/${ATLASPACK_VERSION}/${hash}`;
1761
+ }
1762
+
1671
1763
  return hashString(
1672
1764
  `${ATLASPACK_VERSION}:${JSON.stringify(options.entries)}:${options.mode}:${
1673
1765
  options.shouldBuildLazily ? 'lazy' : 'eager'
@@ -1676,6 +1768,10 @@ function getCacheKey(options) {
1676
1768
  }
1677
1769
 
1678
1770
  function getRequestGraphNodeKey(index: number, cacheKey: string) {
1771
+ if (getFeatureFlag('cachePerformanceImprovements')) {
1772
+ return `${cacheKey}/RequestGraph/nodes/${index}`;
1773
+ }
1774
+
1679
1775
  return `requestGraph-nodes-${index}-${cacheKey}`;
1680
1776
  }
1681
1777
 
@@ -1687,9 +1783,15 @@ export async function readAndDeserializeRequestGraph(
1687
1783
  let bufferLength = 0;
1688
1784
 
1689
1785
  const getAndDeserialize = async (key: string) => {
1690
- let buffer = await cache.getLargeBlob(key);
1691
- bufferLength += Buffer.byteLength(buffer);
1692
- return deserialize(buffer);
1786
+ if (getFeatureFlag('cachePerformanceImprovements')) {
1787
+ const buffer = await cache.getBlob(key);
1788
+ bufferLength += Buffer.byteLength(buffer);
1789
+ return deserialize(buffer);
1790
+ } else {
1791
+ const buffer = await cache.getLargeBlob(key);
1792
+ bufferLength += Buffer.byteLength(buffer);
1793
+ return deserialize(buffer);
1794
+ }
1693
1795
  };
1694
1796
 
1695
1797
  let serializedRequestGraph = await getAndDeserialize(requestGraphKey);
@@ -1722,21 +1824,45 @@ async function loadRequestGraph(options): Async<RequestGraph> {
1722
1824
  }
1723
1825
 
1724
1826
  let cacheKey = getCacheKey(options);
1725
- let requestGraphKey = `requestGraph-${cacheKey}`;
1827
+ let requestGraphKey = getFeatureFlag('cachePerformanceImprovements')
1828
+ ? `${cacheKey}/RequestGraph`
1829
+ : `requestGraph-${cacheKey}`;
1830
+
1726
1831
  let timeout;
1727
- const snapshotKey = `snapshot-${cacheKey}`;
1832
+ const snapshotKey = getFeatureFlag('cachePerformanceImprovements')
1833
+ ? `${cacheKey}/snapshot`
1834
+ : `snapshot-${cacheKey}`;
1728
1835
  const snapshotPath = path.join(options.cacheDir, snapshotKey + '.txt');
1729
1836
 
1837
+ const commonMeta = {
1838
+ cacheKey,
1839
+ snapshotKey,
1840
+ cacheKeyOptions: {
1841
+ version: ATLASPACK_VERSION,
1842
+ entries: options.entries,
1843
+ mode: options.mode,
1844
+ shouldBuildLazily: options.shouldBuildLazily,
1845
+ watchBackend: options.watchBackend,
1846
+ },
1847
+ };
1848
+
1730
1849
  logger.verbose({
1731
1850
  origin: '@atlaspack/core',
1732
1851
  message: 'Loading request graph',
1733
1852
  meta: {
1734
- cacheKey,
1735
- snapshotKey,
1853
+ ...commonMeta,
1736
1854
  },
1737
1855
  });
1738
1856
 
1739
- if (await options.cache.hasLargeBlob(requestGraphKey)) {
1857
+ if (getFeatureFlag('environmentDeduplication')) {
1858
+ await loadEnvironmentsFromCache(options.cache);
1859
+ }
1860
+
1861
+ const hasRequestGraphInCache = getFeatureFlag('cachePerformanceImprovements')
1862
+ ? await options.cache.has(requestGraphKey)
1863
+ : await options.cache.hasLargeBlob(requestGraphKey);
1864
+
1865
+ if (hasRequestGraphInCache) {
1740
1866
  try {
1741
1867
  let {requestGraph} = await readAndDeserializeRequestGraph(
1742
1868
  options.cache,
@@ -1767,23 +1893,36 @@ async function loadRequestGraph(options): Async<RequestGraph> {
1767
1893
  origin: '@atlaspack/core',
1768
1894
  message: `File system event count: ${events.length}`,
1769
1895
  meta: {
1896
+ ...commonMeta,
1770
1897
  trackableEvent: 'watcher_events_count',
1771
1898
  watcherEventCount: events.length,
1772
1899
  duration: Date.now() - startTime,
1773
1900
  },
1774
1901
  });
1775
1902
 
1776
- requestGraph.invalidateUnpredictableNodes();
1777
- requestGraph.invalidateOnBuildNodes();
1778
- requestGraph.invalidateEnvNodes(options.env);
1779
- requestGraph.invalidateOptionNodes(options);
1903
+ if (getFeatureFlag('verboseRequestInvalidationStats')) {
1904
+ const invalidationStats = await invalidateRequestGraph(
1905
+ requestGraph,
1906
+ options,
1907
+ events,
1908
+ );
1909
+
1910
+ logger.verbose({
1911
+ origin: '@atlaspack/core',
1912
+ message: 'Request track loaded from cache',
1913
+ meta: {
1914
+ ...commonMeta,
1915
+ trackableEvent: 'request_tracker_cache_key_hit',
1916
+ invalidationStats,
1917
+ },
1918
+ });
1919
+ } else {
1920
+ requestGraph.invalidateUnpredictableNodes();
1921
+ requestGraph.invalidateOnBuildNodes();
1922
+ requestGraph.invalidateEnvNodes(options.env);
1923
+ requestGraph.invalidateOptionNodes(options);
1924
+ }
1780
1925
 
1781
- await requestGraph.respondToFSEvents(
1782
- options.unstableFileInvalidations || events,
1783
- options,
1784
- 10000,
1785
- true,
1786
- );
1787
1926
  return requestGraph;
1788
1927
  } catch (e) {
1789
1928
  // Prevent logging fs events took too long warning
@@ -1800,13 +1939,227 @@ async function loadRequestGraph(options): Async<RequestGraph> {
1800
1939
  message:
1801
1940
  'Cache entry for request tracker was not found, initializing a clean cache.',
1802
1941
  meta: {
1803
- cacheKey,
1804
- snapshotKey,
1942
+ ...commonMeta,
1943
+ trackableEvent: 'request_tracker_cache_key_miss',
1805
1944
  },
1806
1945
  });
1807
1946
  return new RequestGraph();
1808
1947
  }
1809
1948
 
1949
+ /**
1950
+ * A wrapper around an invalidation type / method
1951
+ */
1952
+ type InvalidationFn = {|
1953
+ key: string,
1954
+ fn: () =>
1955
+ | InvalidationDetail
1956
+ | Promise<InvalidationDetail>
1957
+ | void
1958
+ | Promise<void>,
1959
+ |};
1960
+
1961
+ type InvalidationStats = {|
1962
+ /**
1963
+ * Total number of request graph nodes
1964
+ */
1965
+ nodeCount: number,
1966
+ /**
1967
+ * Number of requests in RequestGraph
1968
+ */
1969
+ requestCount: number,
1970
+ /**
1971
+ * Number of nodes that have been invalidated.
1972
+ */
1973
+ invalidatedCount: number,
1974
+ /**
1975
+ * Percentage of requests that have been invalidated
1976
+ */
1977
+ requestInvalidationRatio: number,
1978
+ /**
1979
+ * Percentage of nodes that have been invalidated
1980
+ */
1981
+ nodeInvalidationRatio: number,
1982
+ /**
1983
+ * Details for each invalidation type
1984
+ */
1985
+ invalidations: InvalidationFnStats[],
1986
+ |};
1987
+
1988
+ /**
1989
+ * Details about an invalidation.
1990
+ *
1991
+ * If this is a fs events invalidation, this key will contain statistics about invalidations
1992
+ * by path.
1993
+ *
1994
+ * If this is a env or option invalidation, this key will contain the list of changed environment
1995
+ * variables or options.
1996
+ */
1997
+ type InvalidationDetail = string[] | FSInvalidationStats;
1998
+
1999
+ /**
2000
+ * Number of invalidations for a given file-system event.
2001
+ */
2002
+ type FSInvalidation = {|
2003
+ path: string,
2004
+ count: number,
2005
+ |};
2006
+
2007
+ type FSInvalidationStats = {|
2008
+ /**
2009
+ * This list will be sorted by the number of nodes invalidated and only the top 10 will be
2010
+ * included.
2011
+ */
2012
+ biggestInvalidations: FSInvalidation[],
2013
+ |};
2014
+
2015
+ /**
2016
+ * Information about a certain cache invalidation type.
2017
+ */
2018
+ type InvalidationFnStats = {|
2019
+ /**
2020
+ * Invalidation type, one of:
2021
+ *
2022
+ * - unpredictable
2023
+ * - onBuild
2024
+ * - env
2025
+ * - option
2026
+ * - fsEvents
2027
+ */
2028
+ key: string,
2029
+ /**
2030
+ * Number of invalidated nodes coming from this invalidation type.
2031
+ */
2032
+ count: number,
2033
+ /**
2034
+ * If this is a env or option invalidation, this key will contain the list of changed values.
2035
+ *
2036
+ * If this is a fs events invalidation, this key will contain statistics about invalidations
2037
+ */
2038
+ detail: null | InvalidationDetail,
2039
+ /**
2040
+ * Time in milliseconds it took to run the invalidation.
2041
+ */
2042
+ duration: number,
2043
+ |};
2044
+
2045
+ /**
2046
+ * Respond to unpredictable, build, environment changes, option changes and file-system events
2047
+ * invalidating RequestGraph nodes.
2048
+ *
2049
+ * Returns the count of nodes invalidated by each invalidation type.
2050
+ */
2051
+ export async function invalidateRequestGraph(
2052
+ requestGraph: RequestGraph,
2053
+ options: AtlaspackOptions,
2054
+ events: Event[],
2055
+ ): Promise<InvalidationStats> {
2056
+ const invalidationFns: InvalidationFn[] = [
2057
+ {
2058
+ key: 'unpredictable',
2059
+ fn: () => requestGraph.invalidateUnpredictableNodes(),
2060
+ },
2061
+ {
2062
+ key: 'onBuild',
2063
+ fn: () => requestGraph.invalidateOnBuildNodes(),
2064
+ },
2065
+ {
2066
+ key: 'env',
2067
+ fn: () => requestGraph.invalidateEnvNodes(options.env),
2068
+ },
2069
+ {
2070
+ key: 'option',
2071
+ fn: () => requestGraph.invalidateOptionNodes(options),
2072
+ },
2073
+ {
2074
+ key: 'fsEvents',
2075
+ fn: () => invalidateRequestGraphFSEvents(requestGraph, options, events),
2076
+ },
2077
+ ];
2078
+
2079
+ const invalidations = [];
2080
+ for (const invalidation of invalidationFns) {
2081
+ invalidations.push(await runInvalidation(requestGraph, invalidation));
2082
+ }
2083
+ const invalidatedCount = invalidations.reduce(
2084
+ (acc, invalidation) => acc + invalidation.count,
2085
+ 0,
2086
+ );
2087
+ const requestCount = requestGraph.nodes.reduce(
2088
+ (acc, node) => acc + (node?.type === REQUEST ? 1 : 0),
2089
+ 0,
2090
+ );
2091
+ const nodeCount = requestGraph.nodes.length;
2092
+ const nodeInvalidationRatio = invalidatedCount / nodeCount;
2093
+ const requestInvalidationRatio = invalidatedCount / requestCount;
2094
+
2095
+ return {
2096
+ invalidations,
2097
+ nodeCount,
2098
+ requestCount,
2099
+ invalidatedCount,
2100
+ nodeInvalidationRatio,
2101
+ requestInvalidationRatio,
2102
+ };
2103
+ }
2104
+
2105
+ interface InvalidateRequestGraphFSEventsInput {
2106
+ respondToFSEvents(
2107
+ events: Event[],
2108
+ options: AtlaspackOptions,
2109
+ timeout: number,
2110
+ shouldLog: boolean,
2111
+ ): Promise<{invalidationsByPath: Map<string, number>, ...}>;
2112
+ }
2113
+
2114
+ /**
2115
+ * Invalidate the request graph based on file-system events.
2116
+ *
2117
+ * Returns statistics about the invalidations.
2118
+ */
2119
+ export async function invalidateRequestGraphFSEvents(
2120
+ requestGraph: InvalidateRequestGraphFSEventsInput,
2121
+ options: AtlaspackOptions,
2122
+ events: Event[],
2123
+ ): Promise<FSInvalidationStats> {
2124
+ const {invalidationsByPath} = await requestGraph.respondToFSEvents(
2125
+ options.unstableFileInvalidations || events,
2126
+ options,
2127
+ 10000,
2128
+ true,
2129
+ );
2130
+ const biggestInvalidations =
2131
+ getBiggestFSEventsInvalidations(invalidationsByPath);
2132
+
2133
+ return {
2134
+ biggestInvalidations,
2135
+ };
2136
+ }
2137
+
2138
+ interface RunInvalidationInput {
2139
+ getInvalidNodeCount(): number;
2140
+ }
2141
+
2142
+ /**
2143
+ * Runs an invalidation function and reports metrics.
2144
+ */
2145
+ export async function runInvalidation(
2146
+ requestGraph: RunInvalidationInput,
2147
+ invalidationFn: InvalidationFn,
2148
+ ): Promise<InvalidationFnStats> {
2149
+ const start = performance.now();
2150
+ const startInvalidationCount = requestGraph.getInvalidNodeCount();
2151
+ const result = await invalidationFn.fn();
2152
+ const count = requestGraph.getInvalidNodeCount() - startInvalidationCount;
2153
+ const duration = performance.now() - start;
2154
+
2155
+ return {
2156
+ key: invalidationFn.key,
2157
+ count,
2158
+ detail: result ?? null,
2159
+ duration,
2160
+ };
2161
+ }
2162
+
1810
2163
  function logErrorOnBailout(
1811
2164
  options: AtlaspackOptions,
1812
2165
  snapshotPath: string,
@@ -1858,3 +2211,19 @@ export function cleanUpOrphans<N, E: number>(graph: Graph<N, E>): NodeId[] {
1858
2211
 
1859
2212
  return removedNodeIds;
1860
2213
  }
2214
+
2215
+ /**
2216
+ * Returns paths that invalidated the most nodes
2217
+ */
2218
+ export function getBiggestFSEventsInvalidations(
2219
+ invalidationsByPath: Map<string, number>,
2220
+ limit: number = 10,
2221
+ ): Array<FSInvalidation> {
2222
+ const invalidations = [];
2223
+ for (const [path, count] of invalidationsByPath) {
2224
+ invalidations.push({path, count});
2225
+ }
2226
+ invalidations.sort((a, b) => b.count - a.count);
2227
+
2228
+ return invalidations.slice(0, limit);
2229
+ }