Difference between revisions of "DelayCall (window::)"
(→delayCall: released) |
|||
Line 3: | Line 3: | ||
== Motivating examples == | == Motivating examples == | ||
When typing text into text fields you often get a list of suggestions which you can pick from. Typical examples is the browsers URL field and search engines search fields. But if you type fast | When typing text into text fields you often get a list of suggestions which you can pick from. Typical examples is the browsers URL field and search engines search fields. But if you type fast then it may not be desirable to provide suggestions at each typed character. Both because it may be too resource demanding to calculate the suggestions, but also because the list may change/flicker in a distracting way. | ||
Instead of updating the list at each keystroke, we will delay the update until the user hesitates the typing a little. I.e. we delay the update until a certain amount of time has elapsed since the last keystroke. | Instead of updating the list at each keystroke, we will delay the update until the user hesitates the typing a little. I.e. we delay the update until a certain amount of time has elapsed since the last keystroke. |
Revision as of 10:58, 20 July 2015
Events may occur more frequent than it is desirable to handle them. window::delayCall provides a way to deal with this problem.
Motivating examples
When typing text into text fields you often get a list of suggestions which you can pick from. Typical examples is the browsers URL field and search engines search fields. But if you type fast then it may not be desirable to provide suggestions at each typed character. Both because it may be too resource demanding to calculate the suggestions, but also because the list may change/flicker in a distracting way.
Instead of updating the list at each keystroke, we will delay the update until the user hesitates the typing a little. I.e. we delay the update until a certain amount of time has elapsed since the last keystroke.
Here are two other examples that can utilize the same solution to avoid flickering and/or performance problems:
- It may be better to delay the actual scroll until there is a pause/hesitation in scroll events.
- A program that receives data updates from an external source, may delay the update of the GUI until there is a pause/hesitation in the external updates.
Drawback: We cannot detect a hesitation/pause until it has exceeded the desired time. So the solution also introduces a delay, which may not always be desirable: The delay time may have to be a compromise between not reacting too often and not pausing too long.
This video shows three control. The middle one invalidate and repaint on each size event, the other two postpones the repainting to a pause in the size events.
delayCall
delayCall can be used to implement such "react on hesitation" solutions.
At first consider this simple example code assumed to be in a control:
constants delay = 100. % milliseconds clauses addData(NewData) :- doAddData(NewData), delayCall(delay, invalidate). clauses removeData(ToRemove) :- doRemoveData(ToRemove), delayCall(delay, invalidate).
The control has predicates for adding and removing data from it. When the data has been updated the control is invalidated so that it will be redrawn using the new data. But to prevent a lot of rapid updates of the control we will postpone the invalidation until the data has been stable for 100ms. This is exactly what the calls to delayCall does: They schedule a call to invalidate to occur after delay (= 100ms) has passed, but they will also cancel a previous scheduling, if such one exists. So in effect the an invalidate will happen when there has been a 100ms hesitation in the data updates.
Now that the basic behavior is in place let us look at the details and some frequent pitfalls and some possible solutions to these.
In its simple form delayCall takes two arguments:
predicates delayCall : (positive Delay, runnable Predicate).
A call to delayCall will:
- Either invoke Predicate when Delay milliseconds has passed.
- Or be overidden by a new invocation of delayCall with the same Predicate in which case the Delay is restarted with the new value.
- Or be lost because the window is destroyed before the Delay has expired.
Notice that that delayCall is per window and per Predicate.
The per window part can be used to avoid executing delayed predicate that is no longer relevant because the window it concerns no longer exist.
The per Predicate part is important because you may want to delayCall for many unrelated things, and such things should not cancel each other. It is very common to use the application window for "global" actions, which are not related to a specific window. For example recalculating a global state when there is a pause in atomic changes.
In the example the predicate is invalidate in both calls, so no matter if we call addData or removeData the invalidate will be postponed further. But calling delayCall(17, somethingElse) will not cancel or postpone the invocation of invalidate.
Just like in this example, the idenitity of predicates is very often exactly what you want delayCall to distinguish on. There is however a pitfall with anonymous predicates, illustrated by this example:
clauses setColor(NewColor) :- delayCall(10, { :- doSetColor(NewColor) }).
The intension of the code is to postpone setting the color until it doesn't change frequently. The code uses an anonymous predicate to capture the new color. Such a use of an anonymous predicate is normally highly recommendable, but here it cause a problem. The problem is that each time you call setColor a new anonymous predicate is created, to capture the NewColor. So each time we call setColor we call delayCall with a new unique predicate. And since it is different from the privous one it will not influence on the previously delay calls. All of them will be executed each with 10 milisecond dealy.
We can solve the problem like this:
facts newColor : color := color_white. % dummy assignment that is not used clauses setColor(NewColor) :- newColor := NewColor, delayCall(10, setNewColor). clauses setNewColor() :- doSetColor(newColor).
Calling this version of setColor will update the fact variable newColor, so this fact variable will always contain the most recent value of NewColor. The call to delayCall now refernce the same predicate (i.e. setNewColor) each time. So in this code each call to setColor will cancel the previous scheduled invocation of setNewColor and schedule another instead. So the invocation of setNewColor will be postponed to a pause in the setColor calls.
Instead of storing the new color in an anomyous predicate we store it in a fact, because then we can delay a call to a named predicate instead of an anonymous predicate.
The overall principle here is to maintain a shaddow (sub-)state which is then merged into the real state in a pause.
The solution above to the anonymous predicate problem is rather complex/elaborate, we both need to introduce extra fact(s) and an extra named predicate.
Worst of all is that the extra facts does not carry real information about the object in which they appear, they only store information that has to be transferred from one point in an algorithm to another point in that algorithm. I.e. they are local to the algorithm rather than global in the object.
Such facts are highly undesirable and very often you will use varM's or anonymous predicates to eliminate them. In this case a varM will not help, because it will also have to be made global in the object for exactly the same reasons as for NewColor.
So it would be very nice to have a way to deal with anonymous predicates in delayCall. The problem was that the identity of anonymous predicates is not very suitable for determine whether to prolong a delay. To solve this problem (and others) we can use another version of delayCall which uses an explicit provided identity value to determine whether to prolong an existing delay or to create a new one.
predicates delayCall : (positive Delay, runnable Predicate, IdType Id).
This version takes an extra Id argumment of any type you like. If you provide the same value in two calls the latter will replace the first (if has not already fired of course), if you provide different values the calls are considered unrelated.
It is the programmers responsibility to ensure that the chosen Id:
- cancel/replace/prolong the intended calls, and
- does not interfere with other usages of delayCall
Many different strategies may be used for the Id's in the sequel we will discuss some.
In the first example above we had two calls belonging to the same delay group. In this example we (indirectly) used the predicates themselves as id. As described this makes good sense in many situations, but requires named predicate.
For anonymous predicates and in cases where you need more detailed control you will have to use another id. The key issue is to use something that "collides" the intendet calls, but don't collide with other calls.
Very often like in the setColor example the calls you want to collide comes from a specific place in the code.
In such cases it is very simple to use the programPoint as Id. The is easily obtained using the builtin predicate programPoint/0:
clauses setColor(NewColor) :- delayCall(10, { :- doSetColor(NewColor) }, programPoint()).
With this Id calls made from that source line will cancel each but it will not have any impact on calls made elsewhere (regardless of their strategy).
If you need to put calls from several lines, or calls to different named predicates in the same group you should consider defining a group id. Also here you can use a programPoint:
constants myDelayCallId : programPoint = programPoint(). ..., delayCall(10, { :- doSomeThing(...) }, myDelayCallId), ... ..., delayCall(20, doSomeThingElse, myDelayCallId), ...
If you for some reason need to be certain that the Id doesn't change when the source code change (for example if a DLL needs to know the same Id as another DLL). You could generate and use a GUID instead. GUID's are 128bit numbers that Windows can generate such that there is an extremely little probability for (world wide) collision. The Visual Prolog IDE can generate GUID's for you, just select Insert -> New GUID in the menu:
constants myDelayCallId : nativeGuid = % {3363A750-0320-4B2A-BE3A-27F66BD7B875} nativeGuid(0x3363A750, 0x0320, 0x4B2A, 0xBE, 0x3A, 0x27, 0xF6, 0x6B, 0xD7, 0xB8, 0x75). ..., delayCall(10, { :- doSomeThing(...) }, myDelayCallId), ... ..., delayCall(20, doSomeThingElse, myDelayCallId), ...
Finally, let us consider using "semantic" Id's. Consider the following code:
clauses onFileChanged(Filename) :- doOnFileChanged(Filename).
A program is monitoring file changes on a disk, when a (relevant) file changes the program has to do something. In the code above onFileChanged is called each time a file changes, and doOnFileChanged represents the work we want to do when the file changes.
However file changes often comes rapidly after each other and it is better to wait with the update when the updates have settled down. So it would be natural to apply a delayCall in the process.
But none of the solutions above seem to cover this example. Because the delayCall has to be per file, rather than per function or per programPoint. A simple solution is keep track of the file in an anonymous predicate and use the filname as Id:
clauses onFileChanged(Filename) :- delayCall(100, { :- doOnFileChanged(Filename) }, Filename).
Now only the last onFileChange for a certain file is remembered and handled delayed.
In a large context we may fear that filenames collide with some other scheme use in some other place. One solution to this is to combine the semantic is with a programPoint, for example like this:
clauses onFileChanged(Filename) :- delayCall(100, { :- doOnFileChanged(Filename) }, tuple(Filename, programPoint())).
Another solution is to create a unique delay id domain:
domains delayId = fileChangeId(string Filename). clauses onFileChanged(Filename) :- delayCall(100, { :- doOnFileChanged(Filename) }, fileChangeId(Filename)).
The advandtage of the programPoint solution is that it does not require any extra definitions, it is simply an "in line" solution. The advantage of the solution with the delayId domain is that it can be used in several places in the code.