Quantum Mechanics of Calculations in JS

Hello, my name is Dmitry Karlovsky and I am ... unemployed. Therefore, I have a lot of free time for practicing music, sports, creativity, languages, JS-conferences and computer science. About the last research in the field of semi-automatic partitioning of long calculations into small quanta for several milliseconds, as a result of which a miniature library appeared $mol_fiber, I will tell you today. But first, let's designate the problems that we will solve ..


Quanta!


This is a text version of the same-name performance on HolyJS 2018 Piter . You can either read it as an article , or open it in the presentation interface , or watch a video .


Issue: Low responsiveness


If we want to have a stable 60 frames per second, then we have only 16 with a trifle of milliseconds to do all the work, including what the browser does to show the results on the screen.


But what if we take the stream for a longer time? Then the user will observe the lagging interface, the slowing animation, and similar UX deterioration.


Low responsiveness


Issue: No escape


It happens that while we perform calculations, the result is no longer interesting to us. For example, we have a virtual scroll, the user is actively pulling it, but we do not keep up with it and cannot render the actual area until the rendering of the previous one returns control to us to handle user events.


Can not be undone


Ideally, no matter how long the work we performed, we should continue to process events and be able to cancel the work that has been started, but not yet completed, at any time.


I'm fast and i know it


But what if our work is not one, but several, but the flow is one? Imagine that you drive on your newly acquired yellow lotus and drive up to the railway crossing. When it is free, you can slip it in a split second. But..


Cool car


Issue: No concurrency


When the crossing is occupied by a kilometer train, you have to stand and wait ten minutes until it passes. Not for that, you bought a sports car, right?


Fast wait slow


And how cool it would be if this line was broken into 10 trains of 100 meters each and there would be a few minutes between them to get through! You would not be too long then.


So, what are the current solutions to these problems in the JS world?


Solution: Workers


The first thing that comes to mind: but let's just carry out all the complex calculations in a separate thread? To do this, we have a mechanism for WebWorkers.


Workers Logic


Events from the UI stream are transmitted to the worker. There they are processed and instructions are already sent back to what and how to change on the page. Thus, we eliminate the UI stream from a large reservoir of computations, but problems are not all solved in this way, and in addition new ones are added.


Workers: Issues: (De) Serialization


Communication between threads occurs by sending messages that are serialized to the byte stream, transferred to another stream, and there they are sent to objects. All this is much slower than a direct method call within a single thread.


(De) serialization


Workers: Issues: Asynchronous only


Messages are transmitted strictly asynchronously. And this means that some of the features you ask for are not available. For example, you cannot stop the ascent of a ui-event from a worker, since by the time the handler is started, the event in the UI thread has already completed its life cycle.


Message Queues


Workers: Issues: Limited API's


The following API is not available to the workers.


  • DOM, CSSOM
  • Canvas
  • GeoLocation
  • History & Location
  • Sync http requests
  • XMLHttpRequest.responseXML
  • Window

Workers: Issues: Can't cancel


And again, we have no way to stop the calculations in the wok.


Stop it!


Yes, we can stop the whole worker, but it will stop all the tasks in it.
Yes, you can run each task in a separate worker, but this is very resource intensive.


Solution: React Fiber


Surely, many have heard how FaceBook heroically rewrote React, breaking all the calculations in it into a bunch of small functions that are started by a special scheduler.


Sly Logic React Fiber


I will not go into details of its implementation, as this is a separate big topic. I note only some of the features due to which it may not suit you ..


React Fiber: React required


Obviously, if you use Angular, Vue or another framework other than React, then React Fiber is useless for you.


React Everywere!


React Fiber: Only rendering


React - covers only the rendering layer. All other layers of the application are left without any quantization.


Not so fast!


React Fiber will not save you when you need, for example, to filter a large block of data by tricky conditions.


React Fiber: Quantization is disabled


Despite the stated support for quantization, it is still turned off by default, as it breaks backward compatibility.


Marketing trap


Quantizing in React is still an experimental thing. Be careful!


React Fiber: Debug is pain


When you turn on quantization, the callstack ceases to match your code, which significantly complicates debugging. But we will return to this issue.


All pain debugging


Solution: quantization


Let's try to summarize the React Fiber approach so as to get rid of the mentioned drawbacks. We want to stay within one stream, but break long calculations into small quanta, between which the browser can render changes already made to the page, and we respond to events.


