-
-
Notifications
You must be signed in to change notification settings - Fork 77
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
DOM building is very slow #131
Comments
I'm afraid that this problem may be inherent to the approach used by Threepenny-GUI. Essentially, building or modifying HTML elements with Threepenny is done by sending JavaScript expressions to the browser over a WebSocket connection, which are then evaluated with There are several bottlenecks:
Looking at your code, it appears that bottleneck 2 may be relevant. In particular, whenever you use the To quickly test whether this does indeed improve performance: The current Github version of Threepenny includes a function |
getElelementbyID is only used once to get the place to insert, and then to register the event handlers. I also thought this might be the bottleneck as it clearly requires a full roundtrip. But that suspicion did not turn out to be true. Removing them has negligible impact on the performance. If we only leave the DOM building code active, I can still see the elements appear slowly, one-by-one it the browser. It can take >100ms for each iteration of the loop, making the construction of the page take seconds. It should be possible to generate the entire page with essentially a single client/server communication as the entire HTML code could be build on the server, sent over in one go and put into the DOM with a single assignment to innerHTML. For instance, I could use blaze-html to generate everything, dump it into a file and embed it with an iframe into my site. Doing it this way would complete orders of magnitudes faster, so I can't imagine that there is some inherent problem why a couple of dozen basically static DOM elements wold require seconds to be created in the browser. |
I did some more experiments to see if there is any fundamental reason for things to be this slow and if I could work around those limitations. If I stop using threepenny's HTML combinator library and simply generate the page myself, then submit it using the FFI in one go, like this...
...the page loading / updating time goes from seconds to milliseconds. The addition of event handlers introduces some noticeable delays again, though. It's unfortunately not obvious how to batch submit those without digging deep into the library. In any case, it seems threepenny does hundreds / thousands of completely redundant FFI calls, submitting elements and attributes one-by-one. This results in page construction times that feel like browsing the web on a 90s 56k modem. There really needs to be some kind of batching with the UI monad / HTML combinators, otherwise this entire part of the library seems completely unusable for anything but very simple GUIs. This is especially bad in combination with bug #130, which requires frequent page reloads on mobile clients. Sorry if all this seems overly critical. I really like the threepenny concept, would love to keep using all of its components and it's very unfortunate that this entire part of the library is basically unusable with the performance of the current implementation. I'll just hack around this for now! |
As indicated, the possible bottlenecks are
I actually have some code that has the potential to address bottleneck 3, that is to address the relative slowness of JavaScript's
I probably don't want to change the fact that everything is an FFI call, as changing this would go against the spirit of the library. However, if it's too slow, then something ought to be done. It turns out that the bottleneck is actually 2! Whenever Threepenny creates a new HTML
That's alright, if it doesn't work, then it doesn't work; no point denying it. 😄 But I'm happy that you like it. |
You're right, given that the performance difference between localhost & LAN is so severe, it must be some form of back- and forth communication, a latency issue. I've rewritten my application yesterday to do the generation with blaze-html and then submit the page with a single FFI call. It now loads perceptually as fast as static HTML from the local disk. I still have to register ~75 event handlers after that, which unfortunately can take a good second or two over LAN. Each handler needs an Couldn't you batch the FFI calls somehow? For instance, I don't actually run the page building in the UI monad, I just build up a list of UI actions to register event handlers and submit them all later once the DOM is there. Couldn't you just build up all the required actions inside your RWS stack and then have a |
That doesn't help with bottleneck 2, only with bottleneck 1. Whenever an FFI call returns a result, the client has to communicate that result to the server, because the server could, in principle, decide to perform an IO action or something else depending on the value of that result. Put differently, the UI monad is not a "closed world", it allows arbitrary IO , so there is no way around having the client send results to the server. Bottleneck 1 can be addressed by batching FFI calls, at least those that use |
Pulling back from looking at the actual monad and FFI APIs, the HTML combinators seem declarative in nature and don't return anything. Of course, if you mix building up DOM elements with some FFI calls you could end up in a situation where you add elements, then eval() JS that depends on these elements being in place already, making deferring / batching break the API. I'm just looking for a way to add maybe a few hundred elements to the DOM without it taking seconds. My solution so far is to build my own HTML with blaze-html and insert it on the client with a single threepenny FFI call. Can't the speed of this solution be somehow replicated with threepenny's HTML combinators as well? Right now, after changing the HTML construction to blaze + 1xFFI, the slowest part of page building is registering events. I'm basically doing this ~75x:
This does a number of roundtrips over the websocket, meaning those ~75x events can take up to a second to be registered. This could, theoretically, all be done in a tiny fraction of that time with a single one way message containing some JS to be eval()'ed in the spirit of this:
I'm not sure if that can be expressed with the current API. Just spitballing here. |
Before this change, the `mkElement` and `mkElementNamespace` functions waited for the browser to return the stable pointer associated to the corresponding element. Now, the server supplies the stable pointer and does not wait for the browser. This should improve performance.
The latest commit 3e9c4e1 addresses bottleneck 2. Whenever Threepenny creates a new HTML Could you test whether this helps with your original code? (This change is a prerequisite for batching FFI calls: In the current API ( |
I hope I didn't make a mistake, but performance does not seem to change perceptively (didn't measure) from Hackage 0.6.0.6. I build with my last version that used threepenny HTML elements (a980d0c) and made sure Stack pulls in the commit you just made from GitHub. |
Ok. Assuming that the old Threepenny code was not in the compilation cache, then my commit didn't help as much as I had hoped for. Say, would it be possible for you to make a branch in your |
Stack rebuild threepenny and it worked fine when testing out the %-escape fix, so I'd assume it build fine :/ Having an 'offline' mode has been on my TODO list for sure. I'd implement that right now, but the problem is that the code which actually uses the threepenny HTML combinators is quite a few revisions behind. So it wouldn't help much if I implemented it on top of HEAD, I guess. Maybe I could do so and create a branch where I do some cherry picking and rebasing and whatever to backport these changes. There also seem to be external tools to mock a RESTful API. I'd have simple requirements, basically just a few static responses to a handful of queries. If you happen to have a favorite web service mocking tool, I'd be happy to just use that and provide you with the dataset to run my program. Otherwise, I'll have a look at implementing & backporting an offline mode. |
For the purpose of debugging this issue, I think there is a simpler way. Looking at your code, why not simply |
You're completely right, I'm way overthinking this. I'll make a branch with what you suggest, but it might take a few days. |
Sounds good! |
There's now a branch called 'threepenny-offline', hacked up from the latest revision that used the threepenny HTML combinators for building the initial page. Right now, in HEAD with blaze for the actual page building, this is the slowest part for me:
I'm a bit worried this will be too slow if somebody has 30-40 lights. It seems the speed of the handler registration is also dependent on the client. Desktop browsers register the events much faster than mobile ones. Server speed is irrelevant, moving from a Mac to a Raspberry Pi had no impact. Latency also matters, of course, localhost is faster than LAN. |
Thanks for the |
Great! Setup should all happen automatically with stack. I recently did a build in a fresh VM and it worked without any manual intervention etc. |
I managed to try out the |
Thanks, good to hear! ;-) The current development branch |
I'm confident that this can be fixed, too. There are two ways to obtain an
When registering events, your code uses the latter approach, which is probably more familiar from JavaScript. However, the former approach has the advantage that the browser never has to send anything back to the server (thanks to commit 3e9c4e1 ), which allows us to batch this call. Once batching is in place, the first approach will be much faster. It is also more natural from the Haskell point of view: elements are referenced by variables, not by "id" strings. |
That's good news, in principle. Currently, I of course need to rely on |
Is there any progress / ETA on this? I don't need a nice or elegant solution, just anything at all to work around this bug. I'd be perfectly happy to just submit everything in one batch for better performance. The DOM building stuff isn't even important as there is at least a viable work around, but the event handling is more tricky to fix from outside the library. It's difficult to provide a good user experience (especially with #130 also being a factor) if the page load times are several seconds even after not using any HTML combinators, and it's not really possible to do much work if any UI handler adds another ~50ms or so to the load time. |
I know what I need to do in order to fix both the DOM building and the event handling (batch everything), it's just a matter of finding free time right now… It's the next programming task I do, but it may be another couple of weeks before enough free time has accumulated. |
That would be great! ;-) I just pushed a basic benchmarking commit to the FF localhost:
FF LAN, faster machine:
Safari, localhost:
iPhone 6 Safari LAN:
This is assuming the FFI calls are blocking. It's interesting to see that page building now seems to be largely independent of the network and server machine. LAN vs localhost makes no difference, but i.e. Safari seems to be a lot faster than Firefox, faster client machines result in faster times. For page building there are 3 FFI calls (one to get the user ID cookie, one to get the client's time and the final one to submit / insert all HTML). In any case, since it's at worst ~100ms all should be fine, and the limiting factor appears to be how fast the client can parse / insert the HTML. Event registration is slow in generally but only really unacceptably slow on iOS. It fluctuates wildly between 2s and 3.5s on an iPhone 6, considerable slower on an iPad Air. This is of course the worst problem as it delays the readyness of the applications by a second or so for every few dozen events. Why do you think the event registration takes so long on mobile devices? It can't be that they're just generally slow, the entire page building process seems to be very fast, the website comes up very quickly. It also can't be the network, as other machines on the LAN do much better. I just want to make sure we're actually looking at the correct issue, not just improving something that doesn't make much of a difference. |
Like I said, I already replaced all the DOM building code with The I think the issue is really that registering events should either not require a network round trip or that there should be a way to batch all the events together so they can be registered on a single round trip. If there's no way to automatically fix this in the library, I'd be perfectly happy with either |
Hm, that's the part I don't quite understand yet. As of commit 77bfa45 , registering an event to an existing (It is, however, currently true that if you call
I guess what I want to say is that I have optimized the case where Threepenny is used for DOM building. Your approach goes a bit beyond my original vision for Threepenny, but then again, I don't see a reason to not include support for your use case.
That seems to be the only way if you want to stick with HTML IDs as opposed to |
I'm probably 'abusing' threepenny a bit at this point. I'm also new to the world of web application development, so thanks for your patience ;-) You're completely right, currently I only have the round trip because of the
I think it would be super useful to have some way of registering events on plain element IDs without the current networking overhead. |
Hi @HeinrichApfelmus i tested your batch branch and observed huge speedups in load times of many elements pages and near zero problems, the only problem i found was that run call messages were retained when a server push occurs. My solution was to add a polling flush thread to the call buffer . Is there anything pending for merging to master? If you want to the code i can send a pull request. |
Could you be more specific? The library user has to call |
@HeinrichApfelmus might be this
i never call flushCallBuffer in the client. But in my application i have problems only when the server generate messages without any client event triggering (what i callserver push) . All the other client calls works fine because of the flush after the event handler in the Foreign.Javascript exportHandler function. |
btw, is there anything here I ought to test? Maybe I did not follow the discussion / commits correctly, but is there any branch / commit etc. that contains a fix for this issue? Those from Sep 16th? |
@blitzcode No, you didn't miss anything. You have already seen the commits from Sep 16th, they are simply a rebase of the older I still need to write up (and test) the workaround… |
@blitzcode Finally! I have written up the workaround that you are probably looking for. 😄 As of commit a32b849 , you can now use As far as I can tell, you only use EDIT: (If you use the |
@HeinrichApfelmus Sweet! I'll have time over the next week to try this out in detail, I'll report back! |
For backward compatibility, the default buffering mode is currently still `NoBuffering`.
Update: I have just pushed the batching / buffering stuff into master. However, for reasons of backwards compatibility, the default mode is still "no buffering" . You have to use
in order to enable the new buffering mode. It should speed up page loading considerably! See the |
So, I had a shot trying out your changes. Looks really neat! I think it's a very helpful addition and quite elegant. I like that I can turn the new behavior on & off, so I can register my event handlers and then go back to the simpler, synchronous mode, i.e.
Very cool. Seems to work as designed as well, in my perf stats the event registration time is down quite a bit. Two questions:
|
Great! 😄
|
Ok, thanks for clarifying! I was able to duplicate the behavior of
A bit messy, but I guess that is what you do? One strange thing I noticed is that some events were failing. After some debugging, I noticed some of my element IDs had spaces in them. That actually worked fine with |
Actually, it turns out that spaces are not allowed in DOM element IDs. The jQuery idiom
Looks fine to me! 😄 I do essentially the same thing, except that instead of declaring a new function, I use a JavaScript function from an external file. Code. The difference should be negligible, however. |
Sure, I wasn't complaining about the spaces issue, more of a 'how did that ever work?' type reaction. Fixed on my side. So, thanks again for these fixes! Not only did you improve all the build in combinators, you also implemented a new way to do event registration and a batching mode for Are you doing a new Hackage release with all the various fixes? If I could have one more xmas gift in the next release, it would be a fix for #130. The only workaround I could come up with for that one has some very unfortunate side-effects, like being incompatible with the special HTML-app modes that iOS/Android have. With these you could add a threepenny application to the home screen, have it load as its own process, no browser controls etc. It would go a long way in making threepenny based software feel more slick and native on mobile devices. I'd love to support that, especially now that the loading times are so fast... |
You're welcome! I'm glad I could help and I'm happy that you appreciate it. 😄
Well, I can certainly look into it. 😉 But that's probably best discussed in the other issue ticket. For this issue, am I right in assuming that the original problem, slow DOM building, has been solved to your satisfaction and this issue can be closed? |
Sure, closed! This was certainly a lot of work to figure out how to solve correctly, thanks! |
Clarifies a question that arose in issue #131 .
Building up even just a few dozens of DOM elements can take a very long time. Example:
https://github.com/blitzcode/hue-dashboard/blob/8a9e5ac466aadd8b6f8fa4557fe2d931a89d03d2/WebUI.hs#L124
On localhost it can easily take a second for the content to arrive in the browser's DOM, on a mobile device over LAN it can take several seconds. I suspect this is because there's back & forth between threepenny and the browser for every single element added? A workaround could be to just build up some HTML locally with something like blaze-html, but I'd of course think it would be better if threepenny just did the batching by itself.
The performance right now makes it difficult to generate even a moderately complex page in time, dynamically building things like a modal dialog etc. seems impractical.
The text was updated successfully, but these errors were encountered: