-
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
[web]: discrepancies between JS and Wasm backends with -O3 or higher. #56949
Comments
I think the only way to fix some of these discrepancies is by implementing Dart properly in both targets. For example: void main () {
final List<int> ints = [1, 2, 3];
final int i = ints[10];
f(i);
}
f(i) {
print(i.runtimeType);
print(i);
} Here the out-of-bounds access will cause a Wasm trap, which cannot be caught from within the same Wasm execution. So the execution of this code will just halt at that point. In JS you get an So the effect of omitting the same check in both platforms are different. Making dart2wasm work like dart2js here requires adding the bounds check back. But there isn't an Making the dart2js work like dart2wasm is probably not possible, and certainly not desirable. So the only way to make both work the same way is to add the bounds check back, and once we do that we can just implement the proper Dart semantics. (throw an exception) |
I don't think we should make any guarantees about what unsafe optimizations do on different platforms, and especially not that they do the same thing. You should be developing and testing without unsafe optimizations, so that any bug actually throws. Defaulting to |
This wouldn't help finding the bug in flutter/devtools#8452. The original code was the following: static FilterTag? parse(String value) {
final parts = value.split(filterTagSeparator);
try {
final useRegExp = parts.last == useRegExpTag;
final query = parts[0].trim();
final settingFilterValues =
(jsonDecode(parts[1]) as List).cast<Map<String, Object?>>();
return FilterTag(
query: query,
settingFilterValues: settingFilterValues,
useRegExp: useRegExp,
);
} catch (_) {
// Return null for any parsing error.
return null;
}
} If you run this without unsafe optimizations the In unsafe mode, the The way to test this is with unsafe optimizations trapping when things go wrong, which is exactly what the release mode does. |
The code in question should at least have used Doing I guess the conclusion would be to enable more lints, and also test with |
Completely agree here. I'd very much would like the default to not be I like the idea of enabling lints to detect scenarios that hide assumptions made by /cc @yjbanov @eyebrowsoffire - any reservations with these ideas for switching the default optimization flags for flutter web over time? /cc @bkonyi - the flutter tool today exposes a way to switch dart2js builds to use different optimization levels, I was wondering if we could generalize this to apply to wasm too? |
Regarding testing unsafe code, another idea could be adding a mode to both dart2js and dart2wasm (and any other target that omits runtime checks) to convert the checks we want to avoid in production mode into some kind of crash/failure that is not possible to catch and handle in Dart. So basically the debug mode, but runtime checks omitted in release mode stop execution when failed, in a way that cannot be accidentally handled. Stopping the execution can be done with a trap in Wasm, but I don't know if it's possible to do in JS. We could even make this the only debug mode, which effectively changes runtime error semantics of the language by making them impossible to catch and handle. (because both debug and release modes of the compiler makes them impossible to catch) |
From #56655 (comment):
So you really shouldn't depend on catching and handling |
I am wary of special debugging modes. Apps need the ability to report back to the server crashes of every kind. How actionable are wasm traps? Can the complete stack be deobfuscated at the server? |
I was thinking that the debug mode should do it, I'm not proposing another debug mode.
So In that case I think what we want is:
Any thoughts on these @lrhn @rakudrama?
Wasm traps can be caught by the JS code calling a Wasm function (that could be the Wasm function for Dart You just can't catch Wasm exception from within the same Wasm execution. The only way to catch a Wasm trap from Wasm code is by catching it in JS, and then passing/returning the caught trap to Wasm. So the call stack would look like: [Wasm (handles trap), JS (catches trap, returns to caller), Wasm (traps)]. Since in the browser you can't start running Wasm without JS, in practice we can catch all Wasm crashes and log/print. |
I checked some of the devtools packages for these two lints.
Fixing these may require significant amount of refactoring, as unlike the bug referenced above, these code don't seem to catch an error directly thrown by one of the lines that can be fixed by checking for errors before the call. |
Is this not already exposed via the |
The unsafe The undefined behavior may manifest in I don't think it makes sense to even attempt to align dart2js and dart2wasm on which of those kinds of undefined behavior is triggered, i.e. align them on specific scenarios on a) or b) or c). It's understandable that a user may want to be able to catch all exceptions. Though if an exception was triggered by undefined behavior, the app is out-of-control, it may have done many wrong things before it threw the exception. It may have triggered things in the event loop later that throw, may have incorrectly modified program state, ... So even if we didn't trap in dart2wasm but threw an exception, there's just no guarantees for a programmer that the app is running correctly afterwards either => It's a false sense of "safety" to think that any undefined behavior inside the body of a try Overall my hope is that we can make things fast enough with dart2wasm that we don't need unsafe modes anymore or only use it in very limited, scoped circumstances. If we can make the cost within 10% of perf/size, maybe that's acceptable. IMHO flutter should've definitely made the default to be sound mode and let users explicitly opt out of this. But since dart2js was using |
And just to be clear, here that means catching all errors. You should always catch all Anything can happen after an With unsafe optimizations enabled, that becomes "Anything can happen after an |
Since fixing these issues in user code is not easily possible, and we still want to omit the checks we are omitting with With this I can test that I'm not relying on errors that will disappear when I deploy my app. This by itself won't fix the discrepancies, because dart2wasm and dart2js can omit different types of errors, or in different standard library functions. One way to fix the discrepancies is to do this for all errors. With this users can test that they are not relying on errors, and the backends can omit the ones that makes sense to omit, for performance. I think it we just turn out-of-bounds and type errors to uncatchable crashes that should solve the big part of the issue. |
Converting errors to being uncatchable seems like something that should have an opt-in flag, and not something that should be default behavior. Because it's not spec-compliant behavior. Then it makes good sense as "yet another non-standard mode". It would worry me if a compiler has no standard compliant mode. That makes it incredibly hard to test that it actually does what it should. It'll be basically impossible to run language tests. There should be an implementation of the specified behavior that options can then choose to diverge from. |
Agree about being opt-in only. Unfortunately, I don't believe we can replicate it in JS. We could bypass on-clauses and make errors more visible, but it won't match wasm traps behavior. To bring this back to the original issue. Our goal is to allow for a seamless transition between JS and Wasm backends. @yjbanov @eyebrowsoffire @kevmoo @srujzs @natebiggs and I brainstormed about this too. Here is a summary of our discussions. We identified 3 sources of inconsistencies between JS and Wasm today that make this transition difficult:
We need to keep apps where they will behave consistently. This requires different approaches for each of the 3 areas above. Ignoring (a) here. We've talked about 3 approaches for (b) and (c):
I propose we prioritize (iii) first, then (ii), then (i). With (iii) we will finally get apps closer to the spec and hopefully eliminate (c). I expect (ii) will help assure developers that they will not hit (b) because it can be analyzed statically. There may still be value in some solutions like (i), but the usefulness is limited since it is highly dependent on test coverage. |
SGTM, but one concern here may be that the scoped pragmas won't affect indirectly called code. If I index a
My point is I should be able to test and debug my app in the same compilation mode/semantics as the release builds. You can do this today, but you have to figure out the right set of flags yourself and somehow pass them to the compiler (e.g. via the Then we can discuss whether the release mode is spec compliant, and maybe fix it. Right now control flow of my program can be different in release and debug builds, I'm merely suggesting that we should fix this. |
Agree. I hope it is rare enough, that we can live with the performance cost. If it becomes critical, I'd lean towards other fine-grain alternatives (like adding a way to override the pragma from a configuration file passed to the compiler). It's not ergonomic, but probably OK for the rare cases.
I partially agree :) I agree that you should get consistent semantics between debug and release. The One of the reasons I think it should be coupled with the spec is that it's likely that the debug and release modes are delivered by different implementations. That's already true for JS, It's likely DDC will be used during development when deploying with dart2wasm, too (and yes, there are other gaps, like in64, that need to be addressed to make that work well). That said, we had many scenarios where developers needed to debug code in I believe that if we address (iii), the flutter-cli will then finally guarantee the consistency we seek. The risk of unspecified behavior (and thus inconsistencies from it) goes down dramatically when only code that is intentionally designed to elide checks is opted-in via pragmas. |
Only if that is the spec compliant semantics 😁 The purpose of running tests is to find bugs before they reach production. The production compiler's unsafe optimizations may hide bugs, so running tests with those optimizations of counterproductive. I think the risk of code hiding a real bug in a way that becomes visible only in production mode, because it fails differently, of a much smaller risk, and a more manageable one, than allowing invalid behavior everywhere. Bugs should fail early. If they don't fail until reaching production, that's annoying, but a bug not falling at all is a much worse problem. |
Unsafe optimizations levels like -O3 and -O4 in dart2js and dart2wasm don't promise any guarantees around semantic behavior if assumptions are wrong. However, they currently take different routes and can cause friction for developers that support both JS and WASM outputs or that are migrating from one to the other.
Take this minimal example:
Here:
int value
fails in-O2
and lower for both compilers, and gets caught by the try-catch block (2).-O4
, dart2js will printnull
on (1) instead.-O4
, dart2wasm will hit a wasm trap and exit with a runtime error that is not caught in (2).Unfortuantely, flutter tools default to using -O4, and it is not obvious to developers that the discrepancy in semantics is coming from unsafe optimizations.
The text was updated successfully, but these errors were encountered: