[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: https://github.com/flutter/flutter/issues/173700 Change-Id: I3167a96e3ead67fd1d5c763ef2a7309d82c1a7c6 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/445540 Commit-Queue: Nate Biggs <natebiggs@google.com> Reviewed-by: Nicholas Shahan <nshahan@google.com>
This commit is contained in:
@@ -248,6 +248,29 @@ class LibraryBundleCompiler implements old.Compiler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks the state of which branch the compiler is on for hot reload checks.
|
||||
///
|
||||
/// This allows hot reload generation checks to be batched into a single
|
||||
/// branch statement.
|
||||
///
|
||||
/// This batched branching allows us to avoid exponential behavior when
|
||||
/// recursing on deeply nested checked calls. Otherwise each branch makes 2
|
||||
/// copies of all the sub-branches leading to 2^n checks.
|
||||
enum HotReloadBranchState {
|
||||
/// The compiler is not along any hot reload check branch yet.
|
||||
none,
|
||||
|
||||
/// The compiler is along the branch with no extra checks. The check rewrite
|
||||
/// logic will be skipped and the normal call will be generated. The root of
|
||||
/// this branch will include a hot reload generation check.
|
||||
uncheckedBranch,
|
||||
|
||||
/// The compiler is along the branch with extra checks. The check rewrite
|
||||
/// logic will be applied for every call that needs it. The root of this
|
||||
/// branch will include a hot reload generation check.
|
||||
checkedBranch,
|
||||
}
|
||||
|
||||
class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
with OnceConstantVisitorDefaultMixin<js_ast.Expression>
|
||||
implements
|
||||
@@ -256,6 +279,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
final Options _options;
|
||||
final SymbolData _symbolData;
|
||||
|
||||
HotReloadBranchState hotReloadCheckedBranch = HotReloadBranchState.none;
|
||||
|
||||
/// Maps each `Class` node compiled in the module to the `Identifier`s used to
|
||||
/// name the class in JavaScript.
|
||||
///
|
||||
@@ -5207,7 +5232,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
// the sub-expressions will have the correct mapping applied.
|
||||
return jsExpression.toStatement()..sourceInformation = continueSourceMap;
|
||||
}
|
||||
return _visitExpression(expr).toStatement();
|
||||
return jsExpression.toStatement();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -6176,10 +6201,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
// Since there are no arguments (unlike methods) the dynamic get path can
|
||||
// be reused for the hot reload checks on a getter.
|
||||
var checkedGet = _emitCast(
|
||||
_emitDynamicGet(
|
||||
_visitExpression(receiver),
|
||||
_emitMemberName(memberName),
|
||||
),
|
||||
_emitDynamicGet(jsReceiver, _emitMemberName(memberName)),
|
||||
node.resultType,
|
||||
)..sourceInformation = _nodeStart(node);
|
||||
return _emitHotReloadSafeInvocation(instanceGet, checkedGet);
|
||||
@@ -6527,11 +6549,13 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
}
|
||||
var receiver = node.receiver;
|
||||
var jsReceiver = _visitExpression(receiver);
|
||||
var jsArguments = _emitArgumentList(node.arguments, target: target);
|
||||
if (node.isNativeListInvariantAddInvocation(_coreTypes.listClass)) {
|
||||
// TODO(nshahan): If this code is retained, can it become invalid after a
|
||||
// hot reload?
|
||||
return js.call('#.push(#)', [jsReceiver, jsArguments]);
|
||||
return js.call('#.push(#)', [
|
||||
jsReceiver,
|
||||
_emitArgumentList(node.arguments, target: target),
|
||||
]);
|
||||
}
|
||||
var name = node.name.text;
|
||||
if (name == 'call') {
|
||||
@@ -6548,12 +6572,23 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
// names of the Object instance members.
|
||||
// TODO(nshahan): What should be checked after a hot reload. I think only
|
||||
// the return type of the NSM can change.
|
||||
return _runtimeCall('#(#, #)', [name, jsReceiver, jsArguments]);
|
||||
return _runtimeCall('#(#, #)', [
|
||||
name,
|
||||
jsReceiver,
|
||||
_emitArgumentList(node.arguments, target: target),
|
||||
]);
|
||||
}
|
||||
// Otherwise generate this as a normal typed method call.
|
||||
var jsName = _emitMemberName(name, member: target);
|
||||
var invocation = js.call('#.#(#)', [jsReceiver, jsName, jsArguments]);
|
||||
if (_shouldRewriteInvocationWithHotReloadChecks(target)) {
|
||||
|
||||
js_ast.Expression generateCall() {
|
||||
var args = _emitArgumentList(node.arguments, target: target);
|
||||
return js.call('#.#(#)', [jsReceiver, jsName, args]);
|
||||
}
|
||||
|
||||
// Only consider checks if we're not in the unchecked branch.
|
||||
if (_shouldRewriteInvocationWithHotReloadChecks(target) &&
|
||||
hotReloadCheckedBranch != HotReloadBranchState.uncheckedBranch) {
|
||||
var checkedInvocation = _rewriteInvocationWithHotReloadChecks(
|
||||
jsReceiver,
|
||||
jsName,
|
||||
@@ -6562,10 +6597,25 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
node.getStaticType(_staticTypeContext),
|
||||
_nodeStart(node),
|
||||
);
|
||||
|
||||
// If we're within the checked branch (i.e. not at the root) then return
|
||||
// the checked call as-is.
|
||||
if (hotReloadCheckedBranch == HotReloadBranchState.checkedBranch) {
|
||||
return checkedInvocation;
|
||||
}
|
||||
|
||||
// We're at the root of the branch so we need to generate the unchecked
|
||||
// branch as well.
|
||||
hotReloadCheckedBranch = HotReloadBranchState.uncheckedBranch;
|
||||
final invocation = generateCall();
|
||||
hotReloadCheckedBranch = HotReloadBranchState.none;
|
||||
|
||||
// As an optimization, avoid extra checks when the invocation code was
|
||||
// compiled in the same generation that it is running.
|
||||
return _emitHotReloadSafeInvocation(invocation, checkedInvocation);
|
||||
}
|
||||
|
||||
final invocation = generateCall();
|
||||
return _isNullCheckableJsInterop(node.interfaceTarget)
|
||||
? _wrapWithJsInteropNullCheck(invocation)
|
||||
: invocation;
|
||||
@@ -7641,12 +7691,17 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
}
|
||||
}
|
||||
|
||||
var fn = _emitStaticTarget(target);
|
||||
var args = _emitArgumentList(node.arguments, target: target);
|
||||
var staticCall = js_ast.Call(fn, args)
|
||||
..sourceInformation = _nodeStart(node);
|
||||
if (_shouldRewriteInvocationWithHotReloadChecks(target)) {
|
||||
var checkedCall = _rewriteInvocationWithHotReloadChecks(
|
||||
js_ast.Call generateCall(js_ast.PropertyAccess fn) {
|
||||
var args = _emitArgumentList(node.arguments, target: target);
|
||||
return js_ast.Call(fn, args)..sourceInformation = _nodeStart(node);
|
||||
}
|
||||
|
||||
// Only consider checks if we're not in the unchecked branch.
|
||||
if (_shouldRewriteInvocationWithHotReloadChecks(target) &&
|
||||
hotReloadCheckedBranch != HotReloadBranchState.uncheckedBranch) {
|
||||
final fn = _emitStaticTarget(target);
|
||||
|
||||
var checkedInvocation = _rewriteInvocationWithHotReloadChecks(
|
||||
fn.receiver,
|
||||
fn.selector,
|
||||
target,
|
||||
@@ -7654,10 +7709,25 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
node.getStaticType(_staticTypeContext),
|
||||
_nodeStart(node),
|
||||
);
|
||||
|
||||
// If we're within the checked branch (i.e. not at the root) then return
|
||||
// the checked call as-is.
|
||||
if (hotReloadCheckedBranch == HotReloadBranchState.checkedBranch) {
|
||||
return checkedInvocation;
|
||||
}
|
||||
|
||||
// We're at the root of the branch so we need to generate the unchecked
|
||||
// branch as well.
|
||||
hotReloadCheckedBranch = HotReloadBranchState.uncheckedBranch;
|
||||
final invocation = generateCall(fn);
|
||||
hotReloadCheckedBranch = HotReloadBranchState.none;
|
||||
|
||||
// As an optimization, avoid extra checks when the invocation code was
|
||||
// compiled in the same generation that it is running.
|
||||
return _emitHotReloadSafeInvocation(staticCall, checkedCall);
|
||||
return _emitHotReloadSafeInvocation(invocation, checkedInvocation);
|
||||
}
|
||||
|
||||
final staticCall = generateCall(_emitStaticTarget(target));
|
||||
return _isNullCheckableJsInterop(target)
|
||||
? _wrapWithJsInteropNullCheck(staticCall)
|
||||
: staticCall;
|
||||
@@ -7713,6 +7783,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
DartType expectedReturnType,
|
||||
SourceLocation? originalCallSiteSourceLocation,
|
||||
) {
|
||||
final savedCheckedBranch = hotReloadCheckedBranch;
|
||||
hotReloadCheckedBranch = HotReloadBranchState.checkedBranch;
|
||||
var hoistedPositionalVariables = <js_ast.Expression>[];
|
||||
var hoistedNamedVariables = <String, js_ast.Expression>{};
|
||||
js_ast.Expression? letAssignments;
|
||||
@@ -7799,7 +7871,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
])..sourceInformation = originalCallSiteSourceLocation;
|
||||
// Cast the result of the checked call or the value returned from a
|
||||
// `NoSuchMethod` invocation.
|
||||
return js_ast.Binary(
|
||||
final result = js_ast.Binary(
|
||||
',',
|
||||
letAssignments,
|
||||
js.call('# == # ? # : #', [
|
||||
@@ -7809,6 +7881,9 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
|
||||
_emitCast(checkResult, expectedReturnType),
|
||||
]),
|
||||
);
|
||||
|
||||
hotReloadCheckedBranch = savedCheckedBranch;
|
||||
return result;
|
||||
}
|
||||
|
||||
js_ast.Expression _emitJSObjectGetPrototypeOf(
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
abstract class Either<L, R> {
|
||||
Either();
|
||||
|
||||
B fold<B>(B ifLeft(L l), B ifRight(R r));
|
||||
|
||||
static Either<L, U> map20<
|
||||
L,
|
||||
A,
|
||||
A2 extends A,
|
||||
B,
|
||||
B2 extends B,
|
||||
C,
|
||||
C2 extends C,
|
||||
D,
|
||||
D2 extends D,
|
||||
E,
|
||||
E2 extends E,
|
||||
F,
|
||||
F2 extends F,
|
||||
G,
|
||||
G2 extends G,
|
||||
H,
|
||||
H2 extends H,
|
||||
I,
|
||||
I2 extends I,
|
||||
J,
|
||||
J2 extends J,
|
||||
K,
|
||||
K2 extends K,
|
||||
LL,
|
||||
LL2 extends LL,
|
||||
M,
|
||||
M2 extends M,
|
||||
N,
|
||||
N2 extends N,
|
||||
O,
|
||||
O2 extends O,
|
||||
P,
|
||||
P2 extends P,
|
||||
Q,
|
||||
Q2 extends Q,
|
||||
R,
|
||||
R2 extends R,
|
||||
S,
|
||||
S2 extends S,
|
||||
T,
|
||||
T2 extends T,
|
||||
U
|
||||
>(
|
||||
Either<L, A2> fa,
|
||||
Either<L, B2> fb,
|
||||
Either<L, C2> fc,
|
||||
Either<L, D2> fd,
|
||||
Either<L, E2> fe,
|
||||
Either<L, F2> ff,
|
||||
Either<L, G2> fg,
|
||||
Either<L, H2> fh,
|
||||
Either<L, I2> fi,
|
||||
Either<L, J2> fj,
|
||||
Either<L, K2> fk,
|
||||
Either<L, LL> fl,
|
||||
Either<L, M> fm,
|
||||
Either<L, N> fn,
|
||||
Either<L, O> fo,
|
||||
Either<L, P> fp,
|
||||
Either<L, Q> fq,
|
||||
Either<L, R> fr,
|
||||
Either<L, S> fs,
|
||||
Either<L, T> ft,
|
||||
U fun(
|
||||
A a,
|
||||
B b,
|
||||
C c,
|
||||
D d,
|
||||
E e,
|
||||
F f,
|
||||
G g,
|
||||
H h,
|
||||
I i,
|
||||
J j,
|
||||
K k,
|
||||
LL l,
|
||||
M m,
|
||||
N n,
|
||||
O o,
|
||||
P p,
|
||||
Q q,
|
||||
R r,
|
||||
S s,
|
||||
T t,
|
||||
),
|
||||
) => fa.fold(
|
||||
left,
|
||||
(a) => fb.fold(
|
||||
left,
|
||||
(b) => fc.fold(
|
||||
left,
|
||||
(c) => fd.fold(
|
||||
left,
|
||||
(d) => fe.fold(
|
||||
left,
|
||||
(e) => ff.fold(
|
||||
left,
|
||||
(f) => fg.fold(
|
||||
left,
|
||||
(g) => fh.fold(
|
||||
left,
|
||||
(h) => fi.fold(
|
||||
left,
|
||||
(i) => fj.fold(
|
||||
left,
|
||||
(j) => fk.fold(
|
||||
left,
|
||||
(k) => fl.fold(
|
||||
left,
|
||||
(l) => fm.fold(
|
||||
left,
|
||||
(m) => fn.fold(
|
||||
left,
|
||||
(n) => fo.fold(
|
||||
left,
|
||||
(o) => fp.fold(
|
||||
left,
|
||||
(p) => fq.fold(
|
||||
left,
|
||||
(q) => fr.fold(
|
||||
left,
|
||||
(r) => fs.fold(
|
||||
left,
|
||||
(s) => ft.fold(
|
||||
left,
|
||||
(t) => right(
|
||||
fun(
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f,
|
||||
g,
|
||||
h,
|
||||
i,
|
||||
j,
|
||||
k,
|
||||
l,
|
||||
m,
|
||||
n,
|
||||
o,
|
||||
p,
|
||||
q,
|
||||
r,
|
||||
s,
|
||||
t,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Either<L, R> left<L, R>(L l) => new Left(l);
|
||||
Either<L, R> right<L, R>(R r) => new Right(r);
|
||||
|
||||
class Left<L, R> extends Either<L, R> {
|
||||
final L _l;
|
||||
|
||||
B fold<B>(B ifLeft(L l), B ifRight(R r)) => ifLeft(_l);
|
||||
|
||||
Left(this._l);
|
||||
}
|
||||
|
||||
class Right<L, R> extends Either<L, R> {
|
||||
final R _r;
|
||||
|
||||
B fold<B>(B ifLeft(L l), B ifRight(R r)) => ifRight(_r);
|
||||
|
||||
Right(this._r);
|
||||
}
|
||||
|
||||
void main() {
|
||||
Either.map20(
|
||||
left(0),
|
||||
left(1),
|
||||
left(2),
|
||||
left(3),
|
||||
left(4),
|
||||
left(5),
|
||||
left(6),
|
||||
left(7),
|
||||
left(8),
|
||||
left(9),
|
||||
left(10),
|
||||
left(11),
|
||||
left(12),
|
||||
left(13),
|
||||
left(14),
|
||||
left(15),
|
||||
left(16),
|
||||
left(17),
|
||||
left(18),
|
||||
left(19),
|
||||
(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) => 20,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user