Conversation
Proof of concept, not ready to merge -- opened to show the code behind the proposal. Gated behind CDKTN_STRUCT_DEDUP=1; unset, generated output is byte-identical to today. The generator names a struct after the path that reaches it, so one block shape reachable by many paths is emitted once per path, along with its mapper functions and OutputReference/List classes (~84% of generated bytes). On datadog 4.18, 95.5% of generated interfaces are structurally identical duplicates; the largest equivalence class is 364 byte-identical interfaces. This is an artifact of the schema serialization rather than the provider. `terraform providers schema -json` inlines every call site: DataDog's source reaches a single getComputeSchema() through getApmLogNetworkRumSecurityAuditQuerySchema() (48 call sites) plus four sibling query helpers, and it comes back out as 364 interfaces. detectAttributeLoops already collapses repeats, but only among a struct's own ancestors (true recursion); sibling branches are never compared and each top-level attribute restarts from an empty map. It is working as designed -- its scope simply cannot see this. This pass compares every struct in a resource against every other and repoints duplicates at one canonical struct. Equality is a full recursive signature and deliberately does NOT reuse getAttributeIdentifier: that one-level comparison is safe for the ancestor-only case but resource-wide it merges DashboardV2WidgetCohortDefinition into DashboardV2WidgetGroupDefinitionWidgetSloListDefinition, which would emit incorrect bindings. Measured on datadog 4.18, end to end: generated TS 99.6 MB -> 12.3 MB (-87.7%) interfaces 13,220 -> 1,491 tsc 0 errors, peak 4,674 MB -> 744 MB jsii 0 errors, assembly 22 MB pacmak go rc=0, 8,110 files, 56.5 MB go build ./... 925 MB peak, 11.1 s, exit 0 (GOMAXPROCS=8, cold cache) against ~18 GB for a single package today, which is what OOM-kills package-go. NOT ready to merge. The canonical-name rule here is a placeholder: it is order-independent but still not stable across provider releases (0.2-0.8% of shapes renamed on datadog 4.17 -> 4.18). A signature -> name registry is the intended fix and is not implemented. Merging shapes is also a breaking API change requiring a major bump per provider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Context from the #387 investigation, for whoever extends this PoC to group scope. Summary: the recommendation has moved from per-resource to per-group (service-scoped) dedup. Measured on the published Go trees of both providers, with references resolved within a package and signatures compared across the scope under test:
Per-group captures ~90% of provider-wide's byte benefit (aws 209.3 of 233.0 MB reclaimed; awscc 360.8 of 390.6 MB) without the two costs #389 rightly rejected provider-wide for: the naming blowup — for awscc, provider-wide made names longer, with 97% embedding a resource name — and a shared-types module that every module in a Go split would have to depend on. Under per-group scope, merged types stay inside the group that owns them. It also fixes the one thing that made service grouping look expensive. Regrouping jsii submodules per service makes each group a single Go package, and Go compiles a package as a unit — which would have taken the largest package to 57,166,831 B (~6.3 GB compile, using #389's measured RSS-per-MB). With per-group dedup applied it lands at 20,699,765 B (~2.3 GB) for aws and 21,360,127 B for awscc — below today's worst ungrouped package, Treat those yields as a ceiling, not a targetThis PoC's Per-group is not a scope parameter on this passWorth being explicit, because it changes the size of the job. This PoC merges inside
And it is cross-language by construction: the assembly has 2,416 jsii submodules ↔ 2,416 Go packages, every type's FQN embeds its submodule ( Practically this means per-group dedup and cross-language service regrouping are the same decision. Per-resource dedup remains the fallback if regrouping isn't wanted. Grouping rule — free for both providersaws —
Yields 250 groups in use (373 defined). Sanity checks that must pass: Stability is measured, not assumed: across all 354 revisions of that file from 2024-06-11 to 2026-08-18, 0 of 1,721 resources ever changed group, and growth was purely additive (+34 groups). The structural reason is that the Terraform resource name contains its service prefix, so a reassignment would require renaming the resource. awscc — no data file needed and none exists. Names are generated from CFN types, so the service is the second underscore segment ( Operationally, vendor a snapshot of Sequencing note#389 argues the name registry must land before anything ships, and that stands. But group scope changes the registry's design inputs — collision counts and rename rates are measured per-scope — so it's worth having the grouped prototype emit those numbers as it goes rather than re-deriving them afterwards. Full measurements, module anatomy, growth rates (aws 0.469 MB/day, awscc 1.60 MB/day) and why awscc forces a module split regardless of trimming are in two comments on #387. #441 is the interim non-breaking Go lever. 🤖 Generated with Claude Code |
Scope note: this PoC is per-resource by constructionGroup (service) scope was not in the original comparison, and measurement since (see #389 and the module-size work in #387) shows it beats per-resource on both size and naming for the two largest providers:
Per-group produces the shortest names of all three scopes, so the naming objection that ruled out provider-wide does not apply to it. This branch cannot express that. The merge runs inside
That is emit-layer work and cross-language by construction, since jsii submodules surface in all five targets. What stays valid here is everything the PoC was opened to show: the duplication exists and is large (95.5% on datadog 4.18), the recursive signature is the correct equality test and the one-level So this remains a useful demonstration of the mechanism and a working measurement harness, but it should not be read as the recommended scope for aws/awscc. Whoever picks up the implementation should plan for group scope in the emit layer rather than extending this pass. Also worth carrying over: the grouping rule needs a tie-break. |
Proposal: #389 — this PR is the proof-of-concept code referenced there. Read #389 first for the full investigation; this PR covers the implementation only.
What this is
The generator names a struct after the path that reaches it, so one block shape reachable by many paths is emitted once per path — along with its mapper functions and
OutputReference/Listclasses, which are 84% of generated bytes.On datadog 4.18, 95.5% of generated interfaces are structurally identical duplicates. The largest equivalence class is 364 byte-identical interfaces.
This is what OOM-kills
package-go: not the Node heap ceiling (tuned three times), but thego buildverification insidejsii-pacmak, which needs ~18 GB for a single generated package on a runner with ~24 GB usable. That memory is not governed byNODE_OPTIONSat all.The duplication is an artifact of schema serialization
terraform providers schema -jsoninlines every call site and discards sharing the provider author wrote by hand:Cross-resource duplication has the same origin:
resource_datadog_powerpack.gocallsgetNonGroupWidgetSchema()defined inresource_datadog_dashboard.go. Dedup recovers structure the provider actually expressed — it is not merging things that merely look alike.detectAttributeLoopsis not brokenIt threads
knownStructsdown each DFS branch, so it merges only when a shape reappears among its own ancestors (true recursion). Sibling branches are never compared and each top-level attribute restarts from{}. The 364-member class is...ApmQueryComputeQueryvs...LogQueryComputeQuery— siblings, invisible to an ancestor-only check by construction. It works as designed; its scope cannot see this.Equality must be a full recursive signature
This pass deliberately does not reuse
getAttributeIdentifier. That comparison uses attribute names plus one level of nesting (with an in-code caveat that it is an approximation). Safe for the ancestor-only case; applied resource-wide it yields 199 classes where an exact recursive hash yields 200 — wrongly mergingDashboardV2WidgetCohortDefinitionwithDashboardV2WidgetGroupDefinitionWidgetSloListDefinition, which share a shallow shape and diverge deeper. That would emit incorrect bindings.Measured — datadog 4.18, end to end
tscjsiijsii-pacmak --target gogo build ./...(GOMAXPROCS=8, cold)925 MB fits a standard 7 GB runner at full parallelism, with the compilation check kept.
Why this is not ready to merge
interface A extends B {}would not help since it retains the per-name classes and functions that are 84% of the bytes. Major bump per provider.Scope: per-resource, not provider-wide (tested)
Provider-wide is never clearly better and is actively bad for
awscc(machine-generated CloudFormation schemas give every resource the same generic block names).Fleet impact — datadog is an outlier
Most providers' duplication is across resources, which per-resource scope does not capture. Six providers (
acme,cfncompat,external,http,null,time) have 0% redundancy — a no-op with no API impact.Caveats
dashboardv2compile figures measured on a Windows host with 32 GB, not Linux CI.🤖 Generated with Claude Code