Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as path from 'path';
import { Construct } from 'constructs';
import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema';
import { App, CfnParameter, CfnResource, Lazy, Stack, TreeInspector } from 'aws-cdk-lib';
import { TreeFile } from 'aws-cdk-lib/core/lib/private/tree-metadata';

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

expect(readJson(assembly.directory, treeArtifact!.file)).toEqual({
const treeJson = readJson(assembly.directory, treeArtifact!.file);

expect(treeJson).toEqual({
version: 'tree-0.1',
tree: expect.objectContaining({
children: expect.objectContaining({
Expand All @@ -185,6 +188,91 @@ describe('tree metadata', () => {
});
});

/**
* Check that we can limit ourselves to a given tree file size
*
* We can't try the full 512MB because the test process will run out of memory
* before synthing such a large tree.
*/
test('tree.json can be split over multiple files', () => {
const MAX_NODES = 1_000;
const app = new App({
context: {
'@aws-cdk/core.TreeMetadata:maxNodes': MAX_NODES,
},
analyticsReporting: false,
});

// GIVEN
const buildStart = Date.now();
const addedNodes = recurseBuild(app, 4, 4);
// eslint-disable-next-line no-console
console.log('Built tree in', Date.now() - buildStart, 'ms');

// WHEN
const synthStart = Date.now();
const assembly = app.synth();
// eslint-disable-next-line no-console
console.log('Synthed tree in', Date.now() - synthStart, 'ms');
try {
const treeArtifact = assembly.tree();
expect(treeArtifact).toBeDefined();

// THEN - does not explode, and file sizes are correctly limited
const sizes: Record<string, number> = {};
recurseVisit(assembly.directory, treeArtifact!.file, sizes);

for (const size of Object.values(sizes)) {
expect(size).toBeLessThanOrEqual(MAX_NODES);
}

expect(Object.keys(sizes).length).toBeGreaterThan(1);

const foundNodes = sum(Object.values(sizes));
expect(foundNodes).toEqual(addedNodes + 2); // App, Tree
} finally {
fs.rmSync(assembly.directory, { force: true, recursive: true });
}

function recurseBuild(scope: Construct, n: number, depth: number) {
if (depth === 0) {
const resourceCount = 450;
const stack = new Stack(scope, 'SomeStack');
for (let i = 0; i < resourceCount; i++) {
new CfnResource(stack, `Resource${i}`, { type: 'Aws::Some::Resource' });
}
return resourceCount + 3; // Also count Stack, BootstrapVersion, CheckBootstrapVersion
}

let ret = 0;
for (let i = 0; i < n; i++) {
const parent = new Construct(scope, `Construct${i}`);
ret += 1;
ret += recurseBuild(parent, n, depth - 1);
}
return ret;
}

function recurseVisit(directory: string, fileName: string, files: Record<string, number>) {
let nodes = 0;
const treeJson: TreeFile = readJson(directory, fileName);
rec(treeJson.tree);
files[fileName] = nodes;

function rec(x: TreeFile['tree']) {
if (isSubtreeReference(x)) {
// We'll count this node as part of our visit to the "real" node
recurseVisit(directory, x.fileName, files);
} else {
nodes += 1;
for (const child of Object.values(x.children ?? {})) {
rec(child);
}
}
}
}
});

test('token resolution & cfn parameter', () => {
const app = new App();
const stack = new Stack(app, 'mystack');
Expand Down Expand Up @@ -396,3 +484,15 @@ describe('tree metadata', () => {
function readJson(outdir: string, file: string) {
return JSON.parse(fs.readFileSync(path.join(outdir, file), 'utf-8'));
}

function isSubtreeReference(x: TreeFile['tree']): x is Extract<TreeFile['tree'], { fileName: string }> {
return !!(x as any).fileName;
}

function sum(xs: number[]) {
let ret = 0;
for (const x of xs) {
ret += x;
}
return ret;
}
22 changes: 22 additions & 0 deletions packages/aws-cdk-lib/core/lib/private/construct-iteration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IConstruct } from 'constructs';
import { LinkedQueue } from './linked-queue';

/**
* Breadth-first iterator over the construct tree
*/
export function* iterateBfs(root: IConstruct) {
// Use a specialized queue data structure. Using `Array.shift()`
// has a huge performance penalty (difference on the order of
// ~50ms vs ~1s to iterate a large construct tree)
const queue = new LinkedQueue<{ construct: IConstruct; parent: IConstruct | undefined }>([{ construct: root, parent: undefined }]);

let next = queue.shift();
while (next) {
for (const child of next.construct.node.children) {
queue.push({ construct: child, parent: next.construct });
}
yield next;

next = queue.shift();
}
}
45 changes: 45 additions & 0 deletions packages/aws-cdk-lib/core/lib/private/linked-queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few improvements

  1. Length tracking
private _length = 0;
public get length() {
  return this._length;
}
  1. isEmpty check
public isEmpty(): boolean {
  return this.head === undefined;
}
  1. clear method
public clear() {
  this.head = undefined;
  this.last = undefined;
  this._length = 0;
}
  1. Iterator support
public *[Symbol.iterator](): IterableIterator<A> {
  let current = this.head;
  while (current) {
    yield current.value;
    current = current.next;
  }
}

for use as

const q = new LinkedQueue<number>([1, 2, 3]);
for (const item of q) {
  console.log(item); // 1, 2, 3
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite comfortable adding these methods that we wouldn't be using.

  • length/isEmpty: sure, I guess. Though if possible I prefer coding style of the form "do, then check" rather than "will next call succeed / do next call". This is a style that will avoid TOCTOU's without having to constantly mentally think about whether you are in a situation where it does or does not apply.
  • clear: not using it, so why add it?
  • iterator: I'm not comfortable writing a for loop over a data structure that gets mutated. I understand we could code the list to make that work, but I would get uneasy seeing the client side of that code, since it relies on too many unknowns.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment was to make it a more general purpose ds. This is great for the current ask.

* A queue that is faster than an array at large throughput
*/
export class LinkedQueue<A> {
private head?: Node<A>;
private last?: Node<A>;

constructor(items?: Iterable<A>) {
if (items) {
for (const x of items) {
this.push(x);
}
}
}

public push(value: A) {
const node: Node<A> = { value };
if (this.head && this.last) {
this.last.next = node;
this.last = node;
} else {
this.head = node;
this.last = node;
}
}

public shift(): A | undefined {
if (!this.head) {
return undefined;
}
const ret = this.head.value;

this.head = this.head.next;
if (!this.head) {
this.last = undefined;
}

return ret;
}
}

interface Node<A> {
value: A;
next?: Node<A>;
}
Loading
Loading