Skip to content
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

Make ASYNCIFY_ADD propagate (or add a setting to do so) #13150

Closed
curiousdannii opened this issue Dec 30, 2020 · 32 comments · Fixed by #21672
Closed

Make ASYNCIFY_ADD propagate (or add a setting to do so) #13150

curiousdannii opened this issue Dec 30, 2020 · 32 comments · Fixed by #21672

Comments

@curiousdannii
Copy link
Contributor

curiousdannii commented Dec 30, 2020

An app I'm porting has 3000 instrumented functions when ASYNCIFY_IGNORE_INDIRECT is disabled, and 100 instrumented functions when it is enabled. Somewhere in between is the actual number of functions that need to be instrumented.

After investigating the code I found one function with indirect calls that does need to be instrumented (there will probably be more), so I specified it in ASYNCIFY_ADD. But then I realised that its status is not propagated upwards, I would need to specify all the functions that call it, etc. I tried adding it to ASYNCIFY_IMPORTS instead, but it was ignored because it's not an import function.

So firstly, have I understood this correctly, that ASYNCIFY_ADD doesn't propagate? (I think ASYNCIFY_REMOVE actually does.) Second, what is the reason for this? Can ASYNCIFY_ADD be changed to propagate? Or could a new setting which propagates be added? I think that would be much more useful and productive than specifying each function in the call tree.

@kripken
Copy link
Member

kripken commented Jan 6, 2021

Neither add nor remove propagate. The current model is that we propagate using all the info we know, and then apply the add and remove changes afterwards.

I agree an option for a propagating add could make sense. It wouldn't always, though, so it can't replace the old one (for example, you may want to add X but know it will only be called from/by only some of the functions that can call it/be called by it). Perhaps there could be a "hint" list, which is like "add" but is done before propagation.

Is it hard to add all the functions that you need, though? maybe we should make that easier. The asyncify-verbose option emits stuff that might help create custom lists, and maybe we could improve it. But it might not be good enough.

@sbc100
Copy link
Collaborator

sbc100 commented Jan 6, 2021

I'm not sure I understand your argument against always propagating. If you know that a function is never called by one of its callers then aren't you saying that a code path is known to be dead. I that case wouldn't it be best to remove the codepath completely (using #ifdef or some other method). Culling such dead codepaths is particularly beneficial in emscripten. Leaving such a suspected-dead codepath in the codebase would also be dangerous since if it did turns out not to be dead after all then you end be calling a async function from a non-async function which would break right?

In other words isn't it reasonable to say "If you don't want a function marked as async, make sure it doesn't transitively call any other async functions".

@curiousdannii
Copy link
Contributor Author

Is it hard to add all the functions that you need, though? maybe we should make that easier. The asyncify-verbose option emits stuff that might help create custom lists, and maybe we could improve it. But it might not be good enough.

I don't know how many I would need to add (I haven't even yet identified all of the indirect call functions that do need to be async), but it could easily be dozens or more. Listing them all wouldn't be fun, especially not if optimising/inlining changes the list. And even asyncify-verbose isn't all that useful as it only gives one reason for each function, so we can't just rely on grepping the output. In my mind this is very much a task for the computer, not humans. ;)

@rdb
Copy link
Contributor

rdb commented Feb 1, 2021

I'm trying to asyncify a relatively small portion of a large codebase where manually adding all the caller functions would be extremely tedious and error-prone (I have 35k instrumented functions with ASYNCIFY_IGNORE_INDIRECT), and break upon any minor code refactoring anywhere in the call chain.

Perhaps ASYNCIFY_IMPORTS could be extended to work for arbitrary C functions? This would be extremely helpful to us.

@rdb
Copy link
Contributor

rdb commented Feb 1, 2021

Upon further reflection, it would actually be even more convenient if we had an EMSCRIPTEN_ASYNCIFY attribute to attach to a function definition in C to instrument it (and have that propagate up the call stack). I would attach it to a function that I know makes an indirect call to something that might need to unwind the stack. It would be a lot less error-prone than manually maintaining such a list of functions.

@kripken
Copy link
Member

kripken commented Feb 1, 2021

@rdb That would be better in general, yes. It's been difficult to do technically. However we may want that type of functionality for module splitting - @tlively this might be another use case for attaching generic attribute to a function, and get them to binaryen.

