Skip to content

Commit

Permalink
chore(core): cache asset hashes
Browse files Browse the repository at this point in the history
Cache asset hashes when staging. This avoids rebundling an asset with
`AssetHashType.OUTPUT` when it is used in multiple stacks in an app.

This also removes unnecessary file system operations for other asset
hash types.

Closes aws#9424
  • Loading branch information
jogold committed Sep 22, 2020
1 parent 272363a commit a06c252
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 9 deletions.
40 changes: 32 additions & 8 deletions packages/@aws-cdk/core/lib/asset-staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ export class AssetStaging extends Construct {
*/
public static readonly BUNDLING_OUTPUT_DIR = '/asset-output';

/**
* Clears the asset hash cache
*/
public static clearAssetHashCache() {
this.assetHashCache = {};
}

private static assetHashCache: { [key: string]: string } = {};

/**
* The path to the asset (stringinfied token).
*
Expand Down Expand Up @@ -82,6 +91,8 @@ export class AssetStaging extends Construct {

private readonly bundleDir?: string;

private cacheHash?: string;

constructor(scope: Construct, id: string, props: AssetStagingProps) {
super(scope, id);

Expand All @@ -102,14 +113,27 @@ export class AssetStaging extends Construct {
const bundlingStacks: string[] = this.node.tryGetContext(cxapi.BUNDLING_STACKS) ?? ['*'];
const runBundling = bundlingStacks.includes(Stack.of(this).stackName) || bundlingStacks.includes('*');
if (runBundling) {
// Determine the source hash in advance of bundling if the asset hash type
// is SOURCE so that the bundler can opt to re-use its previous output.
const sourceHash = hashType === AssetHashType.SOURCE
? this.calculateHash(hashType, props.assetHash, props.bundling)
: undefined;

this.bundleDir = this.bundle(props.bundling, outdir, sourceHash);
this.assetHash = sourceHash ?? this.calculateHash(hashType, props.assetHash, props.bundling);
// Check if we already bundled this source path in this App (e.g. the same
// asset is used in multiple stacks). In this case we can completely skip
// file system and bundling operations.
this.cacheHash = crypto.createHash('sha256')
.update(path.resolve(this.sourcePath))
.update(JSON.stringify(props.bundling))
.digest('hex');
if (AssetStaging.assetHashCache[this.cacheHash]) {
this.assetHash = AssetStaging.assetHashCache[this.cacheHash];
} else {
// Determine the source hash in advance of bundling if the asset hash type
// is SOURCE so that the bundler can opt to re-use its previous output.
const sourceHash = hashType === AssetHashType.SOURCE
? this.calculateHash(hashType, props.assetHash, props.bundling)
: undefined;
this.bundleDir = this.bundle(props.bundling, outdir, sourceHash);
this.assetHash = sourceHash ?? this.calculateHash(hashType, props.assetHash, props.bundling);
// Cache the hash, especially useful when hash type is OUTPUT
AssetStaging.assetHashCache[this.cacheHash] = this.assetHash;
}

this.relativePath = renderAssetFilename(this.assetHash);
this.stagedPath = this.relativePath;
} else { // Bundling is skipped
Expand Down
53 changes: 52 additions & 1 deletion packages/@aws-cdk/core/test/test.staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as cxapi from '@aws-cdk/cx-api';
import * as fs from 'fs-extra';
import { Test } from 'nodeunit';
import * as sinon from 'sinon';
import { App, AssetHashType, AssetStaging, BundlingDockerImage, BundlingOptions, Stack } from '../lib';
import { App, AssetHashType, AssetStaging, BundlingDockerImage, BundlingOptions, FileSystem, Stack } from '../lib';

const STUB_INPUT_FILE = '/tmp/docker-stub.input';
const STUB_INPUT_CONCAT_FILE = '/tmp/docker-stub.input.concat';
Expand All @@ -24,6 +24,7 @@ process.env.CDK_DOCKER = `${__dirname}/docker-stub.sh`;
export = {

'tearDown'(cb: any) {
AssetStaging.clearAssetHashCache();
if (fs.existsSync(STUB_INPUT_FILE)) {
fs.unlinkSync(STUB_INPUT_FILE);
}
Expand Down Expand Up @@ -213,6 +214,56 @@ export = {
test.done();
},

'bundler uses asset cache with OUTPUT'(test: Test) {
// GIVEN
const app = new App();
const stack = new Stack(app, 'stack');
const directory = path.join(__dirname, 'fs', 'fixtures', 'test1');
const fingerPrintSpy = sinon.spy(FileSystem, 'fingerprint');

// WHEN
new AssetStaging(stack, 'Asset', {
sourcePath: directory,
assetHashType: AssetHashType.OUTPUT,
bundling: {
image: BundlingDockerImage.fromRegistry('alpine'),
command: [DockerStubCommand.SUCCESS],
},
});

new AssetStaging(stack, 'AssetDuplicate', {
sourcePath: directory,
assetHashType: AssetHashType.OUTPUT,
bundling: {
image: BundlingDockerImage.fromRegistry('alpine'),
command: [DockerStubCommand.SUCCESS],
},
});

// THEN
const assembly = app.synth();

// We're testing that docker was run exactly once even though there are two bundling assets
// and that the hash is based on the output
test.deepEqual(
readDockerStubInputConcat(),
`run --rm ${USER_ARG} -v /input:/asset-input:delegated -v /output:/asset-output:delegated -w /asset-input alpine DOCKER_STUB_SUCCESS`,
);

test.deepEqual(fs.readdirSync(assembly.directory), [
'asset.33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f',
'cdk.out',
'manifest.json',
'stack.template.json',
'tree.json',
]);

// Only one fingerprinting
test.ok(fingerPrintSpy.calledOnce);

test.done();
},

'bundler considers its options when reusing bundle output'(test: Test) {
// GIVEN
const app = new App();
Expand Down

0 comments on commit a06c252

Please sign in to comment.