@atlaspack/core 2.14.1-dev.144 → 2.14.1-dev.146

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 (67) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/lib/AssetGraph.js +17 -6
  3. package/lib/Atlaspack.js +24 -5
  4. package/lib/BundleGraph.js +6 -5
  5. package/lib/Dependency.js +6 -2
  6. package/lib/Environment.js +4 -3
  7. package/lib/EnvironmentManager.js +137 -0
  8. package/lib/InternalConfig.js +3 -2
  9. package/lib/PackagerRunner.js +52 -15
  10. package/lib/RequestTracker.js +337 -82
  11. package/lib/UncommittedAsset.js +20 -2
  12. package/lib/applyRuntimes.js +2 -1
  13. package/lib/assetUtils.js +2 -1
  14. package/lib/atlaspack-v3/worker/worker.js +8 -0
  15. package/lib/public/Asset.js +3 -2
  16. package/lib/public/Bundle.js +2 -1
  17. package/lib/public/BundleGraph.js +21 -5
  18. package/lib/public/Config.js +98 -3
  19. package/lib/public/Dependency.js +2 -1
  20. package/lib/public/MutableBundleGraph.js +2 -1
  21. package/lib/public/Target.js +2 -1
  22. package/lib/requests/AssetGraphRequest.js +13 -1
  23. package/lib/requests/AssetRequest.js +2 -1
  24. package/lib/requests/BundleGraphRequest.js +13 -1
  25. package/lib/requests/ConfigRequest.js +27 -4
  26. package/lib/requests/TargetRequest.js +18 -16
  27. package/lib/requests/WriteBundleRequest.js +15 -3
  28. package/lib/requests/WriteBundlesRequest.js +1 -0
  29. package/lib/resolveOptions.js +5 -6
  30. package/package.json +22 -18
  31. package/src/AssetGraph.js +12 -6
  32. package/src/Atlaspack.js +29 -13
  33. package/src/BundleGraph.js +13 -8
  34. package/src/Dependency.js +13 -5
  35. package/src/Environment.js +8 -5
  36. package/src/EnvironmentManager.js +145 -0
  37. package/src/InternalConfig.js +6 -5
  38. package/src/PackagerRunner.js +72 -20
  39. package/src/RequestTracker.js +567 -131
  40. package/src/UncommittedAsset.js +23 -3
  41. package/src/applyRuntimes.js +6 -1
  42. package/src/assetUtils.js +4 -3
  43. package/src/atlaspack-v3/worker/compat/plugin-config.js +9 -5
  44. package/src/atlaspack-v3/worker/worker.js +7 -0
  45. package/src/public/Asset.js +9 -2
  46. package/src/public/Bundle.js +2 -1
  47. package/src/public/BundleGraph.js +22 -5
  48. package/src/public/Config.js +129 -14
  49. package/src/public/Dependency.js +2 -1
  50. package/src/public/MutableBundleGraph.js +2 -1
  51. package/src/public/Target.js +2 -1
  52. package/src/requests/AssetGraphRequest.js +13 -3
  53. package/src/requests/AssetRequest.js +2 -1
  54. package/src/requests/BundleGraphRequest.js +13 -3
  55. package/src/requests/ConfigRequest.js +33 -9
  56. package/src/requests/TargetRequest.js +19 -25
  57. package/src/requests/WriteBundleRequest.js +14 -8
  58. package/src/requests/WriteBundlesRequest.js +1 -0
  59. package/src/resolveOptions.js +6 -8
  60. package/src/types.js +9 -7
  61. package/test/Environment.test.js +43 -34
  62. package/test/EnvironmentManager.test.js +192 -0
  63. package/test/PublicEnvironment.test.js +10 -7
  64. package/test/RequestTracker.test.js +115 -3
  65. package/test/public/Config.test.js +108 -0
  66. package/test/requests/ConfigRequest.test.js +187 -3
  67. package/test/test-utils.js +4 -9
@@ -4,7 +4,7 @@ import invariant, {AssertionError} from 'assert';
4
4
  import path from 'path';
5
5
 
6
6
  import {deserialize, serialize} from '@atlaspack/build-cache';
7
- import type {Cache} from '@atlaspack/cache';
7
+ import {LMDBLiteCache, type Cache} from '@atlaspack/cache';
8
8
  import {getFeatureFlag} from '@atlaspack/feature-flags';
9
9
  import {ContentGraph} from '@atlaspack/graph';
