Skip to content

Commit 35ebdaa

Browse files
committed
fix: synthesis fails if tree.json exceeds 512MB
1 parent e2aa338 commit 35ebdaa

File tree

12 files changed

+637
-77
lines changed

12 files changed

+637
-77
lines changed

packages/@aws-cdk-testing/framework-integ/test/core/test/tree-metadata.test.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as path from 'path';
66
import { Construct } from 'constructs';
77
import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema';
88
import { App, CfnParameter, CfnResource, Lazy, Stack, TreeInspector } from 'aws-cdk-lib';
9+
import { TreeFile } from 'aws-cdk-lib/core/lib/private/tree-metadata';
910

1011
abstract class AbstractCfnResource extends CfnResource {
1112
constructor(scope: Construct, id: string) {
@@ -162,7 +163,9 @@ describe('tree metadata', () => {
162163
const treeArtifact = assembly.tree();
163164
expect(treeArtifact).toBeDefined();
164165

165-
expect(readJson(assembly.directory, treeArtifact!.file)).toEqual({
166+
const treeJson = readJson(assembly.directory, treeArtifact!.file);
167+
168+
expect(treeJson).toEqual({
166169
version: 'tree-0.1',
167170
tree: expect.objectContaining({
168171
children: expect.objectContaining({
@@ -185,6 +188,91 @@ describe('tree metadata', () => {
185188
});
186189
});
187190

191+
/**
192+
* Check that we can limit ourselves to a given tree file size
193+
*
194+
* We can't try the full 512MB because the test process will run out of memory
195+
* before synthing such a large tree.
196+
*/
197+
test('tree.json can be split over multiple files', () => {
198+
const MAX_NODES = 1_000;
199+
const app = new App({
200+
context: {
201+
'@aws-cdk/core.TreeMetadata:maxNodes': MAX_NODES,
202+
},
203+
analyticsReporting: false,
204+
});
205+
206+
// GIVEN
207+
const buildStart = Date.now();
208+
const addedNodes = recurseBuild(app, 4, 4);
209+
// eslint-disable-next-line no-console
210+
console.log('Built tree in', Date.now() - buildStart, 'ms');
211+
212+
// WHEN
213+
const synthStart = Date.now();
214+
const assembly = app.synth();
215+
// eslint-disable-next-line no-console
216+
console.log('Synthed tree in', Date.now() - synthStart, 'ms');
217+
try {
218+
const treeArtifact = assembly.tree();
219+
expect(treeArtifact).toBeDefined();
220+
221+
// THEN - does not explode, and file sizes are correctly limited
222+
const sizes: Record<string, number> = {};
223+
recurseVisit(assembly.directory, treeArtifact!.file, sizes);
224+
225+
for (const size of Object.values(sizes)) {
226+
expect(size).toBeLessThanOrEqual(MAX_NODES);
227+
}
228+
229+
expect(Object.keys(sizes).length).toBeGreaterThan(1);
230+
231+
const foundNodes = sum(Object.values(sizes));
232+
expect(foundNodes).toEqual(addedNodes + 2); // App, Tree
233+
} finally {
234+
fs.rmSync(assembly.directory, { force: true, recursive: true });
235+
}
236+
237+
function recurseBuild(scope: Construct, n: number, depth: number) {
238+
if (depth === 0) {
239+
const resourceCount = 450;
240+
const stack = new Stack(scope, 'SomeStack');
241+
for (let i = 0; i < resourceCount; i++) {
242+
new CfnResource(stack, `Resource${i}`, { type: 'Aws::Some::Resource' });
243+
}
244+
return resourceCount + 3; // Also count Stack, BootstrapVersion, CheckBootstrapVersion
245+
}
246+
247+
let ret = 0;
248+
for (let i = 0; i < n; i++) {
249+
const parent = new Construct(scope, `Construct${i}`);
250+
ret += 1;
251+
ret += recurseBuild(parent, n, depth - 1);
252+
}
253+
return ret;
254+
}
255+
256+
function recurseVisit(directory: string, fileName: string, files: Record<string, number>) {
257+
let nodes = 0;
258+
const treeJson: TreeFile = readJson(directory, fileName);
259+
rec(treeJson.tree);
260+
files[fileName] = nodes;
261+
262+
function rec(x: TreeFile['tree']) {
263+
if (isSubtreeReference(x)) {
264+
// We'll count this node as part of our visit to the "real" node
265+
recurseVisit(directory, x.fileName, files);
266+
} else {
267+
nodes += 1;
268+
for (const child of Object.values(x.children ?? {})) {
269+
rec(child);
270+
}
271+
}
272+
}
273+
}
274+
});
275+
188276
test('token resolution & cfn parameter', () => {
189277
const app = new App();
190278
const stack = new Stack(app, 'mystack');
@@ -396,3 +484,15 @@ describe('tree metadata', () => {
396484
function readJson(outdir: string, file: string) {
397485
return JSON.parse(fs.readFileSync(path.join(outdir, file), 'utf-8'));
398486
}
487+
488+
function isSubtreeReference(x: TreeFile['tree']): x is Extract<TreeFile['tree'], { fileName: string }> {
489+
return !!(x as any).fileName;
490+
}
491+
492+
function sum(xs: number[]) {
493+
let ret = 0;
494+
for (const x of xs) {
495+
ret += x;
496+
}
497+
return ret;
498+
}

packages/aws-cdk-lib/aws-ecs-patterns/test/fargate/load-balanced-fargate-service-v2.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ describe('Application Load Balancer', () => {
465465
enableExecuteCommand: true,
466466
loadBalancers: [
467467
{
468-
name: 'lb',
468+
name: 'lb_xyz',
469469
idleTimeout: Duration.seconds(400),
470470
domainName: 'api.example.com',
471471
domainZone: new PublicHostedZone(stack, 'HostedZone', { zoneName: 'example.com' }),
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { IConstruct } from 'constructs';
2+
import { LinkedQueue } from './linked-queue';
3+
4+
/**
5+
* Breadth-first iterator over the construct tree
6+
*
7+
* Replaces `node.findAll()` which both uses recursive function
8+
* calls and accumulates into an array, both of which are much slower
9+
* than this solution.
10+
*/
11+
export function* iterateDfsPreorder(root: IConstruct) {
12+
// Use a specialized queue data structure. Using `Array.shift()`
13+
// has a huge performance penalty (difference on the order of
14+
// ~50ms vs ~1s to iterate a large construct tree)
15+
const queue: IConstruct[] = [root];
16+
17+
let next = queue.pop();
18+
while (next) {
19+
// Get at the construct internals to get at the children faster
20+
// const children: Record<string, IConstruct> = (next.construct.node as any)._children;
21+
for (const child of next.node.children) {
22+
queue.push(child);
23+
}
24+
yield next;
25+
26+
next = queue.pop();
27+
}
28+
}
29+
30+
/**
31+
* Breadth-first iterator over the construct tree
32+
*/
33+
export function* iterateBfs(root: IConstruct) {
34+
// Use a specialized queue data structure. Using `Array.shift()`
35+
// has a huge performance penalty (difference on the order of
36+
// ~50ms vs ~1s to iterate a large construct tree)
37+
const queue = new LinkedQueue<{ construct: IConstruct; parent: IConstruct | undefined }>([{ construct: root, parent: undefined }]);
38+
39+
let next = queue.shift();
40+
while (next) {
41+
for (const child of next.construct.node.children) {
42+
queue.push({ construct: child, parent: next.construct });
43+
}
44+
yield next;
45+
46+
next = queue.shift();
47+
}
48+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* A queue that is faster than an array at large throughput
3+
*/
4+
export class LinkedQueue<A> {
5+
private head?: Node<A>;
6+
private last?: Node<A>;
7+
8+
constructor(items?: Iterable<A>) {
9+
if (items) {
10+
for (const x of items) {
11+
this.push(x);
12+
}
13+
}
14+
}
15+
16+
public push(value: A) {
17+
const node: Node<A> = { value };
18+
if (this.head && this.last) {
19+
this.last.next = node;
20+
this.last = node;
21+
} else {
22+
this.head = node;
23+
this.last = node;
24+
}
25+
}
26+
27+
public shift(): A | undefined {
28+
if (!this.head) {
29+
return undefined;
30+
}
31+
const ret = this.head.value;
32+
33+
this.head = this.head.next;
34+
if (!this.head) {
35+
this.last = undefined;
36+
}
37+
38+
return ret;
39+
}
40+
}
41+
42+
interface Node<A> {
43+
value: A;
44+
next?: Node<A>;
45+
}

packages/aws-cdk-lib/core/lib/private/prepare-app.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { ConstructOrder, Dependable, IConstruct } from 'constructs';
1+
import { Dependable, IConstruct } from 'constructs';
22
import { resolveReferences } from './refs';
33
import { CfnResource } from '../cfn-resource';
44
import { Stack } from '../stack';
55
import { Stage } from '../stage';
6+
import { iterateDfsPreorder } from './construct-iteration';
67

78
/**
89
* Prepares the app for synthesis. This function is called by the root `prepare`
@@ -83,20 +84,26 @@ function findAllNestedStacks(root: IConstruct) {
8384

8485
// create a list of all nested stacks in depth-first post order this means
8586
// that we first prepare the leaves and then work our way up.
86-
for (const stack of root.node.findAll(ConstructOrder.POSTORDER /* <== important */)) {
87-
if (includeStack(stack)) {
88-
result.push(stack);
87+
// Build a preorder list then reverse it
88+
for (const node of iterateDfsPreorder(root)) {
89+
if (includeStack(node)) {
90+
result.push(node);
8991
}
9092
}
9193

94+
result.reverse();
9295
return result;
9396
}
9497

9598
/**
9699
* Find all resources in a set of constructs
97100
*/
98-
function findCfnResources(root: IConstruct): CfnResource[] {
99-
return root.node.findAll().filter(CfnResource.isCfnResource);
101+
function* findCfnResources(root: IConstruct): IterableIterator<CfnResource> {
102+
for (const node of iterateDfsPreorder(root)) {
103+
if (CfnResource.isCfnResource(node)) {
104+
yield node;
105+
}
106+
}
100107
}
101108

102109
interface INestedStackPrivateApi {
@@ -110,7 +117,7 @@ function findTransitiveDeps(root: IConstruct): Dependency[] {
110117
const found = new Map<IConstruct, Set<IConstruct>>(); // Deduplication map
111118
const ret = new Array<Dependency>();
112119

113-
for (const source of root.node.findAll()) {
120+
for (const source of iterateDfsPreorder(root)) {
114121
for (const dependable of source.node.dependencies) {
115122
for (const target of Dependable.of(dependable).dependencyRoots) {
116123
let foundTargets = found.get(source);

packages/aws-cdk-lib/core/lib/private/refs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { IResolvable } from '../resolvable';
1919
import { Stack } from '../stack';
2020
import { Token, Tokenization } from '../token';
2121
import { ResolutionTypeHint } from '../type-hints';
22+
import { iterateDfsPreorder } from './construct-iteration';
2223

2324
export const STRING_LIST_REFERENCE_DELIMITER = '||';
2425

@@ -152,7 +153,7 @@ function renderReference(ref: CfnReference) {
152153
*/
153154
function findAllReferences(root: IConstruct) {
154155
const result = new Array<{ source: CfnElement; value: CfnReference }>();
155-
for (const consumer of root.node.findAll()) {
156+
for (const consumer of iterateDfsPreorder(root)) {
156157
// include only CfnElements (i.e. resources)
157158
if (!CfnElement.isCfnElement(consumer)) {
158159
continue;

0 commit comments

Comments
 (0)