Initial attempt to improve timers.

Started by ., Dec 26, 2016, 10:17 PM

Previous topic - Next topic

.

Spent some time tonight trying to finally fix the timers implementation. At the moment, I only have linux binaries. If anyone has Visual Studio installed and can provide Windows binaries that would be awesome. The repository can be found here. Remember to use the `newapi` branch when retrieving the source.

Features:
  • Timers limit is 1024. This can be increased to much more at compile time. The implementation is very fast and there wouldn't be any impact on performance.
  • Any type of value can be passed as a parameter to the timer. Literally anything you want. Which means you'll have to be careful because it keeps a strong reference to the given parameters.
  • Any type of function can be passed. This does not take function names but rather function objects. Meaning you pass the function directly. Either a regular function, native, or lambda.
  • Supports custom environments. You can force the function to run in different environments. Meaning you can create timers on methods of classes and so on.
  • You can associate custom objects or custom names/tags with each timer instance. Meaning you can associate certain information with the timer so you can later use it.

Unfortunately, I could not maintain backwards compatibility. However, you can make several workarounds in scripts to make them look like the old timers. Although I'd suggest you just update the code to use the new approach.



To create a timer, use the function `MakeTimer`. It uses a different name to not collide with the old one `NewTimer`. The syntax of the function is as following:
MakeTimer(environment, callback, interval, iterations, ...);
  • environment: This can be a table, class or class instance. And the function will run as if it was called on it. You can also use null and it defaults to the root table.
  • callback: This can be a function, lambda or a native function. NOTE: You pass the function by value not by name. Which means you can also pass anonymous functions.
  • interval: The number of milliseconds to wait between each call. Meaning how much time to wait before calling the function.
  • iterations: The number of times to be triggered before terminating itself. If you pass 1 then it calls the function once and then terminates itself. If you pass 0, it calls the function indefinitely.
  • After these parameters you can pas up to 14 values which will be forwarded to the function which you specified. Anything you add here will be forwarded in the same order.

The function returns the timer instance. The function also throws an error if something goes wrong. So you should use a try/catch block if you want your code to continue after that.

Example:
MakeTimer(this, function(separator, list) {
    foreach(index, value in list)
    {
        print(index + separator + value);
    }
}, 1000, 2, " - ", [32, 82, 21, 97]);

Outputs twice every 1000 milliseconds (one second):
[SCRIPT]  0 - 32
[SCRIPT]  1 - 82
[SCRIPT]  2 - 21
[SCRIPT]  3 - 97
[SCRIPT]  0 - 32
[SCRIPT]  1 - 82
[SCRIPT]  2 - 21
[SCRIPT]  3 - 97



To find a timer with a certain tag/name use `FindTimerByTag`. It takes the same parameters as the `format` function. Then it generates a string and looks for a timer with a tag/name similar to that string. Basically, you can pass any type of value here and it'll be converted to a string internally:
FindTimerByTag(value, ...);
The function returns the timer instance which matched the resulted string or throws an error if it couldn't find it.

Example:
MakeTimer(this, print, 1000, 1, "Hello!").SetTag("PrintTest");
FindTimerByTag("PrintTest").Terminate();

This shouldn't output "Hello!" after a second because I searched the timer and terminated it before it could finish.



Here is a list of the the member functions of the class:
string GetTag();
[Timer] SetTag(value, ...);
object GetEnv();
[Timer] SetEnv([table,class,instance] environment);
function GetFunc();
[Timer] SetFunc([function] callback);
object GetData();
[Timer] SetData([object] user_data);
integer GetInterval();
[Timer] SetInterval([integer] interval);
integer GetIterations();
[Timer] SetIterations([integer] iterations);
bool IsSuspended();
bool GetSuspended();
[Timer] SetSuspended([bool] toggle);
integer GetArgCount();
object GetArgument([integer] index);
null Terminate();

All of these methods throw errors if something goes wrong so I'd suggest you pay close attention. This doesn't just die silently.



The `Terminate` method terminates the timer regardless of how many iterations or time it has left. It only invalidates the timer and releases all references to any stored object such as the function itself or the environment. The timer instance is not cleaned until all references to it are released. Meaning it's not stored anywhere else in the script.

Which means that if you continue to use a timer instance after it was terminated you'll receive some errors thrown at you. Nothing bad, just letting you know that the timer you're attempting to use does not exist anymore.



The `GetTag` and `SetTag` can be used to retrieve or modify the associated tag. The tag is a string which acts like a name so you can search the tag by it. The `SetTag` function has the same syntax as the `format` function. Which means it has built in formatting support. The `SetTag`returns back the timer instance so that operations can be chained.

MakeTimer(this, print, 1000, 1, "Hello!").SetTag("PrintTest");
FindTimerByTag("PrintTest").SetTag("abc%s", "xyz");



