JS: Reusing regular global variables through multiple executions

I've mentioned this in passing a long time ago, but I thought I'd put this here.
Not sure if it's intended or not, but this seems quite useful to avoid reinitializing
everything for each item in a column handler and similar.

This demonstrates reusing global variables through multiple executions.
when those are within a few seconds of eachother.

Copy this script to the Script Addins folder, or drag and drop it on
the scripts page. When installed, rightclick the column header in a
tab and select Columns->Script->TestColumn.
Notice it has a different number for each entry. If you refresh (F5)
repeatedly, notice how the numbers increase each time.

If you open the script log, then you can also see that the same
is true for the vector attached to vgTestRegexes. In other words
it displays "Reading from config" once, and the rest displays
"Use previously loaded".

// -------------------------------------------------
var vgTestRegexes;
var giCounter;

function OnInit(initData){
  initData.name = "Test";
  initData.desc = "Test";
  initData.copyright = "myarmor";
  initData.min_version="11.8";
  initData.version = "0.1";
  initData.default_enable = true;
  initData.config.TestRegexes=DOpus.Create.Vector();
}

function OnAddColumns(addColData){
  var col;
  col = addColData.AddColumn();
  col.name = "TestCol";
  col.method = "OnTestCol";
  col.label = "TestColumn";
  col.autogroup = false;
  col.namerefresh=true;
  col.justify = "left";
  col.type = "number";
}

function OnScriptConfigChange(configChangeData){
  Script.RefreshColumn('TestCol');
}

// Handler for the TestCol column
function OnTestCol(scriptColData){
  if (scriptColData.col!="TestCol"){
    return;
  }
  // Check if we have a used variable.
  if (typeof vgTestRegexes==='undefined'){
  // if not, initialize it.
    DOpus.Output('Reading from config');
    vgTestRegexes=Script.config.TestRegexes;
  } else {
  // if we do, use the previous one.
    DOpus.Output('Use previously loaded');
  }
  // Check if we have a used variable.
  if (typeof giCounter==='undefined'){
  // if not, initialize it.
    giCounter=0;
  }
  giCounter++;
  scriptColData.value = giCounter;
}

test.js.txt (1.99 KB)

1 Like

This is good advice, especially for things like columns which can be called frequently.

For what it's worth, scripts can be unloaded and re-loaded (e.g. if not used for a long time), so global variables may be destroyed, but using them to cache objects makes sense, unless the objects require a huge amount of memory. Of course, scripts have to cope with Opus or the computer being restarted, so there will always be times when scripts are unloaded and later re-loaded.

[I fixed the typo you reported in the post and the attachment.]

True, which is why it is important to test the cache variables to see if they're
(still) initialized before using them.

In JScript you would use a guard like this before actually using the variable.

if (typeof variablename==='undefined'){ // note TRIPLE equal-signs (to avoid attempted type-conversions)
  // what to do if the "cache" aren't initialized.
}
// work like normal.

If they aren't initialized you'll need to initialize them just like you normally would do.
Done properly, whether the "cache" is available or not shouldn't affect anything but speed.

Globals is something I usually try to avoid, but sometimes they can be beneficial to (ab)use.

Thanks for fixing it :slight_smile:

You could probably avoid the guards by setting up globals in OnInit. However, that is usually worse than what you're doing now, since then the overhead of setting up all the globals for every script happens when Opus starts, even if the script is never actually used.

Using a "GetRegex" function or similar, which checks the guard and returns the object, creating it first if needed, can keep the code simple while still avoiding extra CPU and memory load in OnInit.