10
10
  import type {
@@ -14,7 +14,7 @@ import type {
14
14
  SerializedContentGraph,
15
15
  Graph,
16
16
  } from '@atlaspack/graph';
17
- import logger from '@atlaspack/logger';
17
+ import logger, {instrument} from '@atlaspack/logger';
18
18
  import {hashString} from '@atlaspack/rust';
19
19
  import type {Async, EnvMap} from '@atlaspack/types';
20
20
  import {
@@ -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 {
@@ -69,6 +69,12 @@ import type {
69
69
  InternalGlob,
70
70
  } from './types';
71
71
  import {BuildAbortError, assertSignalNotAborted, 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,22 @@ 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
+ const isConfigKeyChange =
1127
+ getFeatureFlag('granularTsConfigInvalidation') ||
1128
+ type === 'delete' ||
1129
+ type === 'update';
1130
+ if (configKeyNodes && isConfigKeyChange) {
1076
1131
  for (let nodeId of configKeyNodes) {
1077
1132
  let isInvalid = type === 'delete';
1078
1133
 
1079
- if (type === 'update') {
1134
+ if (type !== 'delete') {
1080
1135
  let node = this.getNode(nodeId);
1081
1136
  invariant(node && node.type === CONFIG_KEY);
1082
1137
 
@@ -1104,6 +1159,13 @@ export class RequestGraph extends ContentGraph<
1104
1159
  }
1105
1160
  }
1106
1161
  }
1162
+
1163
+ const invalidationsAfter = this.getInvalidNodeCount();
1164
+ const invalidationsForEvent = invalidationsAfter - invalidationsBefore;
1165
+ invalidationsByPath.set(
1166
+ _path,
1167
+ (invalidationsByPath.get(_path) ?? 0) + invalidationsForEvent,
1168
+ );
1107
1169
  }
1108
1170
 
1109
1171
  if (getFeatureFlag('fixQuadraticCacheInvalidation')) {
@@ -1124,7 +1186,10 @@ export class RequestGraph extends ContentGraph<
1124
1186
  },
1125
1187
  });
1126
1188
 
1127
- return didInvalidate && this.invalidNodeIds.size > 0;
1189
+ return {
1190
+ didInvalidate,
1191
+ invalidationsByPath,
1192
+ };
1128
1193
  }
1129
1194
 
1130
1195
  hasCachedRequestChunk(index: number): boolean {
@@ -1138,6 +1203,13 @@ export class RequestGraph extends ContentGraph<
1138
1203
  removeCachedRequestChunkForNode(nodeId: number): void {
1139
1204
  this.cachedRequestChunks.delete(Math.floor(nodeId / this.nodesPerBlob));
1140
1205
  }
1206
+
1207
+ /**
1208
+ * Returns the number of invalidated nodes in the graph.
1209
+ */
1210
+ getInvalidNodeCount(): number {
1211
+ return this.invalidNodeIds.size;
1212
+ }
1141
1213
  }
1142
1214
 
1143
1215
  export default class RequestTracker {
@@ -1228,10 +1300,13 @@ export default class RequestTracker {
1228
1300
  return result;
1229
1301
  } else if (node.resultCacheKey != null && ifMatch == null) {
1230
1302
  let key = node.resultCacheKey;
1231
- invariant(this.options.cache.hasLargeBlob(key));
1232
- let cachedResult: T = deserialize(
1233
- await this.options.cache.getLargeBlob(key),
1234
- );
1303
+ if (!getFeatureFlag('cachePerformanceImprovements')) {
1304
+ invariant(this.options.cache.hasLargeBlob(key));
1305
+ }
1306
+
1307
+ let cachedResult: T = getFeatureFlag('cachePerformanceImprovements')
1308
+ ? nullthrows(await this.options.cache.get<T>(key))
1309
+ : deserialize(await this.options.cache.getLargeBlob(key));
1235
1310
  node.result = cachedResult;
1236
1311
  return cachedResult;
1237
1312
  }
@@ -1258,7 +1333,13 @@ export default class RequestTracker {
1258
1333
  }
1259
1334
  }
1260
1335
 
1261
- respondToFSEvents(events: Array<Event>, threshold: number): Async<boolean> {
1336
+ respondToFSEvents(
1337
+ events: Array<Event>,
1338
+ threshold: number,
1339
+ ): Promise<{|
1340
+ didInvalidate: boolean,
1341
+ invalidationsByPath: Map<string, number>,
1342
+ |}> {
1262
1343
  return this.graph.respondToFSEvents(events, this.options, threshold);
1263
1344
  }
1264
1345
 
@@ -1454,113 +1535,158 @@ export default class RequestTracker {
1454
1535
  }
1455
1536
 
1456
1537
  async writeToCache(signal?: AbortSignal) {
1538
+ const options = this.options;
1539
+ async function runCacheImprovements<T>(
1540
+ newPath: (cache: LMDBLiteCache) => Promise<T>,
1541
+ oldPath: () => Promise<T>,
1542
+ ): Promise<T> {
1543
+ if (getFeatureFlag('cachePerformanceImprovements')) {
1544
+ invariant(options.cache instanceof LMDBLiteCache);
1545
+ const result = await newPath(options.cache);
1546
+ return result;
1547
+ } else {
1548
+ const result = await oldPath();
1549
+ return result;
1550
+ }
1551
+ }
1552
+
1457
1553
  let cacheKey = getCacheKey(this.options);
1458
- let requestGraphKey = `requestGraph-${cacheKey}`;
1459
- let snapshotKey = `snapshot-${cacheKey}`;
1554
+ let requestGraphKey = getFeatureFlag('cachePerformanceImprovements')
1555
+ ? `${cacheKey}/RequestGraph`
1556
+ : `requestGraph-${cacheKey}`;
1557
+ let snapshotKey = getFeatureFlag('cachePerformanceImprovements')
1558
+ ? `${cacheKey}/snapshot`
1559
+ : `snapshot-${cacheKey}`;
1460
1560
 
1461
1561
  if (this.options.shouldDisableCache) {
1462
1562
  return;
1463
1563
  }
1464
1564
 
1465
1565
  let total = 0;
1466
- report({
1467
- type: 'cache',
1468
- phase: 'start',
1469
- total,
1470
- size: this.graph.nodes.length,
1471
- });
1566
+ await runCacheImprovements(
1567
+ async (cache) => {
1568
+ await cache.getNativeRef().startWriteTransaction();
1569
+ },
1570
+ () => Promise.resolve(),
1571
+ );
1572
+ try {
1573
+ report({
1574
+ type: 'cache',
1575
+ phase: 'start',
1576
+ total,
1577
+ size: this.graph.nodes.length,
1578
+ });
1472
1579
 
1473
- let serialisedGraph = this.graph.serialize();
1580
+ if (getFeatureFlag('environmentDeduplication')) {
1581
+ await writeEnvironmentsToCache(options.cache);
1582
+ }
1474
1583
 
1475
- // Delete an existing request graph cache, to prevent invalid states
1476
- await this.options.cache.deleteLargeBlob(requestGraphKey);
1584
+ let serialisedGraph = this.graph.serialize();
1477
1585
 
1478
- const serialiseAndSet = async (
1479
- key: string,
1480
- // $FlowFixMe serialise input is any type
1481
- contents: any,
1482
- ): Promise<void> => {
1483
- if (signal?.aborted) {
1484
- throw new Error('Serialization was aborted');
1485
- }
1586
+ // Delete an existing request graph cache, to prevent invalid states
1587
+ await this.options.cache.deleteLargeBlob(requestGraphKey);
1486
1588
 
1487
- await this.options.cache.setLargeBlob(
1488
- key,
1489
- serialize(contents),
1490
- signal
1491
- ? {
1492
- signal: signal,
1493
- }
1494
- : undefined,
1495
- );
1589
+ const serialiseAndSet = async (
1590
+ key: string,
1591
+ // $FlowFixMe serialise input is any type
1592
+ contents: any,
1593
+ ): Promise<void> => {
1594
+ if (signal?.aborted) {
1595
+ throw new Error('Serialization was aborted');
1596
+ }
1496
1597
 
1497
- total += 1;
1598
+ await runCacheImprovements(
1599
+ (cache) => {
1600
+ instrument(
1601
+ `RequestTracker::writeToCache::cache.put(${key})`,
1602
+ () => {
1603
+ cache.getNativeRef().putNoConfirm(key, serialize(contents));
1604
+ },
1605
+ );
1606
+ return Promise.resolve();
1607
+ },
1608
+ async () => {
1609
+ await this.options.cache.setLargeBlob(
1610
+ key,
1611
+ serialize(contents),
1612
+ signal
1613
+ ? {
1614
+ signal: signal,
1615
+ }
1616
+ : undefined,
1617
+ );
1618
+ },
1619
+ );
1498
1620
 
1499
- report({
1500
- type: 'cache',
1501
- phase: 'write',
1502
- total,
1503
- size: this.graph.nodes.length,
1504
- });
1505
- };
1621
+ total += 1;
1506
1622
 
1507
- let queue = new PromiseQueue({
1508
- maxConcurrent: 32,
1509
- });
1623
+ report({
1624
+ type: 'cache',
1625
+ phase: 'write',
1626
+ total,
1627
+ size: this.graph.nodes.length,
1628
+ });
1629
+ };
1510
1630
 
1511
- // Preallocating a sparse array is faster than pushing when N is high enough
1512
- let cacheableNodes = new Array(serialisedGraph.nodes.length);
1513
- for (let i = 0; i < serialisedGraph.nodes.length; i += 1) {
1514
- let node = serialisedGraph.nodes[i];
1631
+ let queue = new PromiseQueue({
1632
+ maxConcurrent: 32,
1633
+ });
1515
1634
 
1516
- let resultCacheKey = node?.resultCacheKey;
1517
- if (
1518
- node?.type === REQUEST &&
1519
- resultCacheKey != null &&
1520
- node?.result != null
1521
- ) {
1522
- queue.add(() => serialiseAndSet(resultCacheKey, node.result));
1635
+ // Preallocating a sparse array is faster than pushing when N is high enough
1636
+ let cacheableNodes = new Array(serialisedGraph.nodes.length);
1637
+ for (let i = 0; i < serialisedGraph.nodes.length; i += 1) {
1638
+ let node = serialisedGraph.nodes[i];
1523
1639
 
1524
- // eslint-disable-next-line no-unused-vars
1525
- let {result: _, ...newNode} = node;
1526
- cacheableNodes[i] = newNode;
1527
- } else {
1528
- cacheableNodes[i] = node;
1640
+ let resultCacheKey = node?.resultCacheKey;
1641
+ if (
1642
+ node?.type === REQUEST &&
1643
+ resultCacheKey != null &&
1644
+ node?.result != null
1645
+ ) {
1646
+ queue.add(() => serialiseAndSet(resultCacheKey, node.result));
1647
+
1648
+ // eslint-disable-next-line no-unused-vars
1649
+ let {result: _, ...newNode} = node;
1650
+ cacheableNodes[i] = newNode;
1651
+ } else {
1652
+ cacheableNodes[i] = node;
1653
+ }
1529
1654
  }
1530
- }
1531
1655
 
1532
- let nodeCountsPerBlob = [];
1656
+ let nodeCountsPerBlob = [];
1533
1657
 
1534
- for (
1535
- let i = 0;
1536
- i * this.graph.nodesPerBlob < cacheableNodes.length;
1537
- i += 1
1538
- ) {
1539
- let nodesStartIndex = i * this.graph.nodesPerBlob;
1540
- let nodesEndIndex = Math.min(
1541
- (i + 1) * this.graph.nodesPerBlob,
1542
- cacheableNodes.length,
1543
- );
1658
+ for (
1659
+ let i = 0;
1660
+ i * this.graph.nodesPerBlob < cacheableNodes.length;
1661
+ i += 1
1662
+ ) {
1663
+ let nodesStartIndex = i * this.graph.nodesPerBlob;
1664
+ let nodesEndIndex = Math.min(
1665
+ (i + 1) * this.graph.nodesPerBlob,
1666
+ cacheableNodes.length,
1667
+ );
1544
1668
 
1545
- nodeCountsPerBlob.push(nodesEndIndex - nodesStartIndex);
1669
+ nodeCountsPerBlob.push(nodesEndIndex - nodesStartIndex);
1546
1670
 
1547
- if (!this.graph.hasCachedRequestChunk(i)) {
1548
- // We assume the request graph nodes are immutable and won't change
1549
- let nodesToCache = cacheableNodes.slice(nodesStartIndex, nodesEndIndex);
1671
+ if (!this.graph.hasCachedRequestChunk(i)) {
1672
+ // We assume the request graph nodes are immutable and won't change
1673
+ let nodesToCache = cacheableNodes.slice(
1674
+ nodesStartIndex,
1675
+ nodesEndIndex,
1676
+ );
1550
1677
 
1551
- queue.add(() =>
1552
- serialiseAndSet(
1553
- getRequestGraphNodeKey(i, cacheKey),
1554
- nodesToCache,
1555
- ).then(() => {
1556
- // Succeeded in writing to disk, save that we have completed this chunk
1557
- this.graph.setCachedRequestChunk(i);
1558
- }),
1559
- );
1678
+ queue.add(() =>
1679
+ serialiseAndSet(
1680
+ getRequestGraphNodeKey(i, cacheKey),
1681
+ nodesToCache,
1682
+ ).then(() => {
1683
+ // Succeeded in writing to disk, save that we have completed this chunk
1684
+ this.graph.setCachedRequestChunk(i);
1685
+ }),
1686
+ );
1687
+ }
1560
1688
  }
1561
- }
1562
1689
 
1563
- try {
1564
1690
  await queue.run();
1565
1691
 
1566
1692
  // Set the request graph after the queue is flushed to avoid writing an invalid state
@@ -1570,6 +1696,18 @@ export default class RequestTracker {
1570
1696
  nodes: undefined,
1571
1697
  });
1572
1698
 
1699
+ await runCacheImprovements(
1700
+ () =>
1701
+ serialiseAndSet(`${cacheKey}/cache_metadata`, {
1702
+ version: ATLASPACK_VERSION,
1703
+ entries: this.options.entries,
1704
+ mode: this.options.mode,
1705
+ shouldBuildLazily: this.options.shouldBuildLazily,
1706
+ watchBackend: this.options.watchBackend,
1707
+ }),
1708
+ () => Promise.resolve(),
1709
+ );
1710
+
1573
1711
  let opts = getWatcherOptions(this.options);
1574
1712
  let snapshotPath = path.join(this.options.cacheDir, snapshotKey + '.txt');
1575
1713
 
@@ -1581,6 +1719,13 @@ export default class RequestTracker {
1581
1719
  } catch (err) {
1582
1720
  // If we have aborted, ignore the error and continue
1583
1721
  if (!signal?.aborted) throw err;
1722
+ } finally {
1723
+ await runCacheImprovements(
1724
+ async (cache) => {
1725
+ await cache.getNativeRef().commitWriteTransaction();
1726
+ },
1727
+ () => Promise.resolve(),
1728
+ );
1584
1729
  }
1585
1730
 
1586
1731
  report({type: 'cache', phase: 'end', total, size: this.graph.nodes.length});
@@ -1614,6 +1759,18 @@ export function getWatcherOptions({
1614
1759
  }
1615
1760
 
1616
1761
  function getCacheKey(options) {
1762
+ if (getFeatureFlag('cachePerformanceImprovements')) {
1763
+ const hash = hashString(
1764
+ `${ATLASPACK_VERSION}:${JSON.stringify(options.entries)}:${
1765
+ options.mode
1766
+ }:${options.shouldBuildLazily ? 'lazy' : 'eager'}:${
1767
+ options.watchBackend ?? ''
1768
+ }`,
1769
+ );
1770
+
1771
+ return `RequestTracker/${ATLASPACK_VERSION}/${hash}`;
1772
+ }
1773
+
1617
1774
  return hashString(
1618
1775
  `${ATLASPACK_VERSION}:${JSON.stringify(options.entries)}:${options.mode}:${
1619
1776
  options.shouldBuildLazily ? 'lazy' : 'eager'
@@ -1622,6 +1779,10 @@ function getCacheKey(options) {
1622
1779
  }
1623
1780
 
1624
1781
  function getRequestGraphNodeKey(index: number, cacheKey: string) {
1782
+ if (getFeatureFlag('cachePerformanceImprovements')) {
1783
+ return `${cacheKey}/RequestGraph/nodes/${index}`;
1784
+ }
1785
+
1625
1786
  return `requestGraph-nodes-${index}-${cacheKey}`;
1626
1787
  }
1627
1788
 
@@ -1631,10 +1792,17 @@ export async function readAndDeserializeRequestGraph(
1631
1792
  cacheKey: string,
1632
1793
  ): Async<{|requestGraph: RequestGraph, bufferLength: number|}> {
1633
1794
  let bufferLength = 0;
1795
+
1634
1796
  const getAndDeserialize = async (key: string) => {
1635
- let buffer = await cache.getLargeBlob(key);
1636
- bufferLength += Buffer.byteLength(buffer);
1637
- return deserialize(buffer);
1797
+ if (getFeatureFlag('cachePerformanceImprovements')) {
1798
+ const buffer = await cache.getBlob(key);
1799
+ bufferLength += Buffer.byteLength(buffer);
1800
+ return deserialize(buffer);
1801
+ } else {
1802
+ const buffer = await cache.getLargeBlob(key);
1803
+ bufferLength += Buffer.byteLength(buffer);
1804
+ return deserialize(buffer);
1805
+ }
1638
1806
  };
1639
1807
 
1640
1808
  let serializedRequestGraph = await getAndDeserialize(requestGraphKey);
@@ -1667,20 +1835,45 @@ async function loadRequestGraph(options): Async<RequestGraph> {
1667
1835
  }
1668
1836
 
1669
1837
  let cacheKey = getCacheKey(options);
1670
- let requestGraphKey = `requestGraph-${cacheKey}`;
1838
+ let requestGraphKey = getFeatureFlag('cachePerformanceImprovements')
1839
+ ? `${cacheKey}/RequestGraph`
1840
+ : `requestGraph-${cacheKey}`;
1841
+
1671
1842
  let timeout;
1672
- const snapshotKey = `snapshot-${cacheKey}`;
1843
+ const snapshotKey = getFeatureFlag('cachePerformanceImprovements')
1844
+ ? `${cacheKey}/snapshot`
1845
+ : `snapshot-${cacheKey}`;
1673
1846
  const snapshotPath = path.join(options.cacheDir, snapshotKey + '.txt');
1674
1847
 
1848
+ const commonMeta = {
1849
+ cacheKey,
1850
+ snapshotKey,
1851
+ cacheKeyOptions: {
1852
+ version: ATLASPACK_VERSION,
1853
+ entries: options.entries,
1854
+ mode: options.mode,
1855
+ shouldBuildLazily: options.shouldBuildLazily,
1856
+ watchBackend: options.watchBackend,
1857
+ },
1858
+ };
1859
+
1675
1860
  logger.verbose({
1676
1861
  origin: '@atlaspack/core',
1677
1862
  message: 'Loading request graph',
1678
1863
  meta: {
1679
- cacheKey,
1680
- snapshotKey,
1864
+ ...commonMeta,
1681
1865
  },
1682
1866
  });
1683
- if (await options.cache.hasLargeBlob(requestGraphKey)) {
1867
+
1868
+ if (getFeatureFlag('environmentDeduplication')) {
1869
+ await loadEnvironmentsFromCache(options.cache);
1870
+ }
1871
+
1872
+ const hasRequestGraphInCache = getFeatureFlag('cachePerformanceImprovements')
1873
+ ? await options.cache.has(requestGraphKey)
1874
+ : await options.cache.hasLargeBlob(requestGraphKey);
1875
+
1876
+ if (hasRequestGraphInCache) {
1684
1877
  try {
1685
1878
  let {requestGraph} = await readAndDeserializeRequestGraph(
1686
1879
  options.cache,
@@ -1711,23 +1904,36 @@ async function loadRequestGraph(options): Async<RequestGraph> {
1711
1904
  origin: '@atlaspack/core',
1712
1905
  message: `File system event count: ${events.length}`,
1713
1906
  meta: {
1907
+ ...commonMeta,
1714
1908
  trackableEvent: 'watcher_events_count',
1715
1909
  watcherEventCount: events.length,
1716
1910
  duration: Date.now() - startTime,
1717
1911
  },
1718
1912
  });
1719
1913
 
1720
- requestGraph.invalidateUnpredictableNodes();
1721
- requestGraph.invalidateOnBuildNodes();
1722
- requestGraph.invalidateEnvNodes(options.env);
1723
- requestGraph.invalidateOptionNodes(options);
1914
+ if (getFeatureFlag('verboseRequestInvalidationStats')) {
1915
+ const invalidationStats = await invalidateRequestGraph(
1916
+ requestGraph,
1917
+ options,
1918
+ events,
1919
+ );
1920
+
1921
+ logger.verbose({
1922
+ origin: '@atlaspack/core',
1923
+ message: 'Request track loaded from cache',
1924
+ meta: {
1925
+ ...commonMeta,
1926
+ trackableEvent: 'request_tracker_cache_key_hit',
1927
+ invalidationStats,
1928
+ },
1929
+ });
1930
+ } else {
1931
+ requestGraph.invalidateUnpredictableNodes();
1932
+ requestGraph.invalidateOnBuildNodes();
1933
+ requestGraph.invalidateEnvNodes(options.env);
1934
+ requestGraph.invalidateOptionNodes(options);
1935
+ }
1724
1936
 
1725
- await requestGraph.respondToFSEvents(
1726
- options.unstableFileInvalidations || events,
1727
- options,
1728
- 10000,
1729
- true,
1730
- );
1731
1937
  return requestGraph;
1732
1938
  } catch (e) {
1733
1939
  // Prevent logging fs events took too long warning
@@ -1744,13 +1950,227 @@ async function loadRequestGraph(options): Async<RequestGraph> {
1744
1950
  message:
1745
1951
  'Cache entry for request tracker was not found, initializing a clean cache.',
1746
1952
  meta: {
1747
- cacheKey,
1748
- snapshotKey,
1953
+ ...commonMeta,
1954
+ trackableEvent: 'request_tracker_cache_key_miss',
1749
1955
  },
1750
1956
  });
1751
1957
  return new RequestGraph();
1752
1958
  }
1753
1959
 
1960
+ /**
1961
+ * A wrapper around an invalidation type / method
1962
+ */
1963
+ type InvalidationFn = {|
1964
+ key: string,
1965
+ fn: () =>
1966
+ | InvalidationDetail
1967
+ | Promise<InvalidationDetail>
1968
+ | void
1969
+ | Promise<void>,
1970
+ |};
1971
+
1972
+ type InvalidationStats = {|
1973
+ /**
1974
+ * Total number of request graph nodes
1975
+ */
1976
+ nodeCount: number,
1977
+ /**
1978
+ * Number of requests in RequestGraph
1979
+ */
1980
+ requestCount: number,
1981
+ /**
1982
+ * Number of nodes that have been invalidated.
1983
+ */
1984
+ invalidatedCount: number,
1985
+ /**
1986
+ * Percentage of requests that have been invalidated
1987
+ */
1988
+ requestInvalidationRatio: number,
1989
+ /**
1990
+ * Percentage of nodes that have been invalidated
1991
+ */
1992
+ nodeInvalidationRatio: number,
1993
+ /**
1994
+ * Details for each invalidation type
1995
+ */
1996
+ invalidations: InvalidationFnStats[],
1997
+ |};
1998
+
1999
+ /**
2000
+ * Details about an invalidation.
2001
+ *
2002
+ * If this is a fs events invalidation, this key will contain statistics about invalidations
2003
+ * by path.
2004
+ *
2005
+ * If this is a env or option invalidation, this key will contain the list of changed environment
2006
+ * variables or options.
2007
+ */
2008
+ type InvalidationDetail = string[] | FSInvalidationStats;
2009
+
2010
+ /**
2011
+ * Number of invalidations for a given file-system event.
2012
+ */
2013
+ type FSInvalidation = {|
2014
+ path: string,
2015
+ count: number,
2016
+ |};
2017
+
2018
+ type FSInvalidationStats = {|
2019
+ /**
2020
+ * This list will be sorted by the number of nodes invalidated and only the top 10 will be
2021
+ * included.
2022
+ */
2023
+ biggestInvalidations: FSInvalidation[],
2024
+ |};
2025
+
2026
+ /**
2027
+ * Information about a certain cache invalidation type.
2028
+ */
2029
+ type InvalidationFnStats = {|
2030
+ /**
2031
+ * Invalidation type, one of:
2032
+ *
2033
+ * - unpredictable
2034
+ * - onBuild
2035
+ * - env
2036
+ * - option
2037
+ * - fsEvents
2038
+ */
2039
+ key: string,
2040
+ /**
2041
+ * Number of invalidated nodes coming from this invalidation type.
2042
+ */
2043
+ count: number,
2044
+ /**
2045
+ * If this is a env or option invalidation, this key will contain the list of changed values.
2046
+ *
2047
+ * If this is a fs events invalidation, this key will contain statistics about invalidations
2048
+ */
2049
+ detail: null | InvalidationDetail,
2050
+ /**
2051
+ * Time in milliseconds it took to run the invalidation.
2052
+ */
2053
+ duration: number,
2054
+ |};
2055
+
2056
+ /**
2057
+ * Respond to unpredictable, build, environment changes, option changes and file-system events
2058
+ * invalidating RequestGraph nodes.
2059
+ *
2060
+ * Returns the count of nodes invalidated by each invalidation type.
2061
+ */
2062
+ export async function invalidateRequestGraph(
2063
+ requestGraph: RequestGraph,
2064
+ options: AtlaspackOptions,
2065
+ events: Event[],
2066
+ ): Promise<InvalidationStats> {
2067
+ const invalidationFns: InvalidationFn[] = [
2068
+ {
2069
+ key: 'unpredictable',
2070
+ fn: () => requestGraph.invalidateUnpredictableNodes(),
2071
+ },
2072
+ {
2073
+ key: 'onBuild',
2074
+ fn: () => requestGraph.invalidateOnBuildNodes(),
2075
+ },
2076
+ {
2077
+ key: 'env',
2078
+ fn: () => requestGraph.invalidateEnvNodes(options.env),
2079
+ },
2080
+ {
2081
+ key: 'option',
2082
+ fn: () => requestGraph.invalidateOptionNodes(options),
2083
+ },
2084
+ {
2085
+ key: 'fsEvents',
2086
+ fn: () => invalidateRequestGraphFSEvents(requestGraph, options, events),
2087
+ },
2088
+ ];
2089
+
2090
+ const invalidations = [];
2091
+ for (const invalidation of invalidationFns) {
2092
+ invalidations.push(await runInvalidation(requestGraph, invalidation));
2093
+ }
2094
+ const invalidatedCount = invalidations.reduce(
2095
+ (acc, invalidation) => acc + invalidation.count,
2096
+ 0,
2097
+ );
2098
+ const requestCount = requestGraph.nodes.reduce(
2099
+ (acc, node) => acc + (node?.type === REQUEST ? 1 : 0),
2100
+ 0,
2101
+ );
2102
+ const nodeCount = requestGraph.nodes.length;
2103
+ const nodeInvalidationRatio = invalidatedCount / nodeCount;
2104
+ const requestInvalidationRatio = invalidatedCount / requestCount;
2105
+
2106
+ return {
2107
+ invalidations,
2108
+ nodeCount,
2109
+ requestCount,
2110
+ invalidatedCount,
2111
+ nodeInvalidationRatio,
2112
+ requestInvalidationRatio,
2113
+ };
2114
+ }
2115
+
2116
+ interface InvalidateRequestGraphFSEventsInput {
2117
+ respondToFSEvents(
2118
+ events: Event[],
2119
+ options: AtlaspackOptions,
2120
+ timeout: number,
2121
+ shouldLog: boolean,
2122
+ ): Promise<{invalidationsByPath: Map<string, number>, ...}>;
2123
+ }
2124
+
2125
+ /**
2126
+ * Invalidate the request graph based on file-system events.
2127
+ *
2128
+ * Returns statistics about the invalidations.
2129
+ */
2130
+ export async function invalidateRequestGraphFSEvents(
2131
+ requestGraph: InvalidateRequestGraphFSEventsInput,
2132
+ options: AtlaspackOptions,
2133
+ events: Event[],
2134
+ ): Promise<FSInvalidationStats> {
2135
+ const {invalidationsByPath} = await requestGraph.respondToFSEvents(
2136
+ options.unstableFileInvalidations || events,
2137
+ options,
2138
+ 10000,
2139
+ true,
2140
+ );
2141
+ const biggestInvalidations =
2142
+ getBiggestFSEventsInvalidations(invalidationsByPath);
2143
+
2144
+ return {
2145
+ biggestInvalidations,
2146
+ };
2147
+ }
2148
+
2149
+ interface RunInvalidationInput {
2150
+ getInvalidNodeCount(): number;
2151
+ }
2152
+
2153
+ /**
2154
+ * Runs an invalidation function and reports metrics.
2155
+ */
2156
+ export async function runInvalidation(
2157
+ requestGraph: RunInvalidationInput,
2158
+ invalidationFn: InvalidationFn,
2159
+ ): Promise<InvalidationFnStats> {
2160
+ const start = performance.now();
2161
+ const startInvalidationCount = requestGraph.getInvalidNodeCount();
2162
+ const result = await invalidationFn.fn();
2163
+ const count = requestGraph.getInvalidNodeCount() - startInvalidationCount;
2164
+ const duration = performance.now() - start;
2165
+
2166
+ return {
2167
+ key: invalidationFn.key,
2168
+ count,
2169
+ detail: result ?? null,
2170
+ duration,
2171
+ };
2172
+ }
2173
+
1754
2174
  function logErrorOnBailout(
1755
2175
  options: AtlaspackOptions,
1756
2176
  snapshotPath: string,
@@ -1802,3 +2222,19 @@ export function cleanUpOrphans<N, E: number>(graph: Graph<N, E>): NodeId[] {
1802
2222
 
1803
2223
  return removedNodeIds;
1804
2224
  }
2225
+
2226
+ /**
2227
+ * Returns paths that invalidated the most nodes
2228
+ */
2229
+ export function getBiggestFSEventsInvalidations(
2230
+ invalidationsByPath: Map<string, number>,
2231
+ limit: number = 10,
2232
+ ): Array<FSInvalidation> {
2233
+ const invalidations = [];
2234
+ for (const [path, count] of invalidationsByPath) {
2235
+ invalidations.push({path, count});
2236
+ }
2237
+ invalidations.sort((a, b) => b.count - a.count);
2238
+
2239
+ return invalidations.slice(0, limit);
2240
+ }