Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
28 changes: 28 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,28 @@
import { IConstruct } from 'constructs';

/**
* Breadth-first iterator over the construct tree
Copy link
Contributor

Choose a reason for hiding this comment

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

You mean Depth-first?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes, copy/paste-o. Thanks.

*
* Replaces `node.findAll()` which both uses recursive function
* calls and accumulates into an array, both of which are much slower
* than this solution.
*/
export function* iterateDfsPreorder(root: IConstruct) {
Copy link
Contributor

Choose a reason for hiding this comment

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

[q]: preorder would be visiting left children before right one, which is not what this method does. Was that intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Preorder means parent before children, which this method does do.

// 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: IConstruct[] = [root];
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be called stack (filo), a queue would be (fifo)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure.


let next = queue.pop();
while (next) {
// Get at the construct internals to get at the children faster
// const children: Record<string, IConstruct> = (next.construct.node as any)._children;
for (const child of next.node.children) {
queue.push(child);
}
yield next;

next = queue.pop();
}
}

21 changes: 14 additions & 7 deletions packages/aws-cdk-lib/core/lib/private/prepare-app.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ConstructOrder, Dependable, IConstruct } from 'constructs';
import { Dependable, IConstruct } from 'constructs';
import { resolveReferences } from './refs';
import { CfnResource } from '../cfn-resource';
import { Stack } from '../stack';
import { Stage } from '../stage';
import { iterateDfsPreorder } from './construct-iteration';

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

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

result.reverse();
return result;
}

