-
Notifications
You must be signed in to change notification settings - Fork 146
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
3.0.0-preview4, batch rendering, async, server-side #24
Conversation
Thanks for the PR. I'll have a look over the week and get back to you as soon as possible. |
any updates on this PR? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have any benchmarks regarding the speed difference between calling the methods directly and making them async?
```c# | ||
await this._context.ClearColorAsync(0, 0, 0, 1); // this call does not draw anything, so it does not need to be included in the explicit batch | ||
|
||
await this._context.BeginBatchAsync(); // begin the explicit batch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how easy it would be but what if you make this return an IDisposable
which calls EndBatchAsync
on disposal. The syntax would look something like this:
using (this._context.BeginBatchAsync()) {
await this._context.ClearAsync(BufferBits.COLOR_BUFFER_BIT);
await this._context.DrawArraysAsync(Primitive.TRIANGLES, 0, 3);
}
However I'm not sure if this is going to work because of all the async
stuff
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do like the fact that this paradigm prevents accidentally beginning a batch and forgetting to end it. You're correct that the async
"end batch" call complicates things, though. I could probably implement it using either a 'fire-and-forget' task, or a blocking call like the ones that are currently used in the old API wrappers. Either solution has a few major drawbacks, though, and I'm not sure they would be good solutions.
A fire-and-forget async
call in a Dispose
method would mean that the operation isn't necessarily complete after control exits the using block, and there would be no way to await
on it. That would prevent any follow-up code that depends on the operations inside the block being actually finished. Probably not feasible from a usability perspective.
A blocking call avoids that issue, but it has another problem: what if the EndBatchAsync
call fails? Dispose
isn't supposed to fail, and using
statements tend to swallow exceptions in their try
block if the finally
block also throws. In the worst case the call might block indefinitely, leaving the thread blocked in the using
statement's implied finally
clause.
The IAsyncDisposable
proposal aims to resolve these concerns and some others. If it makes it into c# 8.0 we could take advantage of it. That proposal has been around a while, though, and I haven't seen any official word that it's going to arrive soon.
@jorolf I don't have any benchmarks, no. I assume that using the synchronous calls would be slightly faster. The smart batching system should keep the difference small in client-side situations where the calls are able to execute almost synchronously as it is. I decided to move entirely away from using the synchronous JS interop instance based on Microsoft's guidance: "We recommend that most JavaScript interop libraries use the async APIs to ensure that the libraries are available in all scenarios, client-side or server-side." They do say "most," though, and a library like this one may be a good candidate for making an exception. There do seem to be ways to detect at runtime whether an app is running client-side or server-side. We could dynamically detect when the code is running client-side, and call the synchronous JS interop methods on the static instance when possible. Or we could just leave the old API calls synchronous and leave it up to callers to use them appropriately. We would provide user guidance in the README and the code comments that explains that the I think any of those scenarios would be fine, really. |
Any update on merging and packaging this? |
I just started playing around with Blazor, so my understanding is still very superficial. Sorry If what I describe below is not as in depth as it could be. I tried this pull request out because I need a version of BlazorExtensions/Canvas that works with the Blazor 0.9. In my test application I have a for loop in C# calling 14400 times When I run my code without I also tried using a These results seem to point to |
@matteo-prosperi Your I'm concerned about the security implications of directly exposing an You might want to create your own fork, either from master or from mine. You can try replacing the current logic with your |
@WilStead I tried the I would love to be able to contribute an implementation but I don't have nearly enough understanding of JavaScript or TypeScript to help here. I am not sure what the security implications of running code from a string would be here. It is my understanding that there is no "server side" involved running this code. I tried reading articles about |
Note that @matteo-prosperi's approach only works when you're using simple types like numbers or strings. However when you're using types like arrays or images it breaks/doesn't work because you can't convert them into executable javascript code. (Well maybe you could but that would be really inefficient) |
@matteo-prosperi It is generally safe to On a production website that involves non-trivial business logic, a skilled hacker could cause practically any amount of trouble with the power to execute any javascript they please. |
@jorolf Yes, I agree. I didn't mean to suggest a solution, just to provide a performance baseline for batch operations and let you know that it is reasonable to achieve at least a 5X performance boost for some scenarios by playing with the way javascript invokations are performed. @WilStead I am not sure I understand your point. I don't think any page, probably even one without any javascript at all, is safe from a user injecting code in his own browser instance using the developer tools. But this is not my field of expertise, so I will not push this further. Also I am not really suggesting to go ahead with such an implementation, I just wanted to provide a performance reference other more elegant implementations can try to reach or surpass.
During the weekend I can try to pinpoint exactly what is slowing down this implementation. |
@WilStead I investigated this more to try identifying what are the reasons for the perfomance differences. When changing the data type from The number of calls on the C# side ( Finally adding/removing I hope you will find this information useful. :-) |
Just out of curiosity: is there a performance difference between using a class or a struct for |
@jorolf I tried using both a class and a struct and it didn't really make any difference. |
@matteo-prosperi That is definitely helpful information. I took some time to refactor the code to use object arrays instead of the custom struct, and to reduce the method call chain by a couple of steps. Hopefully that will lead to a significant performance boost. Unfortunately, I also attempted to update my PR to the latest blazor preview version (3.0.0-preview4), and I am no longer able to build and run the project successfully. The new blazor js throws websocket-related client disconnection errors. It may be wise to wait until the final asp.net 3.0 release. We should be getting a release date at Build next month, supposedly sometime during the second half of this year. Even though client-side blazor isn't expected for the first release, server-side is supposed to be included. Hopefully the API shape at least will be less fluid for both scenarios. Then I can update this to a PR that targets a release version, instead of preview bits that keep changing. |
Hello @WilStead Can you update the PR to the preview 4? Thank you! |
@galvesribeiro I did some work to update the project to preview 4, but it seems the CI build fails now. I wasn't able to see detailed logs to troubleshoot. Everything builds and the test projects are running correctly on two of my own systems. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for the update @WilStead!
Please just remove the reference to MyGet and we can move forward and merge it.
Thanks!
test/Blazor.Extensions.Canvas.Test.ClientSide/Blazor.Extensions.Canvas.Test.ClientSide.csproj
Outdated
Show resolved
Hide resolved
Thank you very much @WilStead for this huge PR! 💃 Will make a release later tonight! Thanks! |
Sorry for the massive PR that touches so much code, but it seemed necessary to do so in order to fully address the big issues. I made an attempt to implement the changes you've discussed in #12 and #13. I'll provide a rundown of the major changes and my reasoning:
In order to support Razor Components server-side, I updated the dependency to the latest version (0.9.0) and changed the base class of
BECanvasComponent
toComponentBase
(which supports both Blazor client-side and Razor Components server-side). The required updating the project to the .NET Core 3.0 preview. I altered the CI build to use that SDK.Since the static, synchronous
JSRuntime.Current
has been deprecated in favor of injectingIJSRuntime
and making async calls (the only option for server-side), I refactored the entire library to adopt an async approach. In most cases I left the synchronous API in place, and those methods now wrap the async calls, but are marked asObsolete
with a message recommending that users switch to the async version. In the case of property setters, wrapped async calls can result in unresponsive UI, so I opted instead for a breaking change: read-only properties with async property setter methods.I implemented a simple call batching solution. "Fire-and-forget" calls which do not return values (both property setters and method calls) execute immediately if there are no pending interop calls, and otherwise are stored in a list until the latest interop call completes. Access is synchronized with a
SemaphoreSlim
to avoid threading issues.Calls which return a value are never batched: it created a negative performance impact, instead of an improvement, when I attempted to implement a callback mechanism that cached incoming calls, awaited results, matched them to initial callers, then dispatched callback messages.
Server-side code and WebGL don't work well together. Because of the server-side rendering mechanism, the last draw operation overrides all previous operations. I implemented a solution which allows users to explicitly group calls into batches, using
BeginBatchAsync
andEndBatchAsync
API calls, analogous toBeginUpdate
andEndUpdate
desktop UI APIs. I'm not satisfied that this solution will resolve all the potential problems, and I admit that I didn't run any extensive WebGL tests (animation, etc), I just made sure that the basic tests already present were working.Speaking of tests, I added a second test project for server-side Razor Components. I also renamed the existing test project to reflect the fact that it's the client-side version (and updated it to v0.9.0).
Server-side Razor Components currently has a bug (dotnet/aspnetcore#6349) which I'm sure you're aware of, that prevents component libraries from loading static files. It seems based on the status of that item that they're close to releasing a fix, but in the meantime I added steps to the Typescript project's MSBuild which copies the compiled
.js
file directly to adist
folder in the server-side test project'swwwroot
, and also places a copy at the root of the solution (for easy user downloading).I updated the README to reflect all the changes.
I also incidentally made a change that I believe @jorolf suggested earlier: I added rules to
.editorconfig
to reflect the underscore naming convention in use throughout most of the library, and updated a few variable names that didn't already adhere to it.I realize that what I've attempted here may be way off-base from what you have in mind. If you prefer not to adopt this massive PR, feel free to simply copy any bits of code you find useful.