flame charts


Above, you see a long calculation that stopped the whole world for more than 100ms. And from the bottom - the same calculation, but divided into time slices of about 16 ms, which gave an average of 60 frames per second. Since, as a rule, we don’t know how much time it will take to calculate, we can’t manually break it into pieces by 16ms in advance. therefore, we need some kind of runtime mechanism that measures the time of task execution and when the quantum size is exceeded, pausing the execution until the next frame of the animation. Let's think about what mechanisms we have for implementing such suspended tasks ..


Concurrency: fibers - stackfull coroutines


In languages ​​such as Go and D, there is an idiom like "coroutine with stack", aka "fiber" or "fiber".


import { Future } from'node-fibers'const one = ()=> Future.wait( future => setTimeout( future.return ) )
const two = ()=> one() + 1const three = ()=> two() + 1const four = ()=> three() + 1
Future.task( four ).detach()

In the example code, you see a function onethat can suspend the current fayber, but at the same time it has quite a synchronous interface. Functions two, threeand four- the usual synchronous functions that do not know anything about the filers. In them you can use all the features of javascript in full. And, finally, on the last line, we simply run the function fourin a separate file server.


It’s quite convenient to use the faybers, but for their support you need support for runtime, which most JS interpreters do not have. However, for NodeJS there is a native extension node-fibersthat adds this support. Unfortunately, no faybery are available in any browser.


Concurrency: FSM - stackless coroutines


In languages ​​such as C # and now JS, there is support for "stringless coroutines" or "asynchronous functions." Such functions are a state machine under the hood and do not know anything about the stack, so they have to be marked with the special keyword "async", and the places where they can be suspended are "await".


const one = ()=> new Promise( done => setTimeout( done ) )
const two = async ()=> ( await one() ) + 1
const three = async ()=> ( await two() ) + 1
const four = async ()=> ( await three() ) + 1
four()

Since we may need to postpone the calculation at any time, it turns out that we have to do almost all the functions in the application as asynchronous. This is not only that the complexity of the code, so also strongly beats on performance. In addition, many APIs that accept callbacks still do not support asynchronous callbacks. A striking example is the method of reduceany array.


Concurrency: semi-fibers - restarts


Let's try to do something similar to the faybera, using only those features that are available to us in any modern browser ..


import { $mol_fiber_async , $mol_fiber_start } from'mol_fiber/web'const one = ()=> $mol_fiber_async( back => setTimeout( back ) )
const two = ()=> one() + 1const three = ()=> two() + 1const four = ()=> three() + 1
$mol_fiber_start( four )

As you can see, the intermediate functions do not know anything about the interruption - this is the usual JS. Only the function oneknows about the possibility of suspension. To interrupt the calculation, it simply throws Promiseas an exception. On the last line, we run the function fourin a separate pseudo-fayber, which tracks the exceptions thrown inside the exception and if it arrives Promise, it subscribes to it resolvein order to restart the fayber later.


Figures


To show how pseudo-fiberers work, let's write not tricky code ..


Typical performance chart


Let's imagine that the function stephere writes something to the console and does some hard work for 20ms. And the function walkcalls twice step, logging the entire process. In the middle will show what is now displayed in the console. And on the right, the state of the pseudo-faibers tree.


$ mol_fiber: no quantization


Let's run this code and see what happens ..


Execution without quantization


So far, everything is simple and obvious. The pseudo-fiber tree is, of course, not involved. And everything would be fine, but this code is executed over 40 ms, which is no good.


$ mol_fiber: cache first


Let's wrap both functions in a special wrapper that launches it in a pseudo-file and see what happens ..


Filling caches


Here you should pay attention to the fact that for each place of the function call oneinside the file walk, a separate file was created. The result of the first call was cached, but instead of the second one, it was thrown Promise, since we had exhausted our time slice.


$ mol_fiber: cache second


Thrown in the first frame Promisewill be automatically re-created in the next one, which will restart the filer walk.


Reuse of caches


As you can see, because of the restart, we again brought "start" and "first done" to the console, but the "first begin" no longer exists, since it is in the file with the cache filled earlier, which is why its handler is more not called. When the fieber cache is filled, walkall the nested faybera are destroyed, since execution will never reach them.


