Skip to content

refactor(toolkit-lib): batch asset-active lookups in cdk gc with a multi-pattern search - #1861

Open
Adityaj0 wants to merge 2 commits into
aws:mainfrom
Adityaj0:perf/gc-batched-asset-lookup
Open

refactor(toolkit-lib): batch asset-active lookups in cdk gc with a multi-pattern search#1861
Adityaj0 wants to merge 2 commits into
aws:mainfrom
Adityaj0:perf/gc-batched-asset-lookup

Conversation

@Adityaj0

Copy link
Copy Markdown
Contributor

fixes #1860

Reason for this change

cdk gc's ActiveAssetCache.contains() checked whether an asset is still referenced by any stack via a linear per-stack String.includes() scan, called once per asset:

public contains(asset: string): boolean {
  for (const stack of this.stacks) {
    if (stack.includes(asset)) {
      return true;
    }
  }
  return false;
}

garbageCollectEcr/garbageCollectS3 call this once per asset in every batch (batch size 1000), so for an account with S stacks and A accumulated orphaned assets, the lookup phase alone costs O(A x S x avg template size) — every single asset re-scans every stack template from scratch. In a long-lived account with hundreds of stacks and tens of thousands of orphaned S3 objects / ECR images, this dominates cdk gc runtime.

Description of changes

  • Added ActiveAssetCache.containsAny(assets: string[]), which builds a single Aho-Corasick multi-pattern search index over a batch of candidate asset identifiers and scans each remembered stack template exactly once, regardless of how many candidates are in the batch. This brings the cost down to O(stacks x avg template size + sum of candidate lengths) per batch.
  • garbageCollectEcr/garbageCollectS3 now call containsAny() once per batch (of up to 1000 assets/tags) instead of calling contains() once per asset.
  • Kept contains(asset) as a thin wrapper around containsAny([asset]) for any other/future single-item callers.
  • The multi-pattern search is exact substring matching — same semantics as before, so results are byte-for-byte identical to the old implementation. This matters a lot here: a false negative would mean deleting an asset that's actually still referenced by a live stack.

Testing

  • Added test/api/garbage-collection/stack-refresh.test.ts, including:
    • basic contains/containsAny behavior across multiple stacks
    • overlapping/substring patterns (one candidate being a substring of another)
    • a template-boundary false-positive regression test (concatenating templates without a separator could otherwise create a spurious match spanning two templates)
    • a randomized-workload test that cross-checks containsAny() against the naive per-candidate contains() for exact agreement (no false negatives, no false positives)
  • Existing garbage-collection.test.ts suite passes unchanged (40/40 tests).

Metrics

Synthetic benchmark modeling a large, long-lived account (500 stacks x ~20KB templates, 50,000 orphaned assets processed in batches of 1000, matching cdk gc's real batch size):

Before After Speedup
Asset-lookup phase ~13.3s ~5.3s ~2.5x

Results were identical between old and new implementations across all benchmarked scales (3,000 / 20,000 / 50,000 assets) — verified via direct set comparison, not just count.

Checklist

  • Unit tests added/updated
  • Integration tests added/updated (not applicable — algorithmic change, no new AWS resource types or cross-service interactions)
  • No manual edits to generated files

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

…pattern search

fixes aws#1860

ActiveAssetCache.contains() checked each candidate asset against every
remembered stack template with a linear per-stack String.includes() scan.
Since cdk gc calls this once per asset (batches of up to 1000, potentially
tens of thousands of assets in a long-lived account), the total cost was
O(assets x stacks x avg template size) -- the search work was fully
re-done from scratch for every single asset.

Add containsAny(), which builds a single Aho-Corasick multi-pattern index
over a batch of candidate asset identifiers and scans each stack template
exactly once, regardless of how many candidates are being checked. This
brings the cost down to O(stacks x avg template size + sum of candidate
lengths) per batch. garbageCollectEcr/garbageCollectS3 now call it once
per batch instead of calling contains() once per asset.

Benchmarked against a synthetic large account (500 stacks x ~20KB
templates, 50,000 orphaned assets in batches of 1000): 13.3s -> 5.3s
(~2.5x), with results cross-checked to be identical to the old
implementation (see stack-refresh.test.ts's randomized-workload test) --
no change in which assets are considered active, so no risk of a false
negative causing a live asset to be deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Adityaj0
Adityaj0 deployed to automation August 20, 2026 10:05 — with GitHub Actions Active
@Adityaj0
Adityaj0 deployed to automation August 20, 2026 10:05 — with GitHub Actions Active
@aws-cdk-automation
aws-cdk-automation requested a review from a team August 20, 2026 10:05
@github-actions github-actions Bot added the p2 label Aug 20, 2026
@Adityaj0 Adityaj0 changed the title perf(toolkit-lib): batch asset-active lookups in cdk gc with a multi-pattern search refactor(toolkit-lib): batch asset-active lookups in cdk gc with a multi-pattern search Aug 23, 2026
CI's eslint (no-bitwise) rejected the LCG's & 0x7fffffff mask. Replace
it with Math.imul for a 32-bit-safe multiply and a modulo-based
non-negative fold, avoiding both bitwise operators and the double-
precision overflow the raw multiplication would otherwise hit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
auto-merge was automatically disabled August 23, 2026 03:13

Head branch was pushed to by a user without write access

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.13%. Comparing base (2845174) to head (5a3bd8f).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1861      +/-   ##
==========================================
+ Coverage   90.93%   91.13%   +0.19%     
==========================================
  Files          80       80              
  Lines       12250    12205      -45     
  Branches     1756     1744      -12     
==========================================
- Hits        11140    11123      -17     
+ Misses       1073     1046      -27     
+ Partials       37       36       -1     
Flag Coverage Δ
suite.unit 91.13% <ø> (+0.19%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(toolkit-lib): cdk gc's active-asset lookup is O(assets x stacks x template size)

2 participants