excubo-ag / Blazor.Canvas

Home Page:https://excubo-ag.github.io/Blazor.Canvas/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Polyline method

Rowingwolf opened this issue · comments

Played with your library today. Was great.

I was wondering if you'd be willing to add a polyline method so I can avoid making the following call that makes numerous interop calls.

I'd done this before in Javascript and the polyline draws much faster than it does when using Blazor WebAssembly even with batching.

Current C#:
public async Task DrawPolyLineAsync(IEnumerable<this Context2D ctx, (double X, double Y)> pointArray)
{
if (null == pointArray || pointArray.Count() < 1)
return;

    await ctx.BeginPathAsync();

    var firstPt = pointArray.First();
    await ctx.MoveToAsync(firstPt.X, firstPt.Y);
    foreach (var pt in pointArray)
        await ctx.LineToAsync(pt.X, pt.Y);

}

Proposed JS:
function DrawPolyIine(ctx, pointArray)
{
if (null == pointArray || pointArray.length < 1)
return;

            ctx.beginPath();
            ctx.moveTo(pointArray[0].X, pointArray[0].Y);
            for (var pnt = 1; pnt < pointArray.length; ++pnt)
                ctx.lineTo(pointArray[pnt].X, pointArray[pnt].Y);        
            ctx.stroke();
}

Hi @Rowingwolf,