So why was first beginbrought out once, and first done- two? It's all about idempotency. console.log- nonidempotent operation, how many times you call it, so many times it will add an entry to the console. But a fayber performing in another fayber is idempotent, it executes a hendlen only on the first call, and on subsequent ones it immediately returns the result from the cache, without leading to any additional side effects.


$ mol_fiber: idempotence first


Let's turn console.login the fayber , thus making it idempotent, and see how the program behaves ..


filling idempotent caches


As you can see, now we have entries in the file tree for each function call log.


$ mol_fiber: idempotence second


The next time Fyber is restarted walk, repeated calls to the function logno longer lead to calls to the present console.log, but as soon as we reach the execution of the faybers with an empty cache, the calls console.logresume.


Reuse of idempotent caches


Please note that in the console, we now do not display anything extra - exactly what would be displayed in the synchronous code without any faders and quantification.


$ mol_fiber: break


How is the interruption of the calculation? At the beginning of the quantum set deadline. And before launching each fayber, it is checked whether we have reached it. And if reached, then it rushes Promise, which is resolved already in the next frame and starts a new quantum ..


if( Date.now() > $mol_fiber.deadline ) {
    thrownewPromise( $mol_fiber.schedule )
}

$ mol_fiber: deadline


The deadline for a quantum is simple. To the current time is added 8 milliseconds. Why exactly 8, because there are as many as 16 for training a frame? The fact is that we don’t know in advance how long the browser will need to render, so we need to leave some time for it to work. But sometimes it happens that the browser does not need to render anything, and then at 8ms quanta we can stick another quantum into the same frame, which will give a dense packing of quanta with minimal processor downtime.


const now = Date.now()
const quant = 8const elapsed = Math.max( 0 , now - $mol_fiber.deadline )
const resistance = Math.min( elapsed , 1000 ) / 10// 0 .. 100 ms
$mol_fiber.deadline = now + quant + resistence

But if we just throw an exception every 8ms, then debugging with an exception stop enabled will turn into a small branch of hell. We need some kind of mechanism to detect this debugger mode. Unfortunately, this can only be understood indirectly: a person needs a time on the order of a second in order to understand whether to continue playing or not. This means that if the control did not return to the script for a long time, then either the debugger was stopped or there was a heavy calculation. To sit on both chairs, we add to the quantum 10% of the elapsed time, but not more than 100 ms. This does not greatly affect the FPS, but it reduces by an order of magnitude the stopping frequency of the debugger due to quantization.


Debug: try / catch


If we are talking about debugging, how do you think where in this code the debugger will stop?


functionfoo() {
    thrownewError( 'Something wrong' ) // [1]
}
try {
    foo()
} catch( error ) {
    handle( error )
    throw error // [2]
} 

As a rule, it is necessary that he stay where the exception is thrown for the first time, but the reality is that he only stops where it was last thrown, which is usually very far from the place of its origin. Therefore, in order not to complicate debugging, exceptions should never be caught, via try-catch. But completely without exception handling is impossible.


Debug: unhandled events


Typically, runtime provides a global event that occurs for each uncaught exception.


functionfoo() {
    thrownewError( 'Something wrong' )
}
window.addEventListener( 'error' , event => handle( event.error ) )
foo()

In addition to being cumbersome, this solution has such a flaw that all exceptions fall here at all and it is rather difficult to understand from which filer and filer the event occurred.


Debug: Promise


The best solution for exception handling is promises ..


functionfoo() {
    thrownewError( 'Something wrong' )
}
newPromise( ()=> {
    foo()
} ).catch( error => handle( error ) ) 

The function passed to the Promise is called immediately, synchronously, but the exception is not intercepted and safely stops the debugger at the place of its occurrence. A little later, asynchronously, it already calls an error handler, in which we know exactly which filer failed and which one failed. This is exactly the mechanism used in $ mol_fiber.


Stack trace: React Fiber


Let's take a look at the glass rays you get at React Fiber ..


Vapid stektrays


As you can see, we get a lot of React intestines. From the useful here only the point of occurrence of the exception and the names of the components above the hierarchy. Not much.


Stack trace: $ mol_fiber


In $ mol_fiber, we get a much more useful structure: no guts, only specific points in the application code through which it came to an exception.


Content Center


This is achieved through the use of the native stack, promises and automatic removal of guts. If you wish, you can expand the error in the console, as in the screenshot, and see the guts, but there is nothing interesting.


$ mol_fiber: handle


So, to break a quantum, Promise rushes.


limit() {
    if( Date.now() > $mol_fiber.deadline ) {
        thrownewPromise( $mol_fiber.schedule )
    }
    // ...
}

But, as you can guess, a Promise can be absolutely any - generally speaking, it’s not important for a file to wait for the next frame, the completion of data loading or something else ..


fail( error : Error ) {
    if( error instanceofPromise ) {
        const listener = ()=> self.start()
        return error.then( listener , listener )
    }
    // ...
}

Fiber simply subscribes to resolve promises and restarts. But manually throwing and catching promises is not necessary, because the kit includes several useful wrappers ..


$ mol_fiber: functions


To turn any synchronous function into an idempotent fayber, just wrap it in $mol_fiber_func..


import { $mol_fiber_func as fiberize } from'mol_fiber/web'const log = fiberize( console.log )
exportconst main = fiberize( ()=> {
    log( getData( 'goo.gl' ).data )
} ) 

Here we made it console.logidempotent, and maintaught to interrupt while waiting for the download.


$ mol_fiber: error handling


But how to respond to exceptional situations if we do not want to use try-catch? Then we can register the error handler by $mol_fiber_catch...


import { $mol_fiber_func as fiberize , $mol_fiber_catch as onError } from'mol_fiber'const getConfig = fiberize( ()=> {
    onError( error => ({ user : 'Anonymous' }) )
    return getData( '/config' ).data
} )

If we return something different from the error in it, it will be the result of the current fayber. In this example, if it is impossible to download the config from the server, the function getConfigwill return the config to the default.


$ mol_fiber: methods


Of course, you can wrap up not only the functions, but also the methods, through the decorator ..


import { $mol_fiber_method as action } from'mol_fiber/web'exportclassMover {
    @action
    move() {
        sendData( 'ya.ru' , getData( 'goo.gl' ) )
    }
} 

Here, for example, we downloaded the data from Google and uploaded it to Yandex.


$ mol_fiber: promises


To download data from the server, it is enough to take, for example, an asynchronous function fetchand turn it into a synchronous one with a slight movement of the hand.


import { $mol_fiber_sync as sync } from'mol_fiber/web'exportconst getData = sync( fetch )

This implementation is good for everyone, but it doesn’t support canceling a request when a faber tree is destroyed, so we need to use a more confused API..


$ mol_fiber: cancel request


import { $mol_fiber_async asasync } from'mol_fiber/web'functiongetData( uri : string ) : Response{
    returnasync( back => {
        var controller = new AbortController();
        fetch( uri , { signal : controller.signal } ).then(
            back( res => res ) ,
            back( error => { throw error } ) ,
        )
        return()=> controller.abort()
    } )
}

The function passed to the wrapper asyncis called only once and the wrapper is passed to it backin which the callbacks need to be wrapped. Accordingly, in these callbacks, you must either return the value or throw an exception. Whatever the result of the callback’s work, it will be the result of the fieber. Note that at the end we return a function that will be called in the event of premature destruction of the fiber.


$ mol_fiber: cancel response


From the server side, it may also be useful to cancel the calculations when the client has fallen off. Let's implement a wrapper over midlewarewhich will create a file in which the original will be launched midleware. And in case of disconnection of the client, it will destroy the fayber, which will lead to the destruction of the entire tree of the files, cancellation of all external requests and so on.


import { $mol_fiber_make as Fiber } from'mol_fiber'
const middle_fiber = middleware => ( req , res ) => {
    const fiber = Fiber( ()=> middleware( req , res ) )
    req.on( 'close' , ()=> fiber.destructor() )
    fiber.start()
}
app.get( '/foo' , middle_fiber( ( req , res ) => {
    //do something
} ) )

$ mol_fiber: concurrency


Faybery make it possible not only to cancel requests, but also to perform them in one stream. Let's imagine that the client makes 3 requests: the first requires long calculations, the second almost does not require them, and the latter is somewhere in between ..


Fast and slow queries


Above, you see a variant without quantization: until the first long request is completed, the rest are waiting for it. Red is marked by the processing of which query is occupied by the processor. In the second case, we took advantage of quantization, with the result that the quick queries calmly flown by, while the long ones were calculated.


$ mol_fiber: properties


Well, it's time to take stock.


Pros:
  • Runtime support isn't required
  • Can be canceled at any time
  • High fps
  • Concurrent execution
  • Debug friendly
  • ~ 3KB gzipped