@curiousdannii
Copy link
Contributor Author

And attributes won't help for all usecases. I bring in the app I'm porting as an unmodified submodule, so controlling compilation and linking external is essential.

@kripken
Copy link
Member

kripken commented Feb 2, 2021

@curiousdannii Yes, we definitely will keep the external API for this regardless of adding a new method with attributes.

@curiousdannii
Copy link
Contributor Author

curiousdannii commented Feb 3, 2021

It wouldn't be as simple as moving this code:

https://github.com/WebAssembly/binaryen/blob/3f1287c4c30ff273972c7580d99ae1ca729ea108/src/passes/Asyncify.cpp#L703-L715

Before this code:

https://github.com/WebAssembly/binaryen/blob/3f1287c4c30ff273972c7580d99ae1ca729ea108/src/passes/Asyncify.cpp#L667-L680

Would it?

(Btw, it looks like if you provide an only-list then it still does all the rest of the processing as if you hadn't. That's a waste of processor time, right?)

@kripken
Copy link
Member

kripken commented Feb 3, 2021

I'm not sure, but yes, I think that could reverse the logic if I'm not missing something. But again, that would need be optional, or a new flag. The design is the harder question, I think.

(Yes, there might be some wasted work there. By far the most work in the pass is flattening the IR and re-optimizing it, so it's no big deal to do more propagation.)

@rdb
Copy link
Contributor

rdb commented Feb 11, 2021

If adding an attribute is difficult, perhaps in the shorter term we could just have a call like emscripten_ensure_asyncify(), which would just be a call to a noop JS function that is added to ASYNCIFY_IMPORTS by default? It would be a useful convenience.

@stale
Copy link

stale bot commented Apr 16, 2022

This issue has been automatically marked as stale because there has been no activity in the past year. It will be closed automatically if no further activity occurs in the next 30 days. Feel free to re-open at any time if this issue is still relevant.

@stale stale bot added the wontfix label Apr 16, 2022
@curiousdannii
Copy link
Contributor Author

curiousdannii commented Apr 16, 2022

This is still important. Help would be very welcome!

@stale stale bot removed the wontfix label Apr 16, 2022
@curiousdannii curiousdannii changed the title Limitations of ASYNCIFY_ADD Make ASYNCIFY_ADD propagate (or add a setting to do so) Apr 17, 2022
@kripken
Copy link
Member

kripken commented May 2, 2022

I think the best path forward here is a new option. Changing the behavior of the existing one should not break users, but it could increase their code size very significantly, which worries me.

I do understand the point of view that always propagating is safer and better, but I think there is also a case for the reverse: sometimes a user knows a function should be added but does not want everything they call to also be added. They could manually remove those calls in the code (as mentioned above), and that would be better, but it may not be practical. Overall, the smallest code size can be achieved by finding all the stack traces needed in practice at runtime and adding those, without the things they call, which is what the current mode allows without any source refactoring.

Adding a new option should be pretty simple, I think. Perhaps ASYNCIFY_ADD_HINT or ASYNCIFY_HINT, since it not just adds the function but also hints that everything it can reach should be added as well?

@curiousdannii
Copy link
Contributor Author

curiousdannii commented May 2, 2022

ASYNCIFY_ADD_PROPAGATED?

ASYNCIFY_REMOVE seems to already propagate.

sometimes a user knows a function should be added but does not want everything they call to also be added

It's the other way round: all callers to these functions need to be instrumented, not everything they call. Which I'm sure you know! But writing about these things can be tricky ;)

@rdb
Copy link
Contributor

rdb commented May 3, 2022

Maybe there could be both an option (I don't care what it's called, but I might offer ASYNCIFY_MARK_INDIRECT) as well as an attribute that could be applied in the source code that has the same effect?

@kripken
Copy link
Member

kripken commented May 3, 2022

It's the other way round: all callers to these functions need to be instrumented, not everything they call. Which I'm sure you know! But writing about these things can be tricky ;)

Oh, yes, I had that reversed... 😄

ASYNCIFY_REMOVE seems to already propagate.

Ah, I guess it does, in the sense that it prevents propagation that would have happened if it were not removed? Fair point.

