The Perils of Sheet Worker Functions

I thought I had finished the sheet worker series, but a forum thread just reminded me of something I hadn’t mentioned. This is a technique that many character sheets use, but which most of them shouldn’t, at least not the wayt hey do. They are actively making their sheets worse.

As a reminder, here’s what a simple sheet worker might look like.

on('change:str', function() {
   getAttrs(['str'], function(values) {
      var score = +values.str || 0;
      var mod = Math.floor(score/2) -5;
      setAttrs({
         str_mod: mod
      });
   });
});Code language: HTML, XML (xml)

This is a simple sheet worker to calculate a modifer for the strength score. If you have several stats which each have a modifier calculated the same way, you might wonder of theseres’s a way to simplify that. So you might end up with a function like this:

function calc_stat_mods(stat_name) {
   getAttrs([stat_name], function(values) {
      var score = +values[stat_name] || 0;
      var mod = Math.floor(score/2) -5;
      setAttrs({
         [stat_name + "_mod"]: mod
      });
   });
});Code language: HTML, XML (xml)

This looks like a useful function. It passes the attribute name, and you could easily call in separate workers like this:

on('change:str', function() { calc_stat_mods('str') });
on('change:dex', function() { calc_stat_mods('dex') });
on('change:con', function() { calc_stat_mods('con') });
on('change:int', function() { calc_stat_mods('int') });
on('change:wis', function() { calc_stat_mods('wis') });
on('change:cha', function() { calc_stat_mods('cha') });Code language: HTML, XML (xml)

That is actually perfectly fine. But it also opens the way to putting that function in a loop:

on('change:str change:dex change:con change:int change:wis change:cha', function() { 
   ['str', 'dex', 'con', 'int', 'wis', 'cha'].forEach(stat => {
      calc_stat_mods(stat);
   }); 
});Code language: HTML, XML (xml)

No sheet would do it exactly like this, but something similar happens in a lot of sheets. Every time any one of the siz stats changes, a loop runs, calling that function for all six stats, one after another. If there are similar functions for calculating skill bonuses, they’ll also be triggered to run.

This is very bad. This means that getAttrs and setAttrs are inside a loop. You should never do this if you can avoid it. Why not?

Asynchronous Functions

Certain functions are described as asynchronous, like getAttrs, setAttrs, and getSectionIDs. What does that mean?

Most code is synchronous and linear – it runs in the order you have it listed in your worker, one line after another. This code can be as messy as you want – efficiency really don’t matter. All the code is run on your own computer, and even the weakest computer today will be more than capable of running any chatracter sheet code you throw at it.

But roll20’s asynchronous functions are difference. The first thing they do is send out a message to roll20’s internet servers and wait for a response. Like when you ask what the value of the ‘str’ attribute is, Roll20 has to consult a database – find the campaign, the character, and the attribute, and then return that value to your browser.

Now imagine there are thhousands or even millions of people playing games on Roll20 and all of them might be running such functions. Your request is put in a queue, and when the Roll20 servers get through everyone ahead of you, they can handle your request and return the value to you.

Now imgaine you do six separate requests, one for each attribute, and then many extra requests for each of the skills, saving throws, attacks and so on based on that attribute. This can be very slow, and your character sheet basically hangs until those repsonses are processed.

A good thing about most asynchronous functions is the way you can bundle different requests into a single request. You can do a getAttrs([‘str’, ‘dex’, ‘con’, etc. instead of a seoarate getAttrs for each stat. This is basically just as fast as requesting the value of one attribute.

So wherever possible, you should look at your code, and see how you can reduce the number of asynchronous functions used – especially the number that would be called at the same time. This leads to the following commandment:

Don’t put getAttrs and setAttrs in a loop.

It’s not actually a commandment, but it should be.

Solving This Problem

There are several ways to handle this issue. A big one is to rewrite your code to avoid cascade effects. A cascade is when one sheet worker triggers another, and that triggers another, etc. For example, when you change STR, that change is registered, then all the skills and attacks based on STR are also each run.

It would be more efficient to have a single worker that contains everything dependent on STR, another that contains everything dependent on CON, and so on.

You might have ratings that are depending on both STR and CON, in which case you’ll need a worker that handles that case (and runs when either of those attributes change). This can easily lead to having a single massive worker that contains everything.

This is actually faster than having multiple workers designed to cascade one after the other, because each separate getAttrs and setAttrs takes time, and in this worker you likely only have one of each.

Personally I like cascading effects. They are slower but I think Roll20 is built to take then into account. A small number of cacsades aren’t going to cause noticeable lag. But it does mean you need some awareness of your sheet – limiting how long cascade chains can get.

The example at the start of this post, where you have cascades being triggered by core attributes, can easily lead to very long cascade chains because the core attributes are used by everything. You need to be careful if playing a game like D&D where characters can have dozens of attributes, attacks, and spells, and your sheet worker design is sloppy enough that some code is being calculated and recalculated over and over. (This happens easily.)

And you need to watch out for loops which contain getAttrs and setAttrs. Avoid them at all costs.

Functions which return a value

Everything said uo to this point describes the situation and solution. But it is handy sometimes to know how to write better functions than those described above. The key is to move the getAttrs and setAttrs out of the function – keep that in the sheet worker.

function calc_stat_mods(stat, values) {
      var score = +values[stat] || 0;
      var mod = Math.floor(score/2) -5;
});

on('change:str change:dex change:con change:int change:wis change:cha', function() { 
   var stats = ['str', 'dex', 'con', 'int', 'wis', 'cha'];
   getAttrs(stats, function(values) {
      var output = {};
      stats.forEach(stat => {
         output[stat + "_mod"] = calc_stat_mods(stat, values);
      });
      setAttrs(output);
   }); 
});Code language: HTML, XML (xml)

This function is a duplicate of the earkier loop, were it loops through the 6 stats and calculates the modifier for all of them when any attribute changes, then updates the attributes on the sheet.

But it avoids putting getAttrs and setAttrs inside a loop – there is only one getAttrs call, and one setAttrs call (all the new values are saved in an output variable which is sent to setAttrs).

If you compare the two workers, you can see they are extremely similar, but the second one if more efficient.In a more practical situation, you’d also include skills and other ratings, and calculate them all at once. This is a proof of concept.

Conclusion

There is a simple conclusion – rewrite your code to avoid these kinds of functions, and if you need them, don’t include getAttrs and setAttrs in the function itself. Build the function so it doesnt need them, but returns a value instead. Remember the prime commandment:

Don’t put getAttrs and setAttrs in a loop.

Series NavigationThe Script Block and Identifying Characters >>

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.