thanks for the suggestion. This is something to think carefully about, since it is an extension of the official API and should therefore be reasonably separated from the main API (as when the HTML API is extended, we don't suddenly have a conflict).

Regarding the concrete proposed API: Wouldn't it be more idiomatic to write the following?

LineTo(Context2D ctx, IEnumerable<(double X, double Y)> points)

So without the implicit moveTo in it.
I think this API could be more composable, considering for example other APIs for multiple curve segments etc.

Would do you think?

Yes, I agree. I am actually surprised it is not part of the Canvas API for performance purposes.

Perhaps offering options would be good. A PolylineAsync with the moveTo and a LineToAsync without.

I agree that changing to LineToAsync is a good idea. My first concern was be for someone porting their JS code over to Blazor and due to the override perhaps making a mistake....but I think the compiler should highlight it.

To keep these from being in conflict, perhaps an Extension object so the code is in a silo. So usage of new function name would be something like:

await ctx.Ext.PolylineAsync(points)

For the JS code I wrote the performance is much better. Using Blazor.Canvas, drawing 30x 360 points (needed per application requirement) takes almost 5 seconds on my laptop. Using my existing JS code the same action is almost instant.

The target device will be a tablet so I expect performance there to be worse....and I often need draw a different dataset -- doing that without a high performance polyline routine would mean the user would have a great deal of lag when reviewing data.

I have considered wrapping my existing code to do the plotting in JS, but I think would prefer to port my JS to C#.....I just am tired of JS and the constant churn.

Hi @Rowingwolf,

thank for your feedback.

I was considering extension methods for this kind of API extension, but I'll have to look into that. The usage would then be

using Excubo.Blazor.Canvas;
using Excubo.Blazor.Canvas.Extensions; // additional API not available without this

var ctx = await canvas.GetContext2DAsync();
await ctx.MoveToAsync(1, 1);
await ctx.LineToAsync(points); // potentially: LinesToAsync(points) ?
await ctx.ArcAsync(arcs);

The benefit of not including the moveto is that you get a better composability between different types of paths. A single moveto in between the sections, should it be required shouldn't hurt performance all that much.

Are you able to share your code drawing those points? I would like to use that as a benchmark when experimenting on this addition.

Does this project also include your experiment with blazor?

That is just the JS. Here is the Blazor test:
https://1drv.ms/u/s!AksqXQUjQt-5g-UZ5xZMrodoKk1XoQ?e=du27bk

Thanks, @Rowingwolf. This is a good starting point to see whether additional APIs have the desired effect. A quick note on your code though: You're currently ignoring the batch, even though you're creating it. In addition to creating it, all methods need to be executed on the batch rather than the context. That cuts the render time in half on my device.

Yes, that helps, but is still about 1.5 seconds. Which is much better, but is still far slower than the JS version. I assume it is due to interop overhead. By doing the lineTo loop in JS, it cuts out that overhead.

Hi @Rowingwolf,

I've done a little bit of experimentation and can share some first insights:

Blazor wasm:

  • without batching: 9500ms
  • with batching: 5800ms
  • experimental code: 2500ms

A much higher performance is possible with blazor server-side rendering, but here batching is actually superior to the proposed new method:

  • without batching: 21129ms (yikes!)
  • with batching: 130ms
  • experimental code: 220ms

In conclusion: there absolutely would be a performance improvement for blazor wasm, but it can't get better than about 2400ms, as that's the time it takes to transfer the amount of data from wasm to js. Potentially unmarshalled interop could be better? This is not something we'll do in this repo though (I can be convinced of it by a super-stable pull request 😉). If you need better performance than this (which I would think you do), then either your data needs to reside in js in the first place and the rendering should be done there as well. Alternatively, switch to server-side blazor and simply use batching.

BR
Stefan

For the application that is my current priority, I have to use wasm -- the software is being run in areas that often have no w-fi or cell service.

An option that avoids unmarshalled interop but that offers better performance would be to use IJSInProcessRuntime and call using Invoke which would make the call synchronous. That would be faster than InvokeAsync while avoiding InvokeUnmarshalled. My assumption is that you are concerned InvokeUnmarshalled could change in the future? Until you commented about it, I didn't realize there was an unmarshalled option.

Hi @Rowingwolf

Some further experimentation:

  • IJSRuntime (async): transferring data (30x400 elements) takes 2400ms
  • IJSInProcessRuntime (sync): transferring data (30x400 elements) takes 2500ms

So no speedup here at all. The unmarshalled option has potential (I've read somewhere that a 50x speedup is feasible), but requires a lot of effort on the js side. My hunch is that most likely this library would profit most from unmarshalled interop, if we enable it for the batching feature (that would also mean implementing unmarshalled interop for just one js method).

Quoting from the docs:

While using WebAssemblyJSRuntime has the least overhead of the JS interop approaches, the JavaScript APIs required to interact with these APIs are currently undocumented and subject to breaking changes in future releases.

While it's tempting, I don't want to invest time into understanding an undocumented, potentially breaking API at this point. If you want to tackle this, I'm of course more than happy to review a pull request.

BR
Stefan

I read the same about unmarshalled calls -- that is why I assumed you wanted to avoid for now. If I can find some time in the next couple weeks I'll play around with it. I've got a deadline of Oct 15 for a project milestone due to a trade show.

Along those same lines, I came across an article this morning by Steve Sanderson touching on performance tips that I though was a good read.

Back to the question of a LineTo call that handles a set of coordinates, is that something you are interested in adding? Only asking because I suspect you might prefer to not include. If that is the case I probably need to fork the code. From your comments it provides a 2x speed improvement which is twice as nice as the current speed. ;)

The thing I dislike about PolyLineTo is that it's essentially a batch LineTo, and batching is already something that is done. I think it probably makes more sense to optimize the batching in general, rather than adding extension methods. I'll play around with that soon and I'll keep you updated.

The experiment of improving batching in general, rather than adding specialized methods was a success: it improves performance of batch processing in general, rather than just for one operation.

Here are the numbers:

env old new savings
wasm 244ms 120ms 50.8%
server 50ms 24ms 52%

This is possible with a combination of the existing batching and a new optimization for batching which you could call "bulk operations": The payload that needs to be sent from wasm to js, or from the server to the browser, is significantly smaller once we do the following:

  • for multiple consecutive assignments to a variable, only take the last assignment
    This is valid, as assigning to a variable multiple times consecutively (i.e. without any operation in between) will simply overwrite the previous values. We only have to consider the very last assignment.
  • for multiple consecutive invocations of methods, it's cheaper to send
[{"Identifier":"lineTo","Bulk":true,"Value":[{X:0,Y:0},{X:1,Y:1},{X:2,Y:2},{X:3,Y:3}]}]

than

[{"Identifier":"lineTo","Value":{X:0,Y:0}},{"Identifier":"lineTo","Value":{X:1,Y:1}},{"Identifier":"lineTo","Value":{X:2,Y:2}},{"Identifier":"lineTo","Value":{X:3,Y:3}},]

(note that in the actual implementation this is even more optimized, because we use one character names. There can be cases where this is less efficient, but I expect that in the vast majority of cases this is the best way to go.

This performance optimization will be available starting with version 2.3.0.

Please let me know if this satisfies your immediate need. Of course I would still be very happy to see an implementation using unmarshalled interop for Blazor wasm (which might further improve performance for wasm, but not for server-side blazor).

I think that should help.

I did some reading about unmarshalled calls and played a little bit with your project. For some reason I can't get breakpoints in your library to fire....so will have to figure it out. I need to be able to be able to access IJSInProcessRuntime.

Great, I'll close this issue for now, because unmarshalled interop is probably better talked about in a fresh issue / PR discussion. Looking forward to this! Thanks for raising this and helping to make the library better :-)