-
Notifications
You must be signed in to change notification settings - Fork 24.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(compiler-cli): prevent stack overflow in decorator transform for large number of files #40374
Conversation
9235c03
to
f14a953
Compare
…large number of files The decorator downleveling transform patches `ts.EmitResolver.isReferencedAliasDeclaration` to prevent elision of value imports that occur only in a type-position, which would inadvertently install the patch repeatedly for each source file in the program. This could potentially result in a stack overflow when a very large number of files is present in the program. This commit fixes the issue by ensuring that the patch is only applied once. This is also a slight performance improvement, as `isReferencedAliasDeclaration` is no longer repeatedly calling into all prior installed patch functions. Fixes angular#40276
…rm for large number of files
f14a953
to
5434613
Compare
…rm for large number of files
program.emit(undefined, (fileName, outputText) => { | ||
written++; | ||
|
||
if (!outputText.includes(`import { Foo } from './foo';`)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this string match a bit brittle to rendering changes? Perhaps a regex might be a bit less fragile?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't consider it brittle, as a change in rendering would not incorrectly allow the test to pass even if the bug was reintroduced. Regarding stability I wouldn't worry about it in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No but it could cause the test to incorrectly fail. For example if the whitespace around the braces changed or the quote character used for the string. How about:
if (!outputText.includes(`import { Foo } from './foo';`)) { | |
if (!/import\s*{\s*Foo\s*}\s*from\s*['"]\.\/foo['"];/.test(outputText)) { |
Although I accept that is pretty ugly.
…rm for large number of files
Note for reviewer: I went through several iterations of this fix after CI feedback; the individual fixup commit still contain the history but it's advised to review this in full, as all intermediate commits are no longer relevant. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for fixing this @JoostK.
I found the patch code quite difficult to reason about, and I think it might have been easier if there was more consistency in the naming of the variables, symbol, functions etc. So I offered a few suggestions that you might like to consider.
@@ -17,9 +17,12 @@ interface TransformationContextWithResolver extends ts.TransformationContext { | |||
getEmitResolver: () => EmitResolver; | |||
} | |||
|
|||
const patchedReferencesAliasesSymbol = Symbol('patchedAliasedReferences'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency...
const patchedReferencesAliasesSymbol = Symbol('patchedAliasedReferences'); | |
const patchedReferencedAliasesSymbol = Symbol('patchedAliasedReferences'); |
and
const patchedReferencesAliasesSymbol = Symbol('patchedAliasedReferences'); | |
const patchedReferencesAliasesSymbol = Symbol('patchedReferencedAliases'); |
} | ||
|
||
const referencedAliases = new Set<ts.Declaration>(); | ||
|
||
const originalReferenceResolution = emitResolver.isReferencedAliasDeclaration; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency...
const originalReferenceResolution = emitResolver.isReferencedAliasDeclaration; | |
const originalIsReferencedAliasDeclaration = emitResolver.isReferencedAliasDeclaration; |
const originalReferenceResolution = emitResolver.isReferencedAliasDeclaration; | ||
// If the emit resolver does not have a function called `isReferencedAliasDeclaration`, then | ||
// we abort gracefully as most likely TS changed the internal structure of the emit resolver. | ||
if (originalReferenceResolution === undefined) { | ||
throwIncompatibleTransformationContextError(); | ||
return; | ||
} | ||
emitResolver.isReferencedAliasDeclaration = function(node, ...args) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also change this function declaration to avoid the unnecessary variable argument:
emitResolver.isReferencedAliasDeclaration = function(node, ...args) { | |
emitResolver.isReferencedAliasDeclaration = function(node, checkChildren) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept it as is on purpose, to avoid potential issues with TS updates which could introduce additional parameters without us being aware (as this is internal API, we don't have accurate type-checking of this call).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case I would propose that we change the EmitResolver
interface above to have the same signature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, done.
emitResolver[patchedReferencesAliasesSymbol] = referencedAliases; | ||
return referencedAliases; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW this could be:
emitResolver[patchedReferencesAliasesSymbol] = referencedAliases; | |
return referencedAliases; | |
return emitResolver[patchedReferencesAliasesSymbol] = referencedAliases; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know; I was trying to avoid that succinctness here but don't feel too strongly about it, so changed as suggested.
// such parameter type symbols previously could be type-only, but now might be also | ||
// used in the `ctorParameters` static property as a value. We want to make sure | ||
// that TypeScript does not elide imports for such type references. Read more | ||
// about this in the description for `patchAliasReferenceResolution`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// about this in the description for `patchAliasReferenceResolution`. | |
// about this in the description for `loadAliasReferenceResolutionPatchOrDie `. |
* See below. Note that this uses sourcegraph as the TypeScript checker file doesn't display on | ||
* Github. | ||
* https://sourcegraph.com/github.com/microsoft/TypeScript@3eaa7c65f6f076a08a5f7f1946fd0df7c7430259/-/blob/src/compiler/checker.ts#L31219-31257 | ||
*/ | ||
export function patchAliasReferenceResolutionOrDie( | ||
context: ts.TransformationContext, referencedAliases: Set<ts.Declaration>): void { | ||
export function loadAliasReferenceResolutionPatchOrDie(context: ts.TransformationContext): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency:
export function loadAliasReferenceResolutionPatchOrDie(context: ts.TransformationContext): | |
export function loadIsReferencedAliasDeclarationPatchOrDie(context: ts.TransformationContext): |
And I would probably drop the OrDie
postfix as it is just an implementation aspect.
program.emit(undefined, (fileName, outputText) => { | ||
written++; | ||
|
||
if (!outputText.includes(`import { Foo } from './foo';`)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No but it could cause the test to incorrectly fail. For example if the whitespace around the braces changed or the quote character used for the string. How about:
if (!outputText.includes(`import { Foo } from './foo';`)) { | |
if (!/import\s*{\s*Foo\s*}\s*from\s*['"]\.\/foo['"];/.test(outputText)) { |
Although I accept that is pretty ugly.
Actually I just had another thought about memory leaking... What is the life-time of the |
So I thought about this as well, and my understanding is that we're not collecting any more data than before. We're now storing everything in a single set, whereas before we'd have individual sets but they would all be retained in the captured closure of a previously installed patch (exactly what this PR is fixing). I did consider clearing the set at the start of the transformer but did not end up doing it (nor trying it) as we'd be assuming that |
…rm for large number of files
…rm for large number of files
…large number of files (#40374) The decorator downleveling transform patches `ts.EmitResolver.isReferencedAliasDeclaration` to prevent elision of value imports that occur only in a type-position, which would inadvertently install the patch repeatedly for each source file in the program. This could potentially result in a stack overflow when a very large number of files is present in the program. This commit fixes the issue by ensuring that the patch is only applied once. This is also a slight performance improvement, as `isReferencedAliasDeclaration` is no longer repeatedly calling into all prior installed patch functions. Fixes #40276 PR Close #40374
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
The decorator downleveling transform patches
ts.EmitResolver.isReferencedAliasDeclaration
to prevent elision of value imports that occur only in a type-position, which would
inadvertently install the patch repeatedly for each source file in the program.
This could potentially result in a stack overflow when a very large number of files is
present in the program.
This commit fixes the issue by ensuring that the patch is only applied once.
This is also a slight performance improvement, as
isReferencedAliasDeclaration
is no longer repeatedly calling into all prior installed patch functions.
Fixes #40276