📜 ⬆️ ⬇️

Managing JavaScript UI stream using WinJS scheduler

From the translator: the article describes the new task scheduler in the WinJS 2.0 library, updated with the release of Windows 8.1. To understand some of the material, it is highly desirable to understand how to work with deferred results (Promise). See the MSDN section on asynchronous programming with JavaScript .

Apart from web workers and background tasks, which are also executed as separate web processes, all JavaScript code in Windows Store applications is executed in the general so-called UI stream. This code can make asynchronous calls to the WinRT API that perform their operations in separate threads, but there is one important point to keep in mind: the results from these non-UI threads are sent back to the UI stream for processing. This means that launching a series of asynchronous calls to WinRT (for example, HTTP requests), all at once, can potentially overload the UI stream if the results come from them at about the same time. Moreover, if you (or WinJS) add elements to the DOM or change the styles that require updating the page layout in the UI stream, this creates even more tasks competing for CPU resources. As a result, your application becomes “slowing down” and non-responsive.

In Windows 8, an application can take a series of steps to reduce such effects, for example, run asynchronous operations within temporary blocks to control the frequency of returns to the UI stream, or combine tasks that require a page refresh cycle so that more operations are performed in one pass. Starting from Windows 8.1, it became possible to asynchronously prioritize various tasks directly in the UI thread.
')


Although the application host offers a low-level scheduler API ( MSApp.executeAtPriority ), we recommend using the WinJS.Utilities.Scheduler API instead . The reasons for this recommendation are that WinJS manages its tasks through this scheduler API, which in turn means that any work that you manage in the same way will be properly coordinated with the work that WinJS does for you. The WinJS scheduler also provides a simpler interface to the entire process, especially when it comes to working with pending results (promises).
It is important to note that the use of a scheduler is not mandatory. It is needed to help you tune the performance of your application, and not to complicate your life! Let's first understand the different priorities that are used by the planner, and then we will see how to plan and manage the work with these priorities in mind.

It is important to note that the use of a scheduler is not mandatory. It is needed to help you tune the performance of your application, and not to complicate your life! Let's first understand the different priorities that are used by the planner, and then we will see how to plan and manage the work with these priorities in mind.

Scheduler Priorities


The relative priorities for the WinJS scheduler are listed in the Scheduler.Priority enumeration, in decreasing order they look like: max , high , aboveNormal , normal (default for application code), belowNormal , idle and min . The following is a general guide to how best to use them:



Although it is not necessary for you to use the scheduler in your code, a small analysis of the use of asynchronous operations will most likely reveal the places where prioritization can play a great role. For example, you can prioritize non-UI work while the splash screen is displayed, because the splash screen is not interactive by definition, or send the most important HTTP requests with max or high priorities while setting secondary requests at belowNormal . This will help to process those first requests before drawing the home page, on which the user expects interaction with the content, and then you can work out the secondary requests in the background. Of course, if the user navigates to a page on which secondary content is needed, you can change the priority of this task to aboveNormal or high .

The WinJS library itself is actively using prioritization. For example, it will work out the source change blocks to bind data with a high priority, and scheduling cleaning tasks will be done with idle priority. In complex controls, for example, ListView, requests for new items required for rendering the visible part of the ListView are done with maximum priority, rendering of visible items is done above normal , preloading the next page of items (forward) is normal (assuming that the user will scroll further), and the preloading of the previous page (for the case of reverse paging) is done on belowNormal .

Planning and Task Management


Now that we know the priorities of the scheduler, we can talk about what needs to be done in order to asynchronously execute code in the UI thread with the right priority. To do this, call the Scheduler.schedule method (the default priority is normal ). This method gives you the opportunity to specify an optional object to use as this inside a function, as well as a name to use for logging and diagnostics. (The Scheduler.execHigh method is a short link to a direct MSApp.execAtPriority call with Priority.high priority. This method takes no additional arguments.)
As a simple illustration, scenario 1 of the HTML Scheduler example adds to the scheduler a set of functions with different priorities in some random (js / schedulesjobscenario.js):

window.output("\nScheduling Jobs..."); var S = WinJS.Utilities.Scheduler; S.schedule(function () { window.output("Running job at aboveNormal priority"); }, S.Priority.aboveNormal); window.output("Scheduled job at aboveNormal priority"); S.schedule(function () { window.output("Running job at idle priority"); }, S.Priority.idle, this); window.output("Scheduled job at idle priority"); S.schedule(function () { window.output("Running job at belowNormal priority"); }, S.Priority.belowNormal); window.output("Scheduled job at belowNormal priority"); S.schedule(function () { window.output("Running job at normal priority"); }, S.Priority.normal); window.output("Scheduled job at normal priority"); S.schedule(function () { window.output("Running job at high priority"); }, S.Priority.high); window.output("Scheduled job at high priority"); window.output("Finished Scheduling Jobs\n"); 


The results window shows that “tasks”, when called, are executed in the expected order:

 Scheduling Jobs... Scheduled job at aboveNormalPriority Scheduled job at idlePriority Scheduled job at belowNormalPriority Scheduled job at normalPriority Scheduled job at highPriority Finished Scheduling Jobs Running job at high priority Running job at aboveNormal priority Running job at normal priority Running job at belowNormal priority Running job at idle priority 


I hope this is not a surprise for you!

When you call the schedule method, you get back an object that satisfies the Scheduler.IJob interface, which defines the following methods and properties:

Properties


Methods



In practice, if you have planned a low priority task, but go to the page where you really need the task to complete before rendering, you simply update its priority property (and then clear the scheduler, as we will see very soon). Similarly, if you have planned some work on the page and the need to continue it has disappeared when you move to another place, simply call the cancel method of your tasks in the page unload method. Or perhaps you have a start page from which you usually go to the detailed and back. In this case, you can pause ( pause ) any task on the start page when you go to the detailed one and then continue ( resume ) when you go back. For a demonstration, see scenarios 2 and 3 in the example.

Scenario 2 also shows the use of the owner property (the code is quite understandable, so you can easily study it yourself). The owner's attribute (token) is created via the Scheduler.createOwnerToken method, then assigned via the owner property of the task (this replaces the previous value). The owner's attribute is simply an object with a single cancelAll method that calls the cancel method for all tasks to which it is attached, and nothing more. This is a simple mechanism, in reality it simply supports an array of tasks, but it allows you to group related tasks and cancel them with a single call. Thus, you do not need to maintain your own lists and go through them for this action. (To make a similar decision to pause and continue, of course, you can simply repeat this pattern in your code.)

Another important feature of the scheduler is the requestDrain method. It allows you to make sure that all scheduled tasks with a given priority or higher will be executed before transferring control to the UI thread. Typically, this is used to ensure that all high priority tasks are completed prior to drawing. requestDrain returns a pending result (promise), which is implemented when all tasks are “cleared”. At this point, you can go to lower priority tasks or add new ones.

A simple demonstration is in scenario 5 of the example. It has two buttons that plan the same sets of different tasks and then call requestDrain with a high priority or priority belowNormal . When the pending result is executed, the corresponding message is displayed (js / drainingscenario.js):

 S.requestDrain(priority).done(function () { window.output("Done draining"); }); 


If you compare the two outputs in parallel ( high on the left, belowNormal on the right), as indicated below, you will notice that the delayed result appears at different times depending on the priority:

 Draining scheduler to high priority | Draining scheduler to belowNormal priority Running job2 at high priority | Running job2 at high priority Done draining | Running job1 at normal priority Running job1 at normal priority | Running job5 at normal priority Running job5 at normal priority | Running job4 at belowNormal priority Running job4 at belowNormal priority | Done draining Running job3 at idle priority | Running job3 at idle priority 


Another method defined in the scheduler is retrieveState , a diagnostic tool that returns a description of current tasks or cleanup requests. If in scenario 5 you add its call immediately after requestDrain , you will get the following results:
 id: 28, priority: high id: 27, priority: normal id: 31, priority: normal id: 30, priority: belowNormal id: 29, priority: idle n requests: *priority: high, name: Drain Request 0 


Prioritization in a chain of pending results


Imagine that you have a set of asynchronous methods that request data that you want to execute sequentially, as described below, processing their results at each step:

 getCriticalDataAsync().then(function (results1) { var secondaryPages = processCriticalData(results1); return getSecondaryDataAsync(secondaryPages); }).then(function (results2) { var itemsToCache = processSecondaryData(results2); return getBackgroundCacheDataAsync(itemsToCache); }).done(function (results3) { populateCache(results3); }); 


By default, all this code will be executed with the current priority against the background of everything else that happens in the UI thread. But you might want the processCriticalData function to execute with high priority, processSecondaryData to work in normal mode and populateCache in idle . By working directly with the scheduler, you would have to do everything in a complicated way:

 var S = WinJS.Utilities.Scheduler; getCriticalDataAsync().done(function (results1) { S.schedule(function () { var secondaryPages = processCriticalData(results1); S.schedule(function () { getSecondaryDataAsync(secondaryPages).done(function (results2) { var itemsToCache = processSecondaryData(results2); S.schedule(function () { getBackgroundCacheDataAsync(itemsToCache).done(function (results3) { populateCache(results3); }); }, S.Priority.idle); }); }, S.Priority.normal); }, S.Priority.high); }); 


In our opinion, going to the dentist is more fun than writing such code! For simplicity, you could wrap the process of setting a new priority inside another pending result, which in turn you would insert into the chain. The best way to do this is to dynamically generate a handler for the completed event (completed), which takes the results from the previous step in the chain, schedules the execution with the right priority and returns the Promise object with the same result:

 function schedulePromise(priority) { //  –  . return function completedHandler (results) { //     ,    //   ,   ... return new WinJS.Promise(function initializer (completeDispatcher) { //       . WinJS.Utilities.Scheduler.schedule(function () { completeDispatcher(results); }, priority); }); } } 


Fortunately, we don’t have to write such code on our own. WinJS.Utilities.Scheduler already contains five ready-made completion handlers like the one above, which also automatically cancel the task when an error occurs. They are called accordingly: schedulePromiseHigh , schedulePromiseAboveNormal , schedulePromiseNormal , schedulePromiseBelowNormal and schedulePromiseIdle .

Having finished completion handlers, you just have to insert the correct method name in the chain of pending results where you want to change the priority, as shown below:

 var S = WinJS.Utilities.Scheduler; getCriticalDataAsync().then(S.schedulePromiseHigh).then(function (results1) { var secondaryPages = processCriticalData(results1); return getSecondaryDataAsync(secondaryPages); }).then(S.schedulePromise.normal).then(function (results2) { var itemsToCache = processSecondaryData(results2); return getBackgroundCacheDataAsync(itemsToCache); }).then(S.schedulePromiseIdle).done(function (results3) { populateCache(results3); }); 


For clarity: the use of these functions is only that you insert their names in the chain of deferred calls. You do not need to call them directly as separate functions (this may not be completely clear in the current documentation).

Long living tasks


All the examples of jobs that we have considered at the moment are short-lived in the sense that we plan some work function with a certain priority and it just performs its work at the time of the call. However, some tasks may require much more time to complete. In this case, you hardly want to block work with a higher priority in your UI thread.

To help in such situations, the scheduler has a built-in interval timer for sorting tasks that are planned with priorities aboveNormal or lower - so that the task can check whether it should not be “cooperatively” supplanted and rescheduled for the next block of work. Here we need to clarify the word “cooperatively”: nothing forces the task to postpone its execution, but since this all affects the performance of the UI of your application, and indeed the whole application, if you do not handle these situations properly, you will harm yourself !

The implementation mechanism for such a maneuver is carried out through the jobInfo object, which is passed as an argument to the work function itself. Let's first take a look at what is available for a function in its scope, it is easiest to understand from a few comments inside the base code:

 var job = WinJS.Utilities.Scheduler.schedule(function worker(jobInfo) { //jobInfo.job –   ,    . //Scheduler.currentPriority –    . //this –  ,  . }, S.Priority.idle, this); 


The members of the jobInfo object are defined in the Scheduler.IJobInfo interface:

Properties



Methods



Scenario 4 of the HTML Scheduler example shows how to work with this. When you click the “Execute a Yielding Task” button, a function called idle- priority worker is added to the scheduler, which simply idle cycles until you press the “Complete Yielding Task” button, which sets the taskCompleted flag to true (js /yieldingscenario.js, with intervals of 2s, replaced by 200ms):

 S.schedule(function worker(jobInfo) { while (!taskCompleted) { if (jobInfo.shouldYield) { //  ,      window.output("Yielding and putting idle job back on scheduler."); jobInfo.setWork(worker); break; } else { window.output("Running idle yielding job..."); var start = performance.now(); while (performance.now() < (start + 200)) { //   ; } } } if (taskCompleted) { window.output("Completed yielding task."); taskCompleted = false; } }, S.Priority.idle); 


If the task is active, it does “work” for 200 ms and then checks whether the shouldYield property is set to true . If so, the function calls the setWork method to reschedule itself (or another function, if necessary). Such a transition can be provoked while the long task is working by clicking the “Add Higher Priority Tasks to Queue” button in the example. You will see how these tasks (with high priority) will work until the next call to the work function. In addition, you can click anywhere else in the interface to make sure that this idle task does not block the UI thread.

Notice that the working function checks shouldYield first to vytisnytsya immediately, if necessary. However, it is quite normal to do a little work first, and then make a check. Once again, this is a matter of cooperation within your own code, so your locks are on your own conscience.

As for setPromise , this is a slightly more subtle idea. Call the setPromise scheduler to wait until the pending result appears before rescheduling the task. Moreover, the following work function for the task is provided directly through the value of the pending result. (As such, the IJobInfo.setPromise method does not manage asynchronous operations, as do other setPromise methods inside WinJS, which in turn are tied to delay mechanisms in WinRT. If you called IJobInfo.setPromise with a delayed result from some random asynchronous API, the scheduler will try to use the execution value of this operation, - it can be anything, in the form of a function, which can lead to an exception.)

In general, if setWork says "let's re-plan with this work function", then setPromise says "wait with re-planning, wait until I give you the necessary function sometime later." This is usually convenient for creating a work queue made up of many jobs with an accompanying task for processing this queue. To illustrate, imagine that you have the following code:

 var workQueue = []; function addToQueue(worker) { workQueue.push(worker); } S.schedule(function processQueue(jobInfo) { while (work.length) { if (jobInfo.shouldYield) { jobInfo.setWork(processQueue); return; } work.shift()(); //    FIFO-  . } }}, S.Priority.belowNormal); 


Assuming that there are some jobs in the queue at the time of the first call to the scheduler, the processQueue task will “cooperatively” release this queue. And, if new jobs are added to the queue at run time, the processQueue will continue to be rescheduled for further execution.

The problem, however, is that the processQueue function ends when the queue is empty, which means that whatever work you add to the queue will not be processed. To fix this, you could force processQueue to periodically call setWork again and again, even if the queue is empty, but it would be a waste of resources. Instead, you can set setPromise to make the scheduler wait until a new job appears in the queue. Here's how it will work:

 var workQueue = []; var haveWork = function () { }; //   function addToQueue(worker) { workQueue.push(worker); haveWork(); } S.schedule(function processQueue(jobInfo) { while (work.length) { if (jobInfo.shouldYield) { jobInfo.setWork(processQueue); return; } work.shift()(); //     FIFO-  . } //   ,  ,        . // ,   setWork  - ,   , //  ,  addToQueue   ,    ,  // haveWork ,     . jobInfo.setPromise(new WinJS.Promise(function (completeDispatcher) { haveWork = function () { completeDispatcher(processQueue) }; })) }); 


Within the framework of this code, suppose we filled out workQueue with some amount of work and then make a call to schedule . At this point and the next, until the queue becomes empty, we are inside the while loop of the processQueue function. Any call to the empty haveWork function requires virtually no additional operations.

If the queue becomes empty, we exit the while loop , but do not want to exit the processQueue . Instead, we want to tell the planner to wait until a new job is added to the queue. That is why we have a stub for the haveWork function, which we will be able to replace with another function that completes the pending result in the processQueue and thus causes the rescheduling of the working function itself.

Note that an alternative way to achieve the same goal would be to use the following assignment of the haveWork function:

 haveWork = completeDispatcher.bind(null, processQueue); 


This gives the same result as an anonymous function, but without creating a closure.

Conclusion


The WinJS Scheduler API allows applications to schedule different tasks within a UI stream with relative priorities, including different tasks within a single chain of pending results. At the same time, the application automatically coordinates its tasks with the tasks that WinJS performs for the same application, for example, optimizing data binding and drawing controls. With careful use of the available priorities, the application can take notable steps toward improving overall performance and user experience.

- Kraig Brockschmidt
Program Manager, Windows Ecosystem and Frameworks Team
Author of “ Programming Windows Store Apps with HTML, CSS, and JavaScript, ” Second Edition

Links


Quick guide to working with the scheduler
HTML Scheduler Example
Download Visual Studio 2013

Source: https://habr.com/ru/post/216623/


All Articles