Splitting up draw ops - Datatables with async draw operations... thoughts?
Splitting up draw ops - Datatables with async draw operations... thoughts?

I have done something horrible. My shame compels me to share how I have sinned, but I am also interested in feedback on this. Read on, ye brave souls, and despair.
I have an application that uses DataTables (locally; NW.js, essentially) and was encountering issues with DataTables gobbling too much CPU time whenever results were added to the tables. The host system is not powerful, with an Intel J4105 for a processor; approximately half the single-threaded & multi-threaded performance of an old Core i5 3570K - and when a result was added to the tables, and said tables already had some 50-100 results, the table.draw() process would occupy the UI for 300-500ms at a time. For each table! We do have a custom fnRowCallback that injects a fair bit of custom content to each row, so it's probably that which is making the draw operation expensive.
This wouldn't be a problem, except, the UI also has other things to do, such as listen to one or more serial port data streams, and data was being lost if it could not be attended to in due time.
Essentially, the application worked great until it reached a certain number of results gathered, and then things started to break.
The performance profiler revealed a lot of time was being spent in _fnDraw() as it spammed calls to _fnCreateTr(), with the latter taking anything from <1ms to a whopping 15ms per call to do its thing. Meanwhile the UI is doing nothing else but waiting for this process to complete.
This is the point where I thought "it sure would be neat if I could chuck DataTables in a web worker", but as far as I am aware, this would go terribly wrong on account of not having any access to the DOM. I've seen projects that purportedly offer ways around this but I have yet to dare explore them.
Nor do I have the luxury of cramming the serial port handler into a web worker; at least, if it worked it would be a huge undertaking.
There's not much I can do to the custom fnRowCallback either; it's quite barebones already, just the minimum stuff needed in order to achieve the necessary behaviour for each row (mainly, coloured backgrounds for the results, based on value).
So... as I said... I did something horrible.
I went into DataTables _fnDraw(), and I wrapped most of its guts in an anonymous async function... then inside the For loop that iterates over each row to be drawn, I wrapped the call to _fnCreateTr() in a setTimeout of zero ms (to force it to execute "later") and wrapped that in a Promise which is then blocked by await, so the For loop wouldn't run away without each row being drawn.
The idea behind this unholy abomination was to split the DataTables draw operation into a series of hundreds of micro events instead of one big monolithic operation; thus allowing other time-critical housekeeping to continue, such as attending the serial port data streams.
The worst part is, it works. It works perfectly. The tables do take a little longer to draw, but they are almost completely nonblocking on the rest of the UI, and even if they had 1000 results - 10-20x more than caused issues before - there are no problems for the UI in keeping up with other important tasks. If instead of directly setting table data I plug them into a faujax handler I wrote (because there is no remote server... all local...), I can skip any lengthy time spent sorting and filtering the data too, since my faujax can do that in just a couple milliseconds for thousands of rows.
Now, that isn't the whole story. I did have to make some additional (horrendous) tweaks; for instance, with the draws becoming asynchronous (and thus, non-blocking) there had to be an awareness somewhere of whether an as-yet-unfinished draw op was still taking place, so the table didn't try to draw itself while it drew itself ("Yo dawg, I herd you like DataTables..."), so each table's settings are extended with a state variable to indicate whether a draw is underway, and the API was extended to add a variant of the draw function that could keep attempting to start a draw operation until it was safely able to complete... a variant for .clear() was also made promise-based, and again resolves only when the table is "safe" so you know you can further manipulate the table, if you're doing that... all quite horrible, I assure you.
But it works. It works really well. The UI remains smooth and responsive throughout, even with lots of data being handled. There do not appear to be any issues with duplicate rows, missing rows, adding or deleting rows (even in rapid succession, faster than the table can nominally draw), sorting tables, errors, crashes or anything. I keep trying to find ways to break it and (so far) it's behaving perfectly.
As if it's waiting, for when I least expect it... For when I lower my guard... Then, it shall get revenge.
Anyway, before the software gods smite my being from this plane of existence I thought I would share this evil little experiment and solicit any feedback, even if it's just "but why" or "vomiting noises". Despite it fundamentally breaking how DataTables is intended to work as far as the API is concerned, I do think there is value in having DataTables perform draw operations in this manner... but then I would be biased, wouldn't I!
Answers
Hi,
Thanks for the write up. Interesting approach - as you say it is a bit of a tradeoff. Trading extra processing time for a more responsive UI while it is drawing, since it can pause while other actions are run by the processor.
At lot of the DataTables extensions code assumes that the draw will be completed by the time the
.
draw
event triggers, so it would be important that this is still the case, but otherwise, good stuff. If that suits your needs - perfectAllan
Thanks for making DataTables! And for tolerating what I did to it, haha; I appreciate the feedback!
I've been tentatively testing it ever since; so far, so good. If I'm not mistaken (entirely possible I am), the
draw
still won't trigger until drawing has completed, since the callback in question_fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings] );
is also wrapped inside the anonymous async function, and the preceding For loop is blocking on each row until all the async draw ops are complete.That said, I'm not really using any plugins except for Scroller, so I might just be unknowingly skirting around disaster by accident!
Scroller is likely to be the most sensitive one for this sort of thing to be honest, so if that is working, excellent!
Allan