Skip to content

Commit e6f188b

Browse files
natebiggsCommit Queue
authored andcommitted
[ddc] Avoid revisiting subexpressions during hot reload invocation rewriting.
In extreme cases with deeply nested invocations in closures, this can lead to an exponential recursive call pattern. For example: https://github.com/spebbe/dartz/blob/8bf79e746d11e6a66c868027e5e1a25fdd270f45/lib/src/either.dart#L108 Moves all nested checks onto a single branch so that there is a single branch with no checks when the generation is the same, and another branch with all the necessary checks when the generation is different. Bug: flutter/flutter#173700 Change-Id: I3167a96e3ead67fd1d5c763ef2a7309d82c1a7c6 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/445540 Commit-Queue: Nate Biggs <[email protected]> Reviewed-by: Nicholas Shahan <[email protected]>
1 parent dab1af8 commit e6f188b

File tree

2 files changed

+318
-18
lines changed

2 files changed

+318
-18
lines changed

pkg/dev_compiler/lib/src/kernel/compiler_new.dart

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,29 @@ class LibraryBundleCompiler implements old.Compiler {
248248
}
249249
}
250250

251+
/// Tracks the state of which branch the compiler is on for hot reload checks.
252+
///
253+
/// This allows hot reload generation checks to be batched into a single
254+
/// branch statement.
255+
///
256+
/// This batched branching allows us to avoid exponential behavior when
257+
/// recursing on deeply nested checked calls. Otherwise each branch makes 2
258+
/// copies of all the sub-branches leading to 2^n checks.
259+
enum HotReloadBranchState {
260+
/// The compiler is not along any hot reload check branch yet.
261+
none,
262+
263+
/// The compiler is along the branch with no extra checks. The check rewrite
264+
/// logic will be skipped and the normal call will be generated. The root of
265+
/// this branch will include a hot reload generation check.
266+
uncheckedBranch,
267+
268+
/// The compiler is along the branch with extra checks. The check rewrite
269+
/// logic will be applied for every call that needs it. The root of this
270+
/// branch will include a hot reload generation check.
271+
checkedBranch,
272+
}
273+
251274
class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
252275
with OnceConstantVisitorDefaultMixin<js_ast.Expression>
253276
implements
@@ -256,6 +279,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
256279
final Options _options;
257280
final SymbolData _symbolData;
258281

282+
HotReloadBranchState hotReloadCheckedBranch = HotReloadBranchState.none;
283+
259284
/// Maps each `Class` node compiled in the module to the `Identifier`s used to
260285
/// name the class in JavaScript.
261286
///
@@ -5207,7 +5232,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
52075232
// the sub-expressions will have the correct mapping applied.
52085233
return jsExpression.toStatement()..sourceInformation = continueSourceMap;
52095234
}
5210-
return _visitExpression(expr).toStatement();
5235+
return jsExpression.toStatement();
52115236
}
52125237

52135238
@override
@@ -6176,10 +6201,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
61766201
// Since there are no arguments (unlike methods) the dynamic get path can
61776202
// be reused for the hot reload checks on a getter.
61786203
var checkedGet = _emitCast(
6179-
_emitDynamicGet(
6180-
_visitExpression(receiver),
6181-
_emitMemberName(memberName),
6182-
),
6204+
_emitDynamicGet(jsReceiver, _emitMemberName(memberName)),
61836205
node.resultType,
61846206
)..sourceInformation = _nodeStart(node);
61856207
return _emitHotReloadSafeInvocation(instanceGet, checkedGet);
@@ -6527,11 +6549,13 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
65276549
}
65286550
var receiver = node.receiver;
65296551
var jsReceiver = _visitExpression(receiver);
6530-
var jsArguments = _emitArgumentList(node.arguments, target: target);
65316552
if (node.isNativeListInvariantAddInvocation(_coreTypes.listClass)) {
65326553
// TODO(nshahan): If this code is retained, can it become invalid after a
65336554
// hot reload?
6534-
return js.call('#.push(#)', [jsReceiver, jsArguments]);
6555+
return js.call('#.push(#)', [
6556+
jsReceiver,
6557+
_emitArgumentList(node.arguments, target: target),
6558+
]);
65356559
}
65366560
var name = node.name.text;
65376561
if (name == 'call') {
@@ -6548,12 +6572,23 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
65486572
// names of the Object instance members.
65496573
// TODO(nshahan): What should be checked after a hot reload. I think only
65506574
// the return type of the NSM can change.
6551-
return _runtimeCall('#(#, #)', [name, jsReceiver, jsArguments]);
6575+
return _runtimeCall('#(#, #)', [
6576+
name,
6577+
jsReceiver,
6578+
_emitArgumentList(node.arguments, target: target),
6579+
]);
65526580
}
65536581
// Otherwise generate this as a normal typed method call.
65546582
var jsName = _emitMemberName(name, member: target);
6555-
var invocation = js.call('#.#(#)', [jsReceiver, jsName, jsArguments]);
6556-
if (_shouldRewriteInvocationWithHotReloadChecks(target)) {
6583+
6584+
js_ast.Expression generateCall() {
6585+
var args = _emitArgumentList(node.arguments, target: target);
6586+
return js.call('#.#(#)', [jsReceiver, jsName, args]);
6587+
}
6588+
6589+
// Only consider checks if we're not in the unchecked branch.
6590+
if (_shouldRewriteInvocationWithHotReloadChecks(target) &&
6591+
hotReloadCheckedBranch != HotReloadBranchState.uncheckedBranch) {
65576592
var checkedInvocation = _rewriteInvocationWithHotReloadChecks(
65586593
jsReceiver,
65596594
jsName,
@@ -6562,10 +6597,25 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
65626597
node.getStaticType(_staticTypeContext),
65636598
_nodeStart(node),
65646599
);
6600+
6601+
// If we're within the checked branch (i.e. not at the root) then return
6602+
// the checked call as-is.
6603+
if (hotReloadCheckedBranch == HotReloadBranchState.checkedBranch) {
6604+
return checkedInvocation;
6605+
}
6606+
6607+
// We're at the root of the branch so we need to generate the unchecked
6608+
// branch as well.
6609+
hotReloadCheckedBranch = HotReloadBranchState.uncheckedBranch;
6610+
final invocation = generateCall();
6611+
hotReloadCheckedBranch = HotReloadBranchState.none;
6612+
65656613
// As an optimization, avoid extra checks when the invocation code was
65666614
// compiled in the same generation that it is running.
65676615
return _emitHotReloadSafeInvocation(invocation, checkedInvocation);
65686616
}
6617+
6618+
final invocation = generateCall();
65696619
return _isNullCheckableJsInterop(node.interfaceTarget)
65706620
? _wrapWithJsInteropNullCheck(invocation)
65716621
: invocation;
@@ -7641,23 +7691,43 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
76417691
}
76427692
}
76437693

7644-
var fn = _emitStaticTarget(target);
7645-
var args = _emitArgumentList(node.arguments, target: target);
7646-
var staticCall = js_ast.Call(fn, args)
7647-
..sourceInformation = _nodeStart(node);
7648-
if (_shouldRewriteInvocationWithHotReloadChecks(target)) {
7649-
var checkedCall = _rewriteInvocationWithHotReloadChecks(
7694+
js_ast.Call generateCall(js_ast.PropertyAccess fn) {
7695+
var args = _emitArgumentList(node.arguments, target: target);
7696+
return js_ast.Call(fn, args)..sourceInformation = _nodeStart(node);
7697+
}
7698+
7699+
// Only consider checks if we're not in the unchecked branch.
7700+
if (_shouldRewriteInvocationWithHotReloadChecks(target) &&
7701+
hotReloadCheckedBranch != HotReloadBranchState.uncheckedBranch) {
7702+
final fn = _emitStaticTarget(target);
7703+
7704+
var checkedInvocation = _rewriteInvocationWithHotReloadChecks(
76507705
fn.receiver,
76517706
fn.selector,
76527707
target,
76537708
node.arguments,
76547709
node.getStaticType(_staticTypeContext),
76557710
_nodeStart(node),
76567711
);
7712+
7713+
// If we're within the checked branch (i.e. not at the root) then return
7714+
// the checked call as-is.
7715+
if (hotReloadCheckedBranch == HotReloadBranchState.checkedBranch) {
7716+
return checkedInvocation;
7717+
}
7718+
7719+
// We're at the root of the branch so we need to generate the unchecked
7720+
// branch as well.
7721+
hotReloadCheckedBranch = HotReloadBranchState.uncheckedBranch;
7722+
final invocation = generateCall(fn);
7723+
hotReloadCheckedBranch = HotReloadBranchState.none;
7724+
76577725
// As an optimization, avoid extra checks when the invocation code was
76587726
// compiled in the same generation that it is running.
7659-
return _emitHotReloadSafeInvocation(staticCall, checkedCall);
7727+
return _emitHotReloadSafeInvocation(invocation, checkedInvocation);
76607728
}
7729+
7730+
final staticCall = generateCall(_emitStaticTarget(target));
76617731
return _isNullCheckableJsInterop(target)
76627732
? _wrapWithJsInteropNullCheck(staticCall)
76637733
: staticCall;
@@ -7713,6 +7783,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
77137783
DartType expectedReturnType,
77147784
SourceLocation? originalCallSiteSourceLocation,
77157785
) {
7786+
final savedCheckedBranch = hotReloadCheckedBranch;
7787+
hotReloadCheckedBranch = HotReloadBranchState.checkedBranch;
77167788
var hoistedPositionalVariables = <js_ast.Expression>[];
77177789
var hoistedNamedVariables = <String, js_ast.Expression>{};
77187790
js_ast.Expression? letAssignments;
@@ -7799,7 +7871,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
77997871
])..sourceInformation = originalCallSiteSourceLocation;
78007872
// Cast the result of the checked call or the value returned from a
78017873
// `NoSuchMethod` invocation.
7802-
return js_ast.Binary(
7874+
final result = js_ast.Binary(
78037875
',',
78047876
letAssignments,
78057877
js.call('# == # ? # : #', [
@@ -7809,6 +7881,9 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
78097881
_emitCast(checkResult, expectedReturnType),
78107882
]),
78117883
);
7884+
7885+
hotReloadCheckedBranch = savedCheckedBranch;
7886+
return result;
78127887
}
78137888

78147889
js_ast.Expression _emitJSObjectGetPrototypeOf(

0 commit comments

Comments
 (0)