Maybe I'm too negative on the idea of just making add propagate. I'd feel more comfortable if we had more feedback from users of Asyncify atm. ping @caiiiycuk @gabrielcuvillier - are you perhaps still using Asyncify these days, and using ASYNCIFY_ADD? If so, would making it propagate cause any issues? Concretely, propagation would mean that any function in that list, we'd also automatically add all functions that we can see statically can reach it.

@gabrielcuvillier
Copy link
Contributor

gabrielcuvillier commented May 7, 2022 via email

@curiousdannii
Copy link
Contributor Author

curiousdannii commented May 7, 2022

ASYNCIFY_IMPORTS already propagates by default, and it's safe by design, though potentially not quite performance optimal. If through manual analysis you determine a function doesn't need to be instrumented you can use ASYNCIFY_REMOVE which also propagates. Probably the main use of this would be when there's a function that sometimes calls an ASYNCIFY import and sometimes doesn't and you can reliably determine which callers will do which.

The main use of ASYNCIFY_ADD is when you use ASYNCIFY_IGNORE_INDIRECT and then you have to add back a few functions which would indirectly call functions which call the imports. And ASYNCIFY_REMOVE is still an option if your analysis leads you to determine some functions never result in an import being called.

Not propagating by default is actually the dangerous option. We should be taking advantage of the compiler's static analysis to include all callers. And the REMOVE option is still there to improve performance, but it's a risk that can be taken only when the human has determined it's safe to do so. But without a propagating ADD option you're still required to propagate because all callers must be instrumented, just it's the human who's doing it rather than the compiler. Which is slow and difficult, and silly considering the compiler already has the functionality to do it, we just need to instruct it to.

@gabrielcuvillier
Copy link
Contributor

Well I had some weird cases that under some code path would lead to yield, but under other code paths it would not (emscripten_sleep is called under "if" conditions that occurs only in very specific cases in the overall loop). So static analysis was too eager for the job at the time I did this.

I didn't though about the ASYNCIFY_ADD + ASYNCIFY_REMOVE combo, but I guess it was because these options were not existing at the time I did the ports (hence my usage of ASYNCIFY_WHITELIST which is in fact deprecated/replaced by ASYNCIFY_ONLY since some time). So my thoughts on this issue are probably a bit outdated.

I am wondering what would happen if a function is found in the propagation list of both REMOVE and ADD (if this is something that may happen)

@kripken
Copy link
Member

kripken commented May 9, 2022

Good points!

Ok, I think I'm convinced that we should accept a PR to modify ASYNCIFY_ADD to propagate, given all that, and the lack of a clear reason or user that would be harmed. However, I'm also curious about @gabrielcuvillier 's last question about interactions between ADD and REMOVE. We should at least document that carefully when making the change.

@curiousdannii
Copy link
Contributor Author

curiousdannii commented May 10, 2022

I think how it works is:

  1. Take the list of ASYNCIFY_IMPORTS, plus any functions that make indirect calls (unless ASYNCIFY_IGNORE_INDIRECT is enabled)
  2. Work backwards, adding any functions that call these to the list, unless the function is in the REMOVE list
  3. Keep going until all callers have been walked
  4. Add the ADD functions to the list

The REMOVE list propagates through the process, but the ADD list is just appended at the end. Instead it should be added to the list in step 1.

Nothing stops a caller of a REMOVE function from being added to the list via another function call. All REMOVE does is stop that single function from being added, stopping the algorithm from walking further from that point, but it's not a promise that its callers will never be instrumented. So if the callers of a REMOVE function can be reached another way they will still be added to the list.

If something matches both the ADD and REMOVE lists, that's tricky. I'd say that if it matches a wildcard in one list and a specific name in the other list, whichever is specific should win. I doubt the code currently does that.

A specific function name in both lists should be an error.

@sbc100
Copy link
Collaborator

sbc100 commented May 10, 2022

If we add the ASYNCIFY_ADD list during (1) then how ASYNCIFY_ADD any different to the existing ASYNCIFY_IMPORTS?

@curiousdannii
Copy link
Contributor Author

I have a vague memory that I tried adding non-JS functions to the IMPORTS list and something complained, though I'm not certain. I think it does extra processing of the IMPORTS list?

Well I just tried it, and nothing complained, but also adding random C functions into ASYNCIFY_IMPORTS doesn't report them or any of their callers in ASYNCIFY_ADVISE. I'm not sure why, but it doesn't seem to have any effect.

