Advertisement Create a free account

[Advice] API, Sheet Workers and Performance

1578432405
Got a few questions I've been mulling over and would like to put it to the community for your thoughts. Should sheet workers be task specific, even if several tasks must be performed on/at the same event?  Such as, if an attribute change affects 3 different and disparate other attributes, should a single sheet worker script be written for the event to handle all 3 different tasks?  Or should 3 different sheet worker scripts be written to fire on same event to specifically handle the 3 disparate tasks? From a perspective of performance, where ever possible should the API be used in lieu of a sheet worker?  I've not tried this, but would it be possible to use a sheet worker event to call an API script?  And, if so, would that perform better than using the sheet worker?  Such as, on("change:someattribute", function() { !myAPIscript });
1578444161

Edited 1578444441
Chris D.
Pro
Sheet Author
API Scripter
My understanding is that sheet workers are vastly preferable to API. For one, sheet workers are available to everybody, but API is limited to PRO accounts. For another, since sheet worker scripts are attached to the sheets, you never have to worry about somebody running incompatible versions of sheet and API. I can't answer from a strictly performance standpoint, but I have always found that sheetworkers are "fast enough" and they always get the job done soon enough. Once again, I can't say this for certain, but I would assume that sheet workers are generally faster and more reliable, since (I think) they are running on users machines, which will usually have lots of free cycles, rather than requests for hundreds of users being bundled on a few servers. I have seen the API act slowly. I have never seen sheet workers act slowly (but maybe that is just me and my machine).  So anyway, my sheet requires an API, since there are some things I want to do that need it, but I personally push as much as is convenent to the sheet workers rather than the API.  You can't cause an API event directly like you suggest, but there is a way for a sheet worker to force an API even and visa versa.  If a sheet worker sets a value, it should be cause an  on("change:attribute", to fire on the API.  If an API uses obj.setWithWorker() It should cause a worker script to fire (on one of the servers, not the users home machines, so there goes your advantage of using the free cycles there).  As to your first question, it really depends upon if you have a programming method where you feel most comfortable doing it one way or the other.  Myself I use a hybrid approach. Rather than have one routine that does everything, or rather than having separate sheetworkers, listening on the same event, my own personal practice is to have each event only call one routine, but that routine passes the job around to separate routines.  example: var taskA = function fnTaska( eventInfo ) { 'use strict'; }; var group1 = function fnGroup1( eventInfo ) { 'use strict'; taskA(); taskB(); }; on("change:repeating_talents:t_target change:repeating_knacks:nac_target change:repeating_skills:sk_target", fn( groupA )); I am not saying that this is the best way to do it, This just feels very clear and organized to me. Every event is only called once. Yet each task is done by a separate routine. 
1578448130

Edited 1578506227
Scott C.
Forum Champion
Sheet Author
API Scripter
So, here's my two cents. Consolidation Sheetworkers should, wherever possible, be consolidated. This is because both getAttrs and setAttrs are asynchronous events. This means that if you have two getAttrs that are fired from a single event, you can't control in what order they are fired. Additionally, each getAttrs call takes a noticeable amount of time to get the attribute values, and putting multiple getAttrs after each other will increase this time as each one can't fire till the call stack is empty. As an example to show how this affects performance at extreme levels. Lets say that you had an attribute change that affected 1000 other attributes. You could either A) do a getAttr for each attribute and then all the calculations you need for each one, or B) you could do a getAttrs of all 1000 at once and then do the calculations for each one. The getAttrs function takes very close to the same amount of time to get attribute values whether you're getting 1 attribute or 1000; let's say this is 50ms for the sake of this comparison. (EDIT: See addendum at bottom) A) Each getAttrs call has to be resolved independently and won't fire until the call stack is clear, so resolving all the getAttrs calls will take 50000 ms (or 50 seconds) + the time required for any calculations/logic you're doing with those values. So for all the effects of the attribute change to occur will take almost a minute B) Here, we do a single getAttrs (50ms) and a bunch of logic (which maybe takes 10's of ms). So for all the effects of the attribute change to be completed takes about 60ms (or about 0.06 seconds). This correlation also means that figuring out everything that might be affected by an attribute change and grabbing every attribute value you might need to respond to an attribute change is exponentially quicker than doing getAttrs in response to the first attribute change and then having other events respond to those changes that will do other getAttrs and potentially trigger other events. Additionally, because the asynchronous events do not fire in a defined order and doing multiple of them at once can take so long, you can run into an issue where all the changes from a given attribute change haven't occurred yet, but the user has already started making other manual changes which now get added to the list of changes and might fire before the remaining effects of the first attribute change. Here's some pseudo code examples of each of these: Multiple getAttrs from a single event (Poor Performance) on('change:attribute_name',(event)=>{     getAttrs(['control1'],(attr)=>{         setAttrs({change1:'yadda'});     });     getAttrs(['control2'],(attr)=>{         setAttrs({change2:'yadda'});     }); }); Cascading getAttrs (Worst Performance) on('change:attribute_name',(event)=>{     getAttrs(['control1'],(attr)=>{         setAttrs({change1:'yadda'});     }); }); on('change:change1',(event)=>{     getAttrs(['control2'],(attr)=>{         setAttrs({change2:'yadda'});     }); }); Get everything all at once (Best performance) on('change:attribute_name',(event)=>{     getAttrs(['control1','control2'],(attr)=>{         setAttrs({              change1:'yadda' ,             change2:'yadda'         });     }); }); Sheetworkers triggering API A sheet that is being designed to be used by the community at large should NEVER be designed to require an API script; the ones that are currently in the repo have been grandfathered in. Requiring an API script makes your sheet unusable by non-pro users. Additionally, the sheetworkers cannot trigger an API script in the way you're suggesting. Sheetworkers can't send any messages to chat, although they can trigger API scripts by causing changes to attributes; however this should not be relied upon if you are designing your sheet to require an API because the API won't be able to respond to these changes if the original event that caused the change was issued by the API itself. Essentially, you can do either of: User caused change -> API response User caused change -> Sheetworker response and change -> API response But you can't do either of: API change -> API response API change -> Sheetworker Response -> API Response I hope that helps, Scott ADDENDUM: The 50 ms I used in the example is much longer than the getAttrs actually takes. A getAttrs takes just a couple milliseconds. This means that doing multiple concurrent getAttrs from a single event is actually not horribly inefficient, although is still less efficient than combining all of them. However, the setAttrs function takes ~100ms per run, so doing the cascading getAttrs method is actually much worse than any other method.
1578692840
Chris and Scott, thank you very much for your informative replies!  This is great as it helps put a lot in perspective but at the time a bit mind bending. :) From a strategic context I fully understand what both of you have suggested.  From a tactical context I'm struggling to grok your pseudo codes (not a software engineer by trade, but hobbyist without an orthodox education in coding).  Scott, I looked into your use of arrows in javascript and believe I now understand how this works.  I'll experiment.  I believe my performance issues are mainly due to me using many setAttr() functions in my scripts.  It never occurred to me that I could write to different variables in one setAttr() call.  This is my "come to Jesus" moment.  ;) Chris, what the shizzle?!  I'm trying to wrap my head around how I'd even begin to use TaskA or group1 in your examples.  :'(  The longer I stare at it, it almost starts to make sense and then I feel my sanity about to snap!  Like I said, I get where you're going with this and I think this architecture would actually be much easier to maintain and troubleshoot.  I'm just not quite sure how I'd follow your example.  Some homework is in order. You've both given me quite a bit to think about, and I've got a significant amount of refactoring to do. Thanks again guys!  I really appreciate you taking the time to respond.
1578698997
Scott C.
Forum Champion
Sheet Author
API Scripter
Heh, I'm not a professional coder either. Most of what I know, I learned from coding things on R20 (thanks for all the instruction Aaron!). And, yeah, the fat arrow is really just a shorthand for function declarations. As for Chris' example, it's really a natural outgrowth of using minimum setAttrs and minimum getAttrs. You have one function that looks at what change came in and then decides what to do. Some of those things that need to be done may be needed in other functions, or just work better if you can separate them out. I tend to have a single listener that reacts to ~90% of the attributes on my sheet. That listener then calls a function which looks at the event and decides what to do, assembles some needed information and then sends it all to a variety of functions that do the actual calculations.