-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Slow interop type tests: foo.isA<Foo>
or foo.isInstanceOfString("Foo")
#60344
Comments
Similar issue: #56130 Caching the constructor isn't always safe as the the class could possibly change since the last Some options:
bool _specialInstanceOfForNoDots(String s) { // instanceof Foo
return JS("(o, gc, s) => o instanceof gc[s]", o.toExternRef, globalContext.toExternRef, s.toJS.toExternRef);
} bool _specialInstanceOfForOneDot(String s1, String s2) { // instanceof Foo.Bar
return JS("(o, gc, s1, s2) => o instanceof gc[s1][s2]", o.toExternRef, globalContext.toExternRef, s1.toJS.toExternRef, s2.toJS.toExternRef);
} Using string interpolation within the |
Just like in other situations (see e.g. #55183) it seems to me that we are paying a high price for an uncommon thing. It's probably very rare that a (We may also put a warning on the If we don't do this, then we should tell users that suffer performance issues to resolve constructors manually and use the unsafe low-level |
From a simple not very scientific benchmark import 'dart:js_interop';
import 'dart:js_interop_unsafe';
@JS('globalThis')
external JSObject get window;
@JS("Array") // so it works with D8
extension type Window(JSAny a) {}
JSFunction? getConstructor(String constructorName) {
if (constructorName.isEmpty) return null;
final parts = constructorName.split('.');
JSObject? constructor = globalContext;
for (final part in parts) {
constructor = constructor?[part] as JSObject?;
if (constructor == null) return null;
}
return constructor as JSFunction;
}
final windowConstructor = getConstructor('Array')!;
main() {
for (int i = 0; i < 5; ++i) {
measure<bool>(
i == 4,
'window.instanceOfString("Window")',
() => window.instanceOfString('Window'),
);
measure<bool>(i == 4, 'window.isA<Window>()', () => window.isA<Window>());
measure<bool>(
i == 4,
'window.instanceOf',
() => window.instanceof(windowConstructor),
);
}
}
T measure<T>(bool doPrint, String name, T Function() fun) {
final sw = Stopwatch()..start();
late T value;
final count = 1000000;
for (int i = 0; i < count; ++i) {
value = fun();
}
if (doPrint) {
print('${sw.elapsed.inMicroseconds / count} us/call for $name');
}
return value;
} seems to indicate
=> It's much faster to use cached constructor (if I measured it right?) |
I don't necessarily mind doing the potentially unsafe thing here as long as we document it. It just means there's a slim chance of something breaking when we make that change. Alternatively, I did a quick mockup of the second option in #60344 (comment), and the benchmarks are a lot closer (I changed everything to use
The benefit is naturally avoiding that slim chance of breakage but also avoiding adding any logic into the compilers to cache constructors correctly. I assume the slightly higher cost compared to the cached constructor is the extra property access on |
Sounds promising that it would become 5x faster! We have it as a goal to allow performant Dart web frameworks based on DOM manipulation - any such framework would probably have such type checks in the hot path. So they want it to be as fast as possible. The question is whether we ask those users to rewrite their code to avoid using
Our compiler sees the
To be honest I find that this "unsafe code" if caching but "safe code" if not caching a fallacy: If we assume that the type check result can change at any time during execution of the program then one cannot really write "safe programs". Imagine: JSObject node;
if (node.isInstanceOfString("HtmlElement")) {
foo();
use((node as Foo).someHtmlElementProperty);
...;
} The call to It's true that My point is that the code that uses In general I view the new JS interop as being static: We declare JS type and their properties/methods statically using extension types -- we assume therefore that at runtime properties/methods aren't added/removed to those types. Similarly I'd view the |
The * `<node>.isA<HTMLInputElement>()` and * `<node>.instanceOfString('HTMLInputElement')` APIs are currently performing poorly, see [0]. Instead we use `<node>.instanceof(<constructor>)` with a cached value of the looked up constructor. This results in 5-10x faster type checks in dart2wasm. [0] dart-lang/sdk#60344
Just to point out that DOM types, like IMHO, this kind of optimization with DOM types is "safe enough." |
Currently it seems the following code
get compiled to something like this
which is implemented in sdk/lib/js_interop/js_interop.dart
This is very inefficient as we do the
instanceOfString
logic (splitting string by.
, looking up in the global context) for every type test.A simple optimization would be to just cache the
constructor
in a global variable, so any following type checks will simply call the JS<obj> instanceof <constructor>
. Maybe there's other ways to optimize it more, but at least this we should do.One may also consider compiling this to per-type JS helper functions, e.g.
object.isA<Window>
to(o) => o instanceof Window
instead of a generic(o, c) => o instanceof c
-- if that's beneficial for performanceIn some DOM related code (e.g. jaspr) this shows up on the profile and I think we should optimize this.
/cc @srujzs @osa1 Could one of you take a look at this?
One could
The text was updated successfully, but these errors were encountered: