Batch ts-morph mutations across modular emit phases#4613
Batch ts-morph mutations across modular emit phases#4613JialinHuang803 wants to merge 4 commits into
Conversation
Generalise the ts-morph batching pattern into a reusable `framework/source-file-batch.ts` module, and apply it to the modular emit phases that still walked the `add*` hotpath one statement at a time. In ts-morph 23, every individual `sourceFile.addX(...)` call re-parses the entire source file via `insertChildText` -> `doManipulation`. Loops that call `addExportDeclaration` / `addInterface` / etc. against the same file therefore grow as O(N x file_size). The bulk variants (`addExportDeclarations([...])`, `addInterfaces([...])`, ...) collapse N re-parses into 1 per kind. Ported from Azure/autorest.typescript#4015. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
All changed packages have been documented.
Show changes
|
|
You can try these changes here
|
commit: |
⚡ Benchmark Results
Full details – comparing
|
| Metric | Baseline | Current | Change |
|---|---|---|---|
| total | 🔴 614.2ms | 🔴 673.8ms | +9.7% 🔴 |
| loader | 🟡 208.6ms | 🟡 220.1ms | +5.5% 🔴 |
| resolver | 🟢 17.9ms | 🟢 17.7ms | -1.0% |
| checker | 🟢 178.9ms | 🟢 179.0ms | +0.0% |
| validation | 🟢 41.3ms | 🟢 40.9ms | -1.2% |
| ↳ validation/@azure-tools/typespec-azure-core | 🟢 6.1ms | 🟢 5.9ms | -2.6% |
| ↳ validation/@typespec/http | 🟢 5.9ms | 🟢 5.8ms | -1.0% |
| ↳ validation/@typespec/rest | 🟢 0.5ms | 🟢 0.6ms | +13.0% |
| ↳ validation/@typespec/versioning | 🔴 27.1ms | 🔴 26.5ms | -2.2% |
| ↳ validation/compiler | 🟢 1.4ms | 🟢 1.5ms | +3.2% |
| linter | 🟢 124.4ms | 🟢 169.7ms | +36.5% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/auth-required | 🟢 0.0ms | 🟢 0.0ms | -3.2% |
| ↳ linter/@azure-tools/typespec-azure-core/bad-record-type | 🟢 0.2ms | 🟢 0.2ms | -10.3% |
| ↳ linter/@azure-tools/typespec-azure-core/byos | 🟢 5.3ms | 🟢 7.2ms | +37.0% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/casing-style | 🟢 0.6ms | 🟢 0.6ms | +4.4% |
| ↳ linter/@azure-tools/typespec-azure-core/composition-over-inheritance | 🟢 0.1ms | 🟢 0.1ms | -13.5% |
| ↳ linter/@azure-tools/typespec-azure-core/documentation-required | 🟢 0.8ms | 🟢 0.9ms | +18.3% |
| ↳ linter/@azure-tools/typespec-azure-core/friendly-name | 🟢 0.6ms | 🟢 0.6ms | +0.0% |
| ↳ linter/@azure-tools/typespec-azure-core/key-visibility-required | 🟢 0.2ms | 🟢 0.2ms | -0.5% |
| ↳ linter/@azure-tools/typespec-azure-core/known-encoding | 🟢 0.3ms | 🟢 0.3ms | +3.9% |
| ↳ linter/@azure-tools/typespec-azure-core/long-running-polling-operation-required | 🟢 0.3ms | 🟢 0.3ms | +5.6% |
| ↳ linter/@azure-tools/typespec-azure-core/no-case-mismatch | 🟢 0.2ms | 🟢 0.4ms | +60.8% |
| ↳ linter/@azure-tools/typespec-azure-core/no-closed-literal-union | 🟢 0.2ms | 🟢 0.2ms | +3.5% |
| ↳ linter/@azure-tools/typespec-azure-core/no-enum | 🟢 0.0ms | 🟢 0.0ms | -1.9% |
| ↳ linter/@azure-tools/typespec-azure-core/no-error-status-codes | 🟢 0.1ms | 🟢 0.1ms | +1.1% |
| ↳ linter/@azure-tools/typespec-azure-core/no-explicit-routes-resource-ops | 🟢 0.1ms | 🟢 0.1ms | +5.4% |
| ↳ linter/@azure-tools/typespec-azure-core/no-format | 🟢 0.6ms | 🟢 0.7ms | +26.2% |
| ↳ linter/@azure-tools/typespec-azure-core/no-generic-numeric | 🟢 0.4ms | 🟢 0.4ms | +2.3% |
| ↳ linter/@azure-tools/typespec-azure-core/no-header-explode | 🟡 17.3ms | 🔴 24.5ms | +41.8% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/no-legacy-usage | 🟢 1.1ms | 🟢 1.1ms | +2.6% |
| ↳ linter/@azure-tools/typespec-azure-core/no-multiple-discriminator | 🟢 0.1ms | 🟢 0.1ms | -1.0% |
| ↳ linter/@azure-tools/typespec-azure-core/no-nullable | 🟢 0.2ms | 🟢 0.2ms | +7.5% |
| ↳ linter/@azure-tools/typespec-azure-core/no-offsetdatetime | 🟢 1.1ms | 🟢 1.3ms | +14.9% |
| ↳ linter/@azure-tools/typespec-azure-core/no-openapi | 🟢 1.9ms | 🟢 2.6ms | +34.3% |
| ↳ linter/@azure-tools/typespec-azure-core/no-private-usage | 🟢 1.8ms | 🟢 2.0ms | +11.3% |
| ↳ linter/@azure-tools/typespec-azure-core/no-query-explode | 🟡 17.8ms | 🔴 24.6ms | +38.1% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/no-response-body | 🔴 22.0ms | 🔴 33.8ms | +53.7% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/no-rest-library-interfaces | 🟢 0.0ms | 🟢 0.0ms | +13.9% |
| ↳ linter/@azure-tools/typespec-azure-core/no-route-parameter-name-mismatch | 🟢 4.8ms | 🟢 6.7ms | +39.8% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/no-rpc-path-params | 🟢 0.2ms | 🟢 0.2ms | +11.6% |
| ↳ linter/@azure-tools/typespec-azure-core/no-string-discriminator | 🟢 0.0ms | 🟢 0.0ms | +6.3% |
| ↳ linter/@azure-tools/typespec-azure-core/no-unknown | 🟢 0.2ms | 🟢 0.2ms | +8.6% |
| ↳ linter/@azure-tools/typespec-azure-core/no-unnamed-union | 🟢 0.3ms | 🟢 0.3ms | +5.6% |
| ↳ linter/@azure-tools/typespec-azure-core/operation-missing-api-version | 🟢 0.2ms | 🟢 0.2ms | +10.1% |
| ↳ linter/@azure-tools/typespec-azure-core/request-body-problem | 🟢 0.3ms | 🟢 0.3ms | +3.7% |
| ↳ linter/@azure-tools/typespec-azure-core/require-versioned | 🟢 0.0ms | 🟢 0.0ms | +2.9% |
| ↳ linter/@azure-tools/typespec-azure-core/response-schema-problem | 🔴 20.8ms | 🔴 27.9ms | +34.0% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/rpc-operation-request-body | 🟢 0.3ms | 🟢 0.3ms | -0.6% |
| ↳ linter/@azure-tools/typespec-azure-core/spread-discriminated-model | 🟢 0.3ms | 🟢 0.3ms | +0.9% |
| ↳ linter/@azure-tools/typespec-azure-core/use-standard-names | 🟢 5.0ms | 🟢 7.7ms | +55.3% 🔴 |
| ↳ linter/@azure-tools/typespec-azure-core/use-standard-operations | 🟢 0.1ms | 🟢 0.1ms | +15.8% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-common-types-version | 🟢 3.6ms | 🟢 3.7ms | +1.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-custom-resource-no-key | 🟢 0.1ms | 🟢 0.1ms | -2.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-custom-resource-usage-discourage | 🟢 0.1ms | 🟢 0.1ms | +1.4% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-delete-operation-response-codes | 🟢 4.4ms | 🟢 4.6ms | +4.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-no-path-casing-conflicts | 🟢 4.0ms | 🟢 3.8ms | -3.4% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-no-record | 🟢 0.3ms | 🟢 0.3ms | +1.1% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-post-operation-response-codes | 🟢 0.4ms | 🟢 0.4ms | +0.2% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-put-operation-response-codes | 🟢 0.0ms | 🟢 0.0ms | +15.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-action-no-segment | 🟢 0.2ms | 🟢 0.2ms | +6.8% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-duplicate-property | 🟢 0.1ms | 🟢 0.1ms | +3.8% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-interface-requires-decorator | 🟢 0.0ms | 🟢 0.0ms | +7.2% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-action-verb | 🟢 0.1ms | 🟢 0.1ms | +13.1% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-envelope-property | 🟢 0.1ms | 🟢 0.1ms | -6.9% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-version-format | 🟢 0.0ms | 🟢 0.0ms | -1.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-key-invalid-chars | 🟢 0.2ms | 🟢 0.2ms | -0.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-name-pattern | 🟢 0.0ms | 🟢 0.0ms | +7.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-operation | 🟢 0.2ms | 🟢 0.2ms | +0.8% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-operation-response | 🟢 4.2ms | 🟢 4.3ms | +2.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-patch | 🟢 0.3ms | 🟢 0.3ms | -1.3% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-path-segment-invalid-chars | 🟢 0.2ms | 🟢 0.2ms | +1.1% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-provisioning-state | 🟢 0.1ms | 🟢 0.1ms | -1.7% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/beyond-nesting-levels | 🟢 0.1ms | 🟢 0.1ms | -10.1% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/empty-updateable-properties | 🟢 0.1ms | 🟢 0.1ms | -9.7% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/improper-subscription-list-operation | 🟢 0.0ms | 🟢 0.0ms | +4.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/lro-location-header | 🟡 12.4ms | 🟡 11.7ms | -6.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/missing-operations-endpoint | 🟢 0.0ms | 🟢 0.0ms | +2.7% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/missing-x-ms-identifiers | 🟢 0.3ms | 🟢 0.3ms | -4.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-empty-model | 🟢 0.1ms | 🟢 0.1ms | -2.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-override-props | 🟢 0.1ms | 🟢 0.1ms | +2.9% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-resource-delete-operation | 🟢 0.2ms | 🟢 0.2ms | -3.9% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-response-body | 🟡 19.2ms | 🟡 18.8ms | -2.2% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/patch-envelope | 🟢 0.1ms | 🟢 0.1ms | -0.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/resource-name | 🟢 0.1ms | 🟢 0.1ms | -7.1% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/secret-prop | 🟢 2.1ms | 🟢 2.2ms | +3.3% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/unsupported-type | 🟢 0.4ms | 🟢 0.3ms | -3.9% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/version-progression | 🟢 0.0ms | 🟢 0.0ms | -3.8% |
| ↳ linter/@azure-tools/typespec-client-generator-core/property-name-conflict | 🟢 1.0ms | 🟢 1.0ms | +5.4% |
| ↳ linter/@azure-tools/typespec-client-generator-core/require-client-suffix | 🟢 0.2ms | 🟢 0.2ms | +1.7% |
| emit | 🔴 5.53s | 🔴 5.52s | -0.2% |
| ↳ emit/@azure-tools/typespec-autorest | 🟢 151.8ms | 🟢 160.5ms | +5.8% 🔴 |
| ↳ emit/@azure-tools/typespec-python | 🔴 4.03s | 🔴 4.05s | +0.7% |
| ↳ emit/@typespec/http-client-js | 🔴 1.19s | 🔴 1.09s | -8.2% 🟢 |
| ↳ emit/@typespec/openapi3 | 🟢 137.3ms | 🟢 149.1ms | +8.6% 🔴 |
| ↳ emit/@typespec/openapi3/compute | 🟢 120.8ms | 🟢 130.4ms | +8.0% 🔴 |
| ↳ emit/@typespec/openapi3/write | 🟢 17.0ms | 🟢 18.4ms | +8.5% 🔴 |
Averaged across 3 specs (azure-arm-resource-manager, azure-core-dataplane, azure-full).
Threshold: changes > ±5% are highlighted.
🟢 Fast · 🟡 Moderate (stages >200ms, rules >10ms) · 🔴 Slow (stages >400ms, rules >20ms)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…urcefile-batch # Conflicts: # packages/typespec-ts/src/modular/build-root-index.ts # packages/typespec-ts/src/modular/build-subpath-index.ts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| throw new Error(`Unsupported pending statement kind ${(s as any).kind}`); | ||
| } | ||
| } | ||
| if (imports.length > 0) { |
There was a problem hiding this comment.
All modifications in the test files are just changes in the order, as statements of the same type are generated in batches now in the sequence of interface, functions, enums...
Summary
Generalise the ts-morph batching pattern shipped in autorest.typescript #4004 (which targeted
resolveReferences) into a reusableframework/source-file-batch.tsmodule, and apply it to the modular emit phases that still walked theadd*hotpath one statement at a time.In ts-morph 23, every individual
sourceFile.addX(...)call re-parses the entire source file viainsertChildText->doManipulation. Loops that calladdExportDeclaration/addInterface/ etc. against the same file therefore grow as O(N x file_size). The bulk variants (addExportDeclarations([...]),addInterfaces([...]), ...) collapse N re-parses into 1 per kind.Ported from Azure/autorest.typescript#4015.
Design
A single module-level reference counter (
batchDepth) tracks how manybeginSourceFileBatchscopes are open, with one sharedMap<SourceFile, StatementStructures[]>collecting pending writes. EachenqueueStatementappends to the queue whilebatchDepth > 0and writes through to ts-morph immediately otherwise -- so call sites likeaddDeclarationcan use it unconditionally.Only the outermost
flushSourceFileBatchdrains the queue; inner flushes just decrement the counter. That keeps nested begin/flush pairs composable: an inner caller can open its own batch without prematurely flushing what an outer caller queued. On drain, structures are grouped byStructureKindand dispatched through the per-kind bulk APIs (addExportDeclarationsetc.), collapsing N re-parses into one per kind per file.Why the binder is not migrated. The binder already collects imports per file in
this.importsand issues a singlefile.addImportDeclarations(arr)per file at the end ofresolveReferences. That is the same end-state the new framework produces (one re-parse per file per kind), so routing it throughenqueueStatementwould add per-call overhead without changing the flush cost, and would push binder-specific ordering policy (sort by module specifier) into a generic module. The framework targets the other pattern: many independent callers each writing one statement to a shared file, or a single caller that interleaves writes with AST reads.What changed
New
packages/typespec-ts/src/framework/source-file-batch.tsbeginSourceFileBatch/flushSourceFileBatch-- reference-counted scope; nested begin/flush pairs compose. The flush is wrapped intry/finallyso a thrown writer never leaks queue state into the next emit.enqueueStatement(file, structure)-- queues while a batch is open, writes immediately otherwise (single entry point for both modes).getEffectiveExportedNames/getQueuedExportNames-- batch-aware views of the export set, so dedup against still-pending writes stays correct.StructureKindand dispatched through the per-kind bulk APIs (works around a ts-morph 23RangeParentHandlerbug in mixed-kindaddStatements).framework/declaration.ts-- drops its own ad-hoc batch and routesaddDeclarationthroughenqueueStatement. Single code path for batched/unbatched.modular/emit-models.ts-- wrapsemitTypesmain loop withbeginSourceFileBatch/flushSourceFileBatch(try/finally).modular/build-subpath-index.ts-- wrapsbuildSubpathIndexFile;partitionAndEmitExportsenqueues both the type-only and value exports.modular/build-root-index.ts-- wrapsbuildRootIndexandbuildSubClientIndexFile; all fouraddExportDeclarationcall sites useenqueueStatement.getExistingExportsswapped togetEffectiveExportedNames(one-line body change);getExportedDeclarations().keys()dedup sets union withgetQueuedExportNames.All queues in the index-builder call sites are single-kind (
ExportDeclaration), so emitted output is byte-identical. TheemitTypesqueue mixes kinds and groups by kind on flush -- same behaviour shipped in the prior emitTypes batching work.Performance results
Measured (on the autorest.typescript repo) by
tsp compileof the Network and Compute ARM specs, against the post-#4004 baseline.Network (
client.tsp, total onEmit7:27.367->2:20.474, -69%)Compute (
client.tsp, total onEmit1:47.209->0:56.485, -47%)