Using Timers Effectively in HDi
The use of timers in HDi applications is very common, and there are a few things you should keep in mind when using them to avoid performance issues when playing back your title.
I will categorise these as:
- The autoReset problem
- The closure problem
- The re-use problem
- The general-abuse problem
The AutoReset Problem
This problem stems from the fact that, by default, timers created in HDi are repeating timers, meaning that they will fire over and over and over again until you tell them to stop. Sometimes this is what the content author wants, but more often than not what they really need is a single notification after the timeout period has elapsed (what we call a "one-shot" timer). It's essentially the difference between setTimeout and setInterval in the browser DOM.
If you want a one-shot timer, the following pattern should be avoided:
// Create a timer; note it auto-repeats by default
var t = createTimer(TIME, CLOCK, callback);
t.enabled = true;
function callback()
{
// do stuff...
// Now disable the timer; sometimes people forget this step!
t.enabled = false;
}
Instead, consider the more straightforward (and less bug-prone) version:
// Create a timer and make sure it only fires once
var t = createTimer(TIME, CLOCK, callback);
t.autoReset = false;
t.enabled = true;
function callback()
{
// do stuff...
}
Sometimes, the problem is compounded because the author assumes the timer is a one-shot timer, and that in order to get repeating semantics they need to re-create the timer every time. That results in code like this:
// Create a timer; note it auto-repeats by default
var t = createTimer(TIME, CLOCK, callback);
t.enabled = true;
function callback()
{
// do stuff...
// Now re-create the timer to ensure we get called again
t = createTimer(TIME, CLOCK, callback);
t.enabled = true;
}
Now what happens is that every time the timer fires, a new timer is created to fire on the same schedule! So after the first callback, there are two timers. After the second callback, there are four timers. After the third callback, there are eight timers, and so on. Eventually the old timers will get garbage collected by the script engine (and stop firing), but this could take a long time to happen depending on what else your script is doing. This is clearly a waste of resources and should be avoided; if you need a repeating timer, leave autoReset at its default value and don't re-create the timer later on (although I prefer explicitly setting autoReset to true to convey the intent that it really should repeat). Otherwise, set autoReset to false at the time you create the timer, and you will be happy.
The Closure Problem
In ECMAScript, a "closure" is a geeky name for a nested function. The reason we call them closures is to do with the way they affect the lifetime of other objects. I don't want to go into great detail now, but essentially a nested function maintains a reference to all the local variables of its containing function, thereby "closing them off" from being released by the garbage collector. Normally this isn't a problem for ECMAScript, because sooner or later the closure will die and thus the other objects it has been keeping references to will also die.
For example:
function MakeAClosure()
{
// Gobble up some memory
var bigObj = CreateECMAScriptObjectThatUsesLotsOfMemory();
// Deliberately set up a circular reference
bigObj.circularReference = Closure;
// Return the closure
return Closure;
// Define the nested function (closure)
function Closure()
{
// doesn't really matter what Closure does;
// it still keeps a referenceto bigObj
}
}
function foo()
{
// Get the closure
var closure = MakeAClosure();
// maybe use it, maybe not...
}
// Call foo
foo();
In this example, we have an outer function (MakeAClosure), a nested function (Closure), another global function (foo), and some global code that calls foo. Here's what happens during execution:
- foo gets called
- foo calls MakeAClosure
- MakeAClosure creates a large object that uses up some memory, and stashes it in bigObj
- The nested function Closure gets a reference to bigObj for reasons that I won't get in to here (check sections 10.2.3, 13, and 13.2 of ECMA-262 for technical details... or I can post more later :-))
- MakeAClosure explicitly makes bigObj hold a reference to Closure so they both refer to each other – a classic circular reference problem
- MakeAClosure returns the Closure
Function object back to Foo- Note that this includes the reference to bigObj
- foo stores the Function object inside the closure variable
- foo exits
- Sooner or later, the garbage collector runs and sees that bigObj is used by closure, and closure is used by bigObj, but there is no way to access either of them (they are not "reachable") so it can safely kill both of them
Here everything is fine and dandy. But if bigObj where a non-ECMAScript object (eg, a Host object), the memory would never be reclaimed in Step 8 because the garbage collector cannot prove that bigObj is unreachable – maybe some other part of the system still relies on it – and so it has to assume that closure is still reachable, too. And since closure holds on to bigObj, the native object is never going to release itself, and the memory is leaked.
Perhaps it's clearer with an example.... using Timers, of course :-)
// Perform two animations, one after the other
function DoTwoPartAnimation()
{
// Perform some animation over 1 second
document.foo.animateProperty("y", "0px;100px", 1);
// Setup a callback to do the other animation after the first is done
var t = createTimer("00:00:01:00", 1, DoPartTwo);
t.autoReset = false;
t.enabled = true;
function DoPartTwo()
{
// Perform second part of animation
document.bar.animateProperty("x", "0px;100px", 1);
}
}
This simple example illustrates a common pattern in HDi – chaining together two or more script-based animations by having a timer fire after the first animation is complete (this is unnecessary with markup animations, but they're not always practical).
In this case, DoTwoPartAnimation creates a local Timer variable, t, and gives it a reference to the nested function DoPartTwo as the callback function. As we learnt above, DoPartTwo maintains a reference to all of DoTwoPartAnimation's variables, including the timer t. So even after the timer has fired, and DoPartTwo has executed, we will leak the timer object (and the Function object for DoPartTwo) because the circular reference can't be broken.
The good news is that there are at least three ways to solve this problem:
- Avoid closures altogether
- Explicitly break the cycle
- Avoid making the timer a local variable
The first is the simplest, and in the case above we can solve the problem by simply making DoPartTwo another global function instead of a nested function. Then although the timer t has a reference to DoPartTwo, DoPartTwo does not have a reference back to t and so the timer will be garbage collected sooner or later (which in turn will release DoPartTwo). The problem here is that often you need to use a nested function in order to correctly perform the callback operation – for example, the callback needs to know some of the parameters that were originally passed to the containing function in order to complete its task.
In that case, another simple solution is to explicitly break the cycle by manually releasing the reference to the timer. For example, we could change the DoPartTwo function slightly to look like this:
function DoPartTwo()
{
document.bar.animateProperty("x", "0px;100px", 1);
// Explicitly break the circular reference
t = null;
}
Now, as with the 1st work-around, the script engine can eventually garbage collect t, which in turn will release DoPartTwo. Nevertheless, the problem with this solution – as with all solutions that require that the programmer religiously follow some strict rules all the time – is that you might forget to add this line to your callback, or you might have a return statement near the top of the function, or you might throw an exception, or... there are various ways your program might miss this opportunity to break the cycle.
So that leaves us with... my preferred approach, which is simply to make your own helper function – much like the web's setTimeout – that will create the timer for you. Here's a simple example:
// Call callback after timecode has elapsed
function SetTimeout(timecode, callback)
{
var t = createTimer(timecode, 1, callback);
t.autoReset = false;
t.enabled = true;
}
Now you can simply replace all the pesky createTimer / autoReset / enabled code with a single call to SetTimeout, and avoid all the closure issues, too!
// Perform two animations, one after the other
function DoTwoPartAnimation()
{
// Perform some animation over 1 second
document.foo.animateProperty("y", "0px;100px", 1);
// Setup a callback to do the other animation after 1s
SetTimeout("00:00:01:00", DoPartTwo);
function DoPartTwo()
{
// Perform second part of animation
document.bar.animateProperty("x", "0px;100px", 1);
}
}
The closure issues are avoided because DoPartTwo has zero knowledge of the Timer object t, which is a local variable of the SetTimeout function. Although the timer has a reference to DoPartTwo, the opposite is not true and so there is no circular reference.
Note about HTTPClient
Note that this "closure problem" also exists with HTTPClient objects and the onStateChange function. The same solutions exist for breaking the circular reference though – avoid closures, null out the object once it is finished (eg, STATE_COMPLETED, STATE_ERROR, or STATE_ABORT), or use a helper to create the object.
In general it doesn't affect other callback-based APIs like FileIO.openTextStream or XMLParser.parse because these are global objects, not local instances created by the enclosing function. The API will release the callback function as soon as it has been used (so it won't leak), and it doesn't really matter how many references there are to global objects; they will always exist.
The Re-Use Problem
Timer re-use is something that can be done correctly, but is often done incorrectly. Sometimes this is done by mistake (eg, forgetting to put var in front of a variable declaration, making it global), but other times it is deliberately done and leads to brittle code.
Here's a canonical example:
// Create timer; assume it is enabled
var timer = createTimer(TIME, CLOCK, CALLBACK);
function EventHandler()
{
// Clobber global timer; assume it is enabled
timer = createTimer(TIME2, CLOCK2, CALLBACK2);
}
function CancelTheTimer()
{
// Which timer are you cancelling?
timer.enabled = false;
}
The code is pretty much self explanatory:
- A global timer is created
- An event handler clobbers the first timer and creates a new one
- A cancel function disables the timer
The problem is that depending on the order in which EventHandler and CancelTheTimer are called, different things may happen. If CancelTheTimer is called before EventHandler, it will cancel the global timer, but if the order is reversed then the global timer will fire and the event-handler timer will be disabled instead!
Even if you think you know the order in which events will occur, they may not always fire in exactly the same order – especially if they rely on user input or some asynchronous processes such as the progression of the title timeline. Better to always assume that the worst will happen, and program accordingly.
The simple solution here is to always use a unique name for your timers, or ensure they are local variables that won't conflict with other timers. (I would say to use the SetTimeout function from above, but that won't allow you to cancel the timer).
The General-Abuse Problem
The "general abuse" category basically covers any use of a timer that is unnecessary given the feature set of HDi. Sometimes authors abuse timers because they are not aware of HDi's built-in features (in which case it is generally easy to change from an abusive timer to use the right feature), or sometimes it is because the application's design has been built around timers rather than some more elegant solution (in which case it can be much harder to re-work the code to use fewer timers).
Two prime examples of the former include:
- Performing many set operations on a repeating 1-frame timer, rather than using the <animate> element in markup or the animateProperty API
- Constantly polling currentTitle.elapsedTime to check for a specific timecode, rather than using an <event> in markup or a PauseAt in the playlist
Examples of the latter are harder to come by, but (with no intention of bashing the other format) we have seen authors trying to replicate the behaviour of BD-J applications on HD DVD by an over-zealous use of timers, since they don't necessarily realise that the HDi system will do a lot of the heavy lifting for them.
Final Thought
An astute observer might ask the question "what happens if a timer is garbage-collected before it fires?" Good question. It is entirely possible that the local variable t used in these examples will be collected before the timer fires, especially if the timeout is quite long (say, several minutes). The answer is that the HD DVD specification requires timers to fire at least once, even if the script engine has released all references to them.
Comments
Anonymous
October 24, 2007
One of the key programming concepts in HDi is the use of asynchronous processing with callback functions.Anonymous
December 07, 2007
If you know what an error code means, it can be immensely helpful in diagnosing the problem with yourAnonymous
December 19, 2007
If you know what an error code means, it can be immensely helpful in diagnosing the problem with your