Cons:
  • Instrumentation is required
  • All code should be idempotent
  • Longer total execution

$ mol_fiber is not a magic pill that you took and now everything has become chocolate. This is a tool that can help you automatically quantize computations without turning code into noodles. But you need to apply it wisely, understanding what you are doing and why. In addition, it is worth bearing in mind that this is still an experiment that has been tested in the laboratory, but has not yet been tested in combat. It will be great if you play around with this technology and share feedback. Thank you for your attention and feel free to ask questions.


Links



Call back


Feedback


Excellent : This is the only lecture, perhaps, which I listened to much more and more attentively than others)


Excellent : Awesome report, special thanks for the clear presentation of a complex topic.


Excellent : Super report. Especially in view of the fact that it is just for my current problem.


Excellent : Good in-depth technical report. I did not understand everything, but I was damned interested and wanted to try the proposed solution. I will review the video as soon as I have access.


Excellent : The report was cool, the current is a shame that I did not understand much how it works under the hood. And for the performance of a separate respect, sales did not think about the quantization of operations)


Excellent : great and interesting topic, but some confusion of the presentation of the material.


Excellent : Great theme which again is needed right here and now. There is only one minus, there are no examples of implementation and integration into current projects, because the product has just been written.


Excellent : Understand the importance of the problem and the approach to the solution. Advertised library, written by the speaker, as which there is no certainty.


Excellent : The approach is clear, but I still have doubts. If the server responds longer than 16ms, will I ever get a response? The numbers 16 and 8 are understandable, but if the browser render hits 8, it may not be good. It was necessary to ask these questions before, but I needed time to think. However, in any case, the author respects both the fact of the development of such an approach, and the "brightness".


Excellent : In general, I liked a lot of points - I feel good expertise and the ability to submit a topic. Thank!


Excellent : Good report and submission. Discovered an approach how to implement faybery. Loved it!


Excellent : Interesting report, but not quite clear where in the business it is applicable. And so for the general development of a straight gun report.


Good : It was interesting, in principle, even everything is clear, but I still want to twist my hands to wrap my head around it completely, but before I had time to do it, I still didn’t fully understand how it turns out to quantize long / heavy queries, but it seems to me just more clear will be in practice.


Good : Interesting topic, but a certain confusion of the presentation of the material.


Good : Practical applicability.


Good : It was just interesting to hear about the fayber and quantum mechanics, I still can not get. And so I will look specifically at the mol.


Good : The speaker failed to justify why it is worth using its implementation, there is no comparison with analogues. However, the report is interested, I will do it, if there is time.


Good : interesting framework idea.


Good : I have metaphysical disagreements with the author, but in general the report is interesting. I can’t say that I’ll immediately apply $ mol, but I’ll look at the Fayber, it’s interesting.


Good : Technically cool, I told quite well, I didn’t hear about it before. But at the beginning of the show about controlling the girl with the console and catching her is terrible. I wanted to leave.


Well : I learned something new for myself, but the topic is very specific and the presentation was a bit messy.


Well : If before that I had heard about $ mol only in a comic context, now I want to try a fayber at work. The presentation (pdf, not a report) was boring, but this was compensated by the girl at the beginning.


Good : It was interesting to hear about quanta, animation, and they say. But unfortunately, I do not see this practical use.


Good : Instead of manipulating a girl, it was probably worth writing a demo) that would be much clearer and clearer.


Normally : did not understand the report. need to revise.


Normal : In some places I missed what the reporter was saying. "Mola" library and "why?". It is a mystery code for the overhead.


So-so : bad feed, uninteresting speaker.


So-so : Interesting topic. But the author focused on banal and understandable things, and complex ones flew by instantly. There is not enough humor in such a complex topic. There is not enough performance comparison with other similar technologies.


So-so : The beginning of the report was very lively: the game with the girl looked funny. Then there was something not very clear and accessible (perhaps only for me). In the end, I did not understand the connection between the title of the report and what was happening there: how does quantum computing mechanics relate to rendering a 16ms frame?


So-so : I did not work with Fibers. The report heard the theory of the work of fiber. But I didn’t think at all how to use mol_fiber in my home ... Small examples are excellent, but how to apply it on a large application with 30fps in order to accelerate to 60fps - there was no understanding. Now, if the author paid more attention to this and less to the internal structure of the module - the score would be higher.


Also popular now: