Skip to content

Commit e05f28a

Browse files
authored
Merge branch 'main' into eks-133
2 parents e05c22d + 79a3389 commit e05f28a

File tree

3 files changed

+145
-32
lines changed

3 files changed

+145
-32
lines changed

.github/workflows/pr-linter.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
steps:
3737
- name: 'Download workflow_run artifact'
3838
if: github.event_name == 'workflow_run'
39-
uses: dawidd6/action-download-artifact@v10
39+
uses: dawidd6/action-download-artifact@v11
4040
with:
4141
run_id: ${{ github.event.workflow_run.id }}
4242
name: pr_info

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

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import * as fs from 'fs';
55
import * as path from 'path';
66
import { Construct } from 'constructs';
77
import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema';
8-
import { App, CfnParameter, CfnResource, Lazy, Stack, TreeInspector } from 'aws-cdk-lib';
9-
import { TreeFile } from 'aws-cdk-lib/core/lib/private/tree-metadata';
8+
import { App, AssumptionError, CfnParameter, CfnResource, Lazy, Stack, TreeInspector } from 'aws-cdk-lib';
9+
import { ForestFile, TreeFile } from 'aws-cdk-lib/core/lib/private/tree-metadata';
1010

1111
abstract class AbstractCfnResource extends CfnResource {
1212
constructor(scope: Construct, id: string) {
@@ -219,16 +219,31 @@ describe('tree metadata', () => {
219219
expect(treeArtifact).toBeDefined();
220220

221221
// THEN - does not explode, and file sizes are correctly limited
222-
const sizes: Record<string, number> = {};
223-
recurseVisit(assembly.directory, treeArtifact!.file, sizes);
222+
const treeSizes: Record<string, number> = {};
223+
recurseVisit(assembly.directory, treeArtifact!.file, undefined, treeSizes);
224+
225+
// `treeSizes` has keys of the form `<file>#<tree id>`. Combine them.
226+
const fileSizes: Record<string, number> = {};
227+
for (const [treeName, count] of Object.entries(treeSizes)) {
228+
const fileName = treeName.split('#')[0];
229+
if (fileName in fileSizes) {
230+
fileSizes[fileName] += count;
231+
} else {
232+
fileSizes[fileName ] = count;
233+
}
234+
}
224235

225-
for (const size of Object.values(sizes)) {
236+
for (const size of Object.values(treeSizes)) {
237+
expect(size).toBeLessThanOrEqual(MAX_NODES);
238+
}
239+
for (const size of Object.values(fileSizes)) {
226240
expect(size).toBeLessThanOrEqual(MAX_NODES);
227241
}
228242

229-
expect(Object.keys(sizes).length).toBeGreaterThan(1);
243+
expect(Object.keys(treeSizes).length).toBeGreaterThan(1);
244+
expect(Object.keys(fileSizes).length).toBeLessThan(Object.keys(treeSizes).length);
230245

231-
const foundNodes = sum(Object.values(sizes));
246+
const foundNodes = sum(Object.values(treeSizes));
232247
expect(foundNodes).toEqual(addedNodes + 2); // App, Tree
233248
} finally {
234249
fs.rmSync(assembly.directory, { force: true, recursive: true });
@@ -253,16 +268,29 @@ describe('tree metadata', () => {
253268
return ret;
254269
}
255270

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;
271+
function recurseVisit(directory: string, fileName: string, treeId: string | undefined, files: Record<string, number>) {
272+
let nodes: number;
273+
274+
if (treeId) {
275+
// Assume a forest file
276+
const treeJson: ForestFile = readJson(directory, fileName);
277+
for (const [tid, tree] of Object.entries(treeJson.forest)) {
278+
nodes = 0;
279+
rec(tree);
280+
files[`${fileName}#${tid}`] = nodes;
281+
}
282+
} else {
283+
// Assume a tree file
284+
const treeJson: TreeFile = readJson(directory, fileName);
285+
nodes = 0;
286+
rec(treeJson.tree);
287+
files[fileName] = nodes;
288+
}
261289

262290
function rec(x: TreeFile['tree']) {
263291
if (isSubtreeReference(x)) {
264292
// We'll count this node as part of our visit to the "real" node
265-
recurseVisit(directory, x.fileName, files);
293+
recurseVisit(directory, x.fileName, x.treeId, files);
266294
} else {
267295
nodes += 1;
268296
for (const child of Object.values(x.children ?? {})) {
@@ -437,7 +465,7 @@ describe('tree metadata', () => {
437465
test('failing nodes', () => {
438466
class MyCfnResource extends CfnResource {
439467
public inspect(_: TreeInspector) {
440-
throw new Error('Forcing an inspect error');
468+
throw new AssumptionError('Forcing an inspect error');
441469
}
442470
}
443471

packages/aws-cdk-lib/core/lib/private/tree-metadata.ts

Lines changed: 102 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ export interface TreeFile {
113113
tree: TreeNode;
114114
}
115115

116+
export interface ForestFile {
117+
version: 'forest-0.1';
118+
forest: Record<string, TreeNode>;
119+
}
120+
116121
type TreeNode = Node | SubTreeReference;
117122

118123
/**
@@ -121,7 +126,15 @@ type TreeNode = Node | SubTreeReference;
121126
interface SubTreeReference {
122127
readonly id: string;
123128
readonly path: string;
124-
readonly fileName: string;
129+
fileName: string;
130+
131+
/**
132+
* If set, indicates the subtree in the forest file
133+
*
134+
* If this is set then `fileName` must point to a ForestFile, and this indicates
135+
* the tree inside the forest.
136+
*/
137+
treeId?: string;
125138
}
126139

127140
/**
@@ -144,11 +157,29 @@ interface SubTreeReference {
144157
* leaves in-place to a different node type will (probably) minimally change
145158
* the size of the tree, whereas adding C more children that will all become
146159
* references to substrees will add an unpredictable size to the tree.
160+
* - Because doing this will end up with a lot of subtrees (as many as there are leaf nodes
161+
* in the trees, which is degree ^ depth), writing those all to individual files creates
162+
* A LOT of files which is undesirable for people looking at the directory. We will
163+
* accumulate subtrees into "forest" files which each hold a set of trees, identified
164+
* by a filename and a tree ID. We allocate them by first building the individual trees,
165+
* then accumulating subtrees up to a node count into forests, and then writing the
166+
* name of the forest file and the tree ID into the node that points to the subtree.
147167
*
148168
* Here's a sense of the numbers: a project with 277k nodes leads to an 136M JSON
149169
* file (490 bytes/node). We'll estimate the size of a node to be 1000 bytes.
150170
*/
151171
class FragmentedTreeWriter {
172+
/**
173+
* We only care about the identify of this object.
174+
*
175+
* Whatever tree in the forest is "pointed to" by this pointer is the main tree.
176+
*/
177+
private readonly mainTreePointer: SubTreeReference = {
178+
fileName: 'yyy',
179+
id: 'id',
180+
path: 'path',
181+
};
182+
152183
private readonly forest = new Array<Tree>();
153184

154185
/**
@@ -168,8 +199,6 @@ class FragmentedTreeWriter {
168199

169200
private readonly maxNodes: number;
170201

171-
private subtreeCtr = 1;
172-
173202
constructor(private readonly outdir: string, private readonly rootFilename: string, options?: FragmentedTreeWriterOptions) {
174203
this.maxNodes = options?.maxNodesPerTree ?? 500_000;
175204
}
@@ -178,14 +207,59 @@ class FragmentedTreeWriter {
178207
* Write the forest to disk, return the root file name
179208
*/
180209
public writeForest(): string {
181-
for (const tree of this.forest) {
182-
const treeFile: TreeFile = { version: 'tree-0.1', tree: tree.root };
183-
fs.writeFileSync(path.join(this.outdir, tree.filename), JSON.stringify(treeFile), { encoding: 'utf-8' });
210+
const forestFiles = this.allocateSubTreesToForestFiles();
211+
212+
// We can now write the forest files, and the main file.
213+
const mainTree = this.forest.find(t => t.referencingNode === this.mainTreePointer);
214+
if (mainTree) {
215+
const treeFile: TreeFile = { version: 'tree-0.1', tree: mainTree.root };
216+
fs.writeFileSync(path.join(this.outdir, this.rootFilename), JSON.stringify(treeFile), { encoding: 'utf-8' });
217+
}
218+
219+
for (const forestFile of forestFiles) {
220+
fs.writeFileSync(path.join(this.outdir, forestFile.fileName), JSON.stringify(forestFile.file), { encoding: 'utf-8' });
184221
}
185222

186223
return this.rootFilename;
187224
}
188225

226+
/**
227+
* Find all non-main tree and combine them into forest files
228+
*
229+
* This will mutate the pointing nodes as a side effect.
230+
*/
231+
private allocateSubTreesToForestFiles(): IncompleteForestFile[] {
232+
// First, find all non-main trees and allocate them to forests.
233+
const ret = new Array<IncompleteForestFile>();
234+
235+
for (const tree of this.forest) {
236+
if (tree.referencingNode === this.mainTreePointer) {
237+
// Main tree, not interesting for the purposes of allocating subtrees to forests
238+
continue;
239+
}
240+
241+
let targetForest: typeof ret[0];
242+
if (ret.length === 0 || ret[ret.length - 1].nodeCount + tree.nodes > this.maxNodes) {
243+
targetForest = {
244+
fileName: `trees-${ret.length + 1}.json`,
245+
file: { version: 'forest-0.1', forest: { } },
246+
nodeCount: 0,
247+
};
248+
ret.push(targetForest);
249+
} else {
250+
targetForest = ret[ret.length - 1];
251+
}
252+
253+
const treeId = `t${Object.keys(targetForest.file.forest).length}`;
254+
targetForest.file.forest[treeId] = tree.root;
255+
targetForest.nodeCount += tree.nodes;
256+
tree.referencingNode.fileName = targetForest.fileName;
257+
tree.referencingNode.treeId = treeId;
258+
}
259+
260+
return ret;
261+
}
262+
189263
public addNode(construct: IConstruct, parent: IConstruct | undefined, node: Node) {
190264
// NOTE: we could copy the 'node' object to be safe against tampering, but we trust
191265
// the consuming code so we know we don't need to.
@@ -195,7 +269,7 @@ class FragmentedTreeWriter {
195269
throw new AssumptionError('Can only add exactly one node without a parent');
196270
}
197271

198-
this.addNewTree(node, this.rootFilename);
272+
this.addNewTree(node, this.mainTreePointer);
199273
} else {
200274
// There was a provision in the old code for missing parents, so we're just going to ignore it
201275
// if we can't find a parent.
@@ -213,10 +287,10 @@ class FragmentedTreeWriter {
213287
/**
214288
* Add a new tree with the given Node as root
215289
*/
216-
private addNewTree(root: Node, filename: string): Tree {
290+
private addNewTree(root: Node, referencingNode: SubTreeReference): Tree {
217291
const tree: Tree = {
218292
root,
219-
filename,
293+
referencingNode: referencingNode,
220294
nodes: nodeCount(root),
221295
};
222296

@@ -240,13 +314,15 @@ class FragmentedTreeWriter {
240314
throw new AssumptionError(`Could not find parent of ${JSON.stringify(parent)}`);
241315
}
242316

243-
tree = this.addNewTree(parent, `tree-${this.subtreeCtr++}.json`);
244-
245-
setChild(grandParent, {
317+
const subtreeReference: SubTreeReference = {
246318
id: parent.id,
247319
path: parent.path,
248-
fileName: tree.filename,
249-
} satisfies SubTreeReference);
320+
fileName: 'xxx', // Will be replaced later
321+
};
322+
323+
tree = this.addNewTree(parent, subtreeReference);
324+
325+
setChild(grandParent, subtreeReference);
250326

251327
// To be strictly correct we should decrease the original tree's nodeCount here, because
252328
// we may have moved away any number of children as well. We don't do that; the tree
@@ -286,7 +362,7 @@ class FragmentedTreeWriter {
286362
if (tree) {
287363
return tree;
288364
}
289-
throw new AssumptionError(`Could not find tree for node: ${JSON.stringify(node)}, tried ${tried}, ${Array.from(this.subtreeRoots).map(([k, v]) => `${k.path} => ${v.filename}`)}`);
365+
throw new AssumptionError(`Could not find tree for node: ${JSON.stringify(node)}, tried ${tried}`);
290366
}
291367
}
292368

@@ -329,17 +405,26 @@ interface Tree {
329405
* The root of this particular tree
330406
*/
331407
root: Node;
408+
332409
/**
333-
* The filename that `root` will be serialized to
410+
* The node that is pointing to this tree
411+
*
412+
* This may be "mainTreePointer", in which case this tree indicates the main tree.
334413
*/
335-
filename: string;
414+
referencingNode: SubTreeReference;
336415

337416
/**
338417
* How many nodes are in this tree already
339418
*/
340419
nodes: number;
341420
}
342421

422+
interface IncompleteForestFile {
423+
fileName: string;
424+
nodeCount: number;
425+
file: ForestFile;
426+
}
427+
343428
export function isSubtreeReference(x: TreeFile['tree']): x is Extract<TreeFile['tree'], { fileName: string }> {
344429
return !!(x as any).fileName;
345430
}

0 commit comments

Comments
 (0)