Check and initialize in its own function is good and relatively inexpensive alternative.
However check and return is potentially expensive because the check is only needed
once per execution (they don't disappear mid-execution).

Using the variable through a function could easily waste any benefits from using the "cache" in the first
place. I mean, if you have code like this:

function GetRegex(){
  if (typeof gRE==='undefined'){
    // initialize gRE here
  }
  return gRE;
}

// and some other place in the code ...

for (var i=0;i<item.count;i++){
  if (GetRegex.test(item.name)){
    ....
  }
}

it would compare the type to the string 'undefined' for every single iteration, compared to
this, which does it only once, no matter how many iterations:

if (typeof gRE==='undefined'){
  // initialize gRE here
}
for (var i=0;i<item.count;i++){
  if (gRE.test(item.name)){
    ....
  }
}

The code is just to illustrate my point, but the difference should be obvious.

I was thinking of:

[code]function GetRegex(){
if (typeof gRE==='undefined'){
// initialize gRE here
}
return gRE;
}

function SomeOtherCode(){
var gRE = GetRegex();

for (var i=0;i<item.count;i++){
if (gRE.test(item.name)){
....
}
}
}[/code]

That would work, but is there a need to create a local version of the variable?

In my opinion it would make more sense to do this:

CheckVars(); // the function checks the variable(s) before use
...use global variables directly here

which is what I mean by a check and initialize (but not return) function.

Probably makes no time or speed difference, but one style makes it harder to forget the init call when copy & pasting code, as the variable won't exist without it. Mostly a stylistic choice, though.

That's true. :slight_smile:

In this case it would have existed (same name), but that is easily attributed to copy/paste..
I guess thats kind of ironic considering the above. :slight_smile:

The variable would exist, but might not be initialised. Using a local variable ensures if it exists it has been initialised (unless you go oit of your way to declare the var and forget to init it, of course.)

If you really need/want to use global variables, I'd suggest to initialize them right there in the global scope. That code cannot be executed twice and there's no need to test for initialization-state within a function at all. It makes no sense to init a global variable in a local scope from my point of view. In cases where you want a lot of global stuff to be initialized, a dedicated function can be handy which is feeded with the global "this":

[code]//global scope
InitGlobals(this);

//accessing global "FSO" object
var file = FSO.GetFile("C:\lala.txt");

function InitGlobals( gThis ){
gThis.FSO = new ActiveXObject("Scripting.FilesystemObject");
//..
}
[/code]

While we are at globals:
A nicer way to access globals within a function is, to "save" the global context in a variable..

//save global context var gThis = this;..and use that var within any function to access the global scope. It greatly helps to differentiate between local and global scope of things and is a somewhat cleaner approach for using global variables.

function MyFunc(p1, p2){ var file = gThis.FSO.GetFile("C:\\lala.txt"); }

That all reminds me of telling, that I miss a OnBeforeAction() event, which gets called once whenever the script is about to actually do something, as there's no need to run any script-specific code in the global scope if DO is about to execute OnInit() only. I currently try/catch for "Script", which tells me DO is not just re/loading the script to prevent any unnecessary initialization in that very moment.

Globals are a must if you like your column script to perform as best as possible, so you maybe agree that initializing more complex stuff in that scope is necessary, but totally useless unless the OnOninit()/script-init-phase as been passed.

If the initialisation is expensive enough to care about when or how often it happens, you may not want to do it in the global scope as you're then causing that delay whenever the script is loaded, even if the part of the script which needs the variable is never used.

It's also good if as little as possible is done during startup, since a lot is already being done at startup and any extra delays are going to be bad for the user.

[quote]If the initialisation is expensive enough to care about when or how often it happens, you may not want to do it in the global scope as you're then causing that delay whenever the script is loaded, even if the part of the script which needs the variable is never used.

It's also good if as little as possible is done during startup, since a lot is already being done at startup and any extra delays are going to be bad for the user.[/quote]

As said, I test for existence of the Script object to take care of all that, but this methodology is not used by anyone else and basically unknown.

I think this initialization topic would get more attention and care, if DO features a dedicated function to initialize things right before calling any command or column function repeatedly. Ideally it would pass that global "this", just like in my example above.

I'm not sure what the try/catch for "Script" part meant.

I think if Opus is re-loading the script, the script's old global state is gone and initialisation is as necessary as it was the first time the script was loaded. (It must work that way, else a new version of a script which no longer had the same variables would leak the old objects.)

The global "this" seems to be the Global object.
It contains (is the parent of) DOpus, isNaN, escape, unescape, Infinity and so on.
In other words, the functions/properties you don't need to prefix.
I'm not sure why you would pass that around.

this.DOpus.Output('test') works just like DOpus.Output('test').

@leo
The Script object of DO is available only, if DO is about to actually run a command or script function. It does not exist if DO is about to run OnInit() when re-loading a script. This is an indicator for me, when to initialize various objects which are required by the script, but are not needed at the time DO reads and runs the script from disk to add it to its internal catalogue.

I think few people are aware of that fact, so most of the scripts run the full initializiation, regardless of initialization or runtime, which then can lead to the problems you mentioned above (extended loading times of DO e.g.).

The global state is not always gone and I'm really glad it is not. For column functions, the script kind of stays alive and reuses the global state for as long as there are incoming requests for column values. It needs some seconds of idleness before the scripts global context and its thread is gone. In case you have a folder format with a sorted script column e.g., DO will only run the global scope once, and then continues to call to column function for every item in the filedisplay. These executions share the global context, because the specific script (thread?) is kept busy. I think it's the same for requests to label values.

@myarmor
You can pass the global "this" to a function of your choice, to add variables and objects to the global context, even though you are in the local context of an Initialization() function e.g. You normally cannot add things to the global context from within a function. Adding items to the global scope from a function reduces clutter and verboseness of the upper parts of a script. And you're right, prefixing the globals is not required when only read-accessing them, but it helps to keep local and global things apart, especially for scripts which make heavy use of standalone integers and strings, which are not tied to some kind of easy to remember object(-name).

That is your assumption from observing an implementation detail. I don't recommend basing your code on that assumption unless we guarantee it, as it is subject to change and very much a side-effect, not part of the design.

If you need init code to be called at least once before each event handler (and don't want it in OnInit to avoid slowing down startup for things that may not be used), just write a function to do the init and call it at the start of each event handler. Make it check if the init is needed, so it's safe to call multiple times, and you're done. Just one line at the top of each event handler. It's easy to do things that way, and asking for trouble to go off that path.

Of course, global state exists as long as the script is still loaded. But the global state is gone when Opus decides to unload the script. If the script is unloaded, everything is gone, as if Opus was restarted as far as the script is concerned.

Yes, OnInit is not run once per file per column or anything like that. But it can be run more than once, since scripts can be unloaded when idle and later re-loaded when needed again.