/**
* Find all resources in a set of constructs
*/
function findCfnResources(root: IConstruct): CfnResource[] {
return root.node.findAll().filter(CfnResource.isCfnResource);
function* findCfnResources(root: IConstruct): IterableIterator<CfnResource> {
for (const node of iterateDfsPreorder(root)) {
if (CfnResource.isCfnResource(node)) {
yield node;
}
}
}

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

for (const source of root.node.findAll()) {
for (const source of iterateDfsPreorder(root)) {
for (const dependable of source.node.dependencies) {
for (const target of Dependable.of(dependable).dependencyRoots) {
let foundTargets = found.get(source);
Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/core/lib/private/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { IResolvable } from '../resolvable';
import { Stack } from '../stack';
import { Token, Tokenization } from '../token';
import { ResolutionTypeHint } from '../type-hints';
import { iterateDfsPreorder } from './construct-iteration';

export const STRING_LIST_REFERENCE_DELIMITER = '||';

Expand Down Expand Up @@ -152,7 +153,7 @@ function renderReference(ref: CfnReference) {
*/
function findAllReferences(root: IConstruct) {
const result = new Array<{ source: CfnElement; value: CfnReference }>();
for (const consumer of root.node.findAll()) {
for (const consumer of iterateDfsPreorder(root)) {
// include only CfnElements (i.e. resources)
if (!CfnElement.isCfnElement(consumer)) {
continue;
Expand Down
50 changes: 25 additions & 25 deletions packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import { Node, IConstruct } from 'constructs';
import { ISynthesisSession } from './types';
import * as cxschema from '../../../cloud-assembly-schema';
Expand Down Expand Up @@ -70,13 +72,20 @@ export function addStackArtifactToAssembly(
...stackNameProperty,
};

const metaFile = path.join(session.assembly.outdir, `${stack.artifactId}.metadata.json`);
const hasMeta = Object.keys(meta).length > 0;

if (hasMeta) {
fs.writeFileSync(metaFile, JSON.stringify(meta, undefined, 2), 'utf-8');
}

// add an artifact that represents this stack
session.assembly.addArtifact(stack.artifactId, {
type: cxschema.ArtifactType.AWS_CLOUDFORMATION_STACK,
environment: stack.environment,
properties,
dependencies: deps.length > 0 ? deps : undefined,
metadata: Object.keys(meta).length > 0 ? meta : undefined,
metadataFile: hasMeta ? metaFile : undefined,
displayName: stack.node.path,
});
}
Expand All @@ -87,17 +96,24 @@ export function addStackArtifactToAssembly(
function collectStackMetadata(stack: Stack) {
const output: { [id: string]: cxschema.MetadataEntry[] } = { };

visit(stack);

return output;
const queue: IConstruct[] = [stack];

function visit(node: IConstruct) {
// break off if we reached a node that is not a child of this stack
const parent = findParentStack(node);
if (parent !== stack) {
return;
let next = queue.shift();
while (next) {
// break off if we reached a Stack construct that is not a NestedStack
if (Stack.isStack(next) && next !== stack && next.nestedStackParent === undefined) {
continue;
}

handleNode(next);

queue.push(...next.node.children);
next = queue.shift();
}

return output;

function handleNode(node: IConstruct) {
if (node.node.metadata.length > 0) {
// Make the path absolute
output[Node.PATH_SEP + node.node.path] = node.node.metadata.map(md => {
Expand All @@ -118,22 +134,6 @@ function collectStackMetadata(stack: Stack) {
return resolved as cxschema.MetadataEntry;
});
}

for (const child of node.node.children) {
visit(child);
}
}

function findParentStack(node: IConstruct): Stack | undefined {
if (Stack.isStack(node) && node.nestedStackParent === undefined) {
return node;
}

if (!node.node.scope) {
return undefined;
}

return findParentStack(node.node.scope);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Construct, IConstruct } from 'constructs';
import { App } from '../../app';
import { CfnResource } from '../../cfn-resource';
import { constructInfoFromConstruct } from '../../helpers-internal';
import { iterateDfsPreorder } from '../../private/construct-iteration';
import { Stack } from '../../stack';

/**
Expand Down Expand Up @@ -77,24 +78,24 @@ export class ConstructTree {
this._constructByPath.set(this.root.node.path, root);
// do this once at the start so we don't have to traverse
// the entire tree everytime we want to find a nested node
this.root.node.findAll().forEach(child => {
for (const child of iterateDfsPreorder(this.root)) {
this._constructByPath.set(child.node.path, child);
const defaultChild = child.node.defaultChild;
if (defaultChild && CfnResource.isCfnResource(defaultChild)) {
const stack = Stack.of(defaultChild);
const logicalId = stack.resolve(defaultChild.logicalId);
this.setLogicalId(stack, logicalId, child);
}
});
}

// Another pass to include all the L1s that haven't been added yet
this.root.node.findAll().forEach(child => {
for (const child of iterateDfsPreorder(this.root)) {
if (CfnResource.isCfnResource(child)) {
const stack = Stack.of(child);
const logicalId = Stack.of(child).resolve(child.logicalId);
this.setLogicalId(stack, logicalId, child);
}
});
}
}

private setLogicalId(stack: Stack, logicalId: string, child: Construct) {
Expand Down
40 changes: 40 additions & 0 deletions packages/aws-cdk-lib/core/test/synthesis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,46 @@ describe('synthesis', () => {
Template.fromStack(stack);
}).toThrow('Synthesis has been called multiple times and the construct tree was modified after the first synthesis');
});

test('performance', () => {
const MAX_NODES = 1_000;
const app = new cdk.App({
context: {
'@aws-cdk/core.TreeMetadata:maxNodes': MAX_NODES,
},
});

// GIVEN
const buildStart = Date.now();
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 {
} finally {
fs.rmSync(assembly.directory, { force: true, recursive: true });
}

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

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

function list(outdir: string) {
Expand Down
37 changes: 31 additions & 6 deletions packages/aws-cdk-lib/cx-api/lib/cloud-artifact.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import * as fs from 'fs';
import { join } from 'path';
import type { CloudAssembly } from './cloud-assembly';
import { MetadataEntryResult, SynthesisMessage, SynthesisMessageLevel } from './metadata';
import * as cxschema from '../../cloud-assembly-schema';
import { CloudAssemblyError } from './private/error';
import { UnscopedValidationError } from '../../core';

/**
* Artifact properties for CloudFormation stacks.
Expand Down Expand Up @@ -70,6 +73,8 @@ export class CloudArtifact {
*/
private _deps?: CloudArtifact[];

private _metaCache?: Metadata;

protected constructor(public readonly assembly: CloudAssembly, public readonly id: string, manifest: cxschema.ArtifactManifest) {
this.manifest = manifest;
this.messages = this.renderMessages();
Expand Down Expand Up @@ -97,17 +102,35 @@ export class CloudArtifact {
* @returns all the metadata entries of a specific type in this artifact.
*/
public findMetadataByType(type: string): MetadataEntryResult[] {
let metadata: Metadata;
if (this.manifest.metadataFile) {
metadata = this.loadMetadataFromFile();
} else {
metadata = this.manifest.metadata ?? {};
}

const result = new Array<MetadataEntryResult>();
for (const path of Object.keys(this.manifest.metadata || {})) {
for (const entry of (this.manifest.metadata || {})[path]) {
if (entry.type === type) {
result.push({ path, ...entry });
}
}
for (const [path, entries] of Object.entries(this.manifest.metadata ?? {})) {
result.push(...entries
.filter(e => e.type === type)
.map(e => ({ path, ...e })));
}
return result;
}

private loadMetadataFromFile(): Metadata {
if (!this.manifest.metadataFile) {
throw new UnscopedValidationError('Should have had a file');
}

if (!this._metaCache) {
const meta = JSON.parse(fs.readFileSync(join(this.assembly.directory, this.manifest.metadataFile), 'utf-8'));
this._metaCache = meta as NonNullable<typeof this._metaCache>;
}

return this._metaCache;
}

private renderMessages() {
const messages = new Array<SynthesisMessage>();

Expand Down Expand Up @@ -144,3 +167,5 @@ export class CloudArtifact {
return this.manifest.displayName ?? this.id;
}
}

type Metadata = Record<string, cxschema.MetadataEntry[]>;
Loading