The `GetEnv` and `SetEnv` allow you to retrieve or modify the function environment at any time after you've created the timer. The `SetEnv`returns back the timer instance so that operations can be chained.



The `GetFunc` and `SetFunc` allow you to retrieve or modify the callback function at any time after you've created the timer. The `SetFunc`returns back the timer instance so that operations can be chained.



The `GetData` and `SetData` allows you to store anything you want alongside the timer instance. Meaning you can associate certain values with a timer so you can later retrieve it, even from within the callback function if you want. The `SetData`returns back the timer instance so that operations can be chained.

Example:
MakeTimer(this, function() {
    local list = FindTimerByTag("ABC").GetData();
    foreach (index, value in list)
    {
        print(index + " - " + value);
    }
}, 1000, 1).SetTag("ABC").SetData([32, 54, 22]);

Outputs:
[SCRIPT]  0 - 32
[SCRIPT]  1 - 54
[SCRIPT]  2 - 22



The `GetInterval` and `SetInterval` allows you retrieve or modify the timer interval at any time after you've created the timer. The `SetInterval`returns back the timer instance so that operations can be chained.



The `GetIterations` and `SetIterations` allows you retrieve or modify the remaining iterations at any time after you've created the timer. The `SetIterations`returns back the timer instance so that operations can be chained.



The `GetSuspended` and `SetSuspended` allows you see if blocked or block the interval from forwarding calls at any time after you've created the timer without terminating the timer instance. The `IsSuspended` is an alias for `GetSuspended`. Setting this to true means that the function will not receive any calls even if the interval time elapsed. The `SetSuspended`returns back the timer instance so that operations can be chained.



The `GetArgCount` allows you to see how many arguments the timer will forward to the callback function.



The `GetArgument` allows you to retrieve arguments the timer will forward to the callback function.

Example:
local t = MakeTimer(this, function(v, a) {
     print(v+a);
}, 1000, 1, "Hello", "World!");
print(t.GetArgument(1));

Outputs:
[SCRIPT]  World!
[SCRIPT]  HelloWorld!

Indexing starts from 0, so 1 means the second argument.
.

.

#1
Windows binaries are now available. And here are also some workarounds which makes it possible to make the new timers work as the old ones.

NOTE: Make sure you have Visual C++ Redistributable for Visual Studio 2015 installed on your computer.

The first approach implies that the function which you intend to call already exists in the root table and can be retrieved at the time of making the timer:
/* Wrapper function to allow the new timer implementation to be used with the same syntax as the old one.
 * NOTE: THIS ASSUMES THAT THE FUNCTION ALREADY EXISTS IN THE ROOT TABLE!!!
*/
function NewTimer(name, ...)
{
    vargv.insert(0, this);
    // Use the root table as the environment
    vargv.insert(1, getroottable());
    // Grab the function object from the root table
    vargv.insert(2, getroottable().rawget(name));
    // Forward the call to the actual implementation
    return MakeTimer.acall(vargv);
}

Example:
NewTimer("print", 1000, 1, "Hello!");
The second approach can be a bit slower than the first one because it involves retrieving the function on each call. Kinda like the original implementation does. But it works even if the function which you intend to call does not exist in the root table at the moment you create the timer. This should provide the most compatibility at the cost of a little performance.
/* Wrapper function to allow the new timer implementation to be used with the same syntax as the old one.
 * NOTE: THIS IMPLEMENTATION WORKS EVEN IF THE FUNCTION DOES NOT EXIST ALREADY!
*/
function NewTimer(name, ...)
{
    vargv.insert(0, this);
    // Use the root table as the environment
    vargv.insert(1, getroottable());
    // Inject a forwarding function object
    vargv.insert(2, function(...) {
        // Retrieve the callback from the root table
        local fn = getroottable().rawget(vargv[0]);
        // Replace the callback name with an environment
        vargv[0] = this;
        // Forward the call to the actual callback
        fn.acall(vargv);
    });
    // Inject the name before the arguments
    vargv.insert(5, name);
    // Forward the call to the actual implementation
    return MakeTimer.acall(vargv);
}

Example:
NewTimer("print", 1000, 1, "Hello!");
Either way, both approaches are still faster and safer than the original timers implementation. And now the plugin should be fully compatible with older scripts as well. With the added bonus that you can pass any value to the callback function.

However, if you used members of the CTimer class then those won't be the same. So you'll have to update those either way.



If you want to still use the `Delete()` method instead of `Terminate()` then add the following in your script and make sure it's execute before you create any timers:
CTimer.newmember("Delete", CTimer.Terminate);
Will create an alias for the `Terminate()` method and allow you to use it as the old implementation.
.

DizzasTeR

Oh shit how didn't I notice this before

This is totally MTA:SA like! Cheers man! \o/