Btw, if people are working on this stuff, it might be nice to take a look at #13179 too.

@kripken
Copy link
Member

kripken commented May 13, 2022

@curiousdannii

[..] The REMOVE list propagates through the process, but the ADD list is just appended at the end. Instead it should be added to the list in step 1.

That all sounds right to me.

A specific function name in both lists should be an error.

Sounds right to me too.

I'd say that if it matches a wildcard in one list and a specific name in the other list, whichever is specific should win. I doubt the code currently does that.

Yes, reading it, it has a PatternMatcher class and it matches or not using either the list or a wildcard, it doesn't care how it matches.

For simplicity, erroring on any match in both lists, even a wildcard, might be good enough, though I agree it would be nice for a wildcard to be ignored if the other is not a wildcard.

@sbc100

If we add the ASYNCIFY_ADD list during (1) then how ASYNCIFY_ADD any different to the existing ASYNCIFY_IMPORTS?

Good point that they are similar, but the list of imports is used for runtime assertions and stuff like that (e.g. to check that only stuff in ASYNCIFY_IMPORTS start a pause, and nothing else). So I think we should keep them separate.

If we're agreed on the above then I think we have a plan here. If so, I'd be happy to help with any guidance about how to go about the code for anyone that wants to work on it.

@curiousdannii
Copy link
Contributor Author

For simplicity, erroring on any match in both lists, even a wildcard, might be good enough, though I agree it would be nice for a wildcard to be ignored if the other is not a wildcard.

That sounds risky,.. start with a warning first?

@kripken
Copy link
Member

kripken commented May 18, 2022

Warning sounds fine to me too.

@curiousdannii
Copy link
Contributor Author

Since porting my C library to Rust, I now have to disable ASYNCIFY_IGNORE_INDIRECT in all 6 of my executables (compared to only 2 before). So for me this is even more important than before. At least it's only a performance/filesize hit, not a correctness issue.

Could I please ask that the team kindly prioritise this a little higher, to see whether the solution really is as simple as suggested in this comment? #13150 (comment)
Though adding warnings/errors for matches in both lists would be more involved.

@kripken
Copy link
Member

kripken commented Apr 1, 2024

@curiousdannii Re-reading the discussion above, I think the solution you propose in that comment would work, but it would be a breaking change. As I mentioned before, that has downsides for people that like the current behavior, which we have gotten reports of. So I think the best path forward is to add a new option, ASYNCIFY_ADD_PROP or a better name if we have one, which seems like the safest option overall.

I don't think anyone objects to adding a new option - if someone does please speak up - and if not then what's left is for someone to volunteer to take the time to add such an option in Binaryen and Emscripten. If someone is interested to do so and needs pointers on how to get started, let me know.

@kripken
Copy link
Member

kripken commented Apr 1, 2024

Actually wait, there is a PR that seems to be stalled, WebAssembly/binaryen#5935

Let's see if we can get that landed.

@kripken
Copy link
Member

kripken commented Apr 1, 2024

Good news, that PR was all ready to land, and I landed it now.

To use this right now you can use -sBINARYEN_EXTRA_PASSES=--pass-arg=asyncify-propagate-addlist. Perhaps we don't need to add a new option on the Emscripten side here, but it would be good to document and test this option.

curiousdannii added a commit to curiousdannii/emscripten that referenced this issue Apr 1, 2024
@curiousdannii
Copy link
Contributor Author

Awesome! I've made a PR to add an Emscripten setting, and enabled it by default as I think having it off by default is unsafe. But if the team thinks it shouldn't be on then it can just be switched off.

kripken pushed a commit that referenced this issue Apr 9, 2024
This setting affects whether the ASYNCIFY_ADD list propagates, that is,
whether it automatically leads to more things added based on the
inference. It is on by default, which is safer, but may increase code size
in some cases - to undo that, set it to 0.

Fixes #13150
impact-maker pushed a commit to impact-maker/emscripten that referenced this issue Apr 11, 2024
)

This setting affects whether the ASYNCIFY_ADD list propagates, that is,
whether it automatically leads to more things added based on the
inference. It is on by default, which is safer, but may increase code size
in some cases - to undo that, set it to 0.

Fixes emscripten